Browse Source

Megre with develop/3.6 to include flaky test fixes

pull/9155/head
Dmytro Skarzhynets 3 years ago
parent
commit
0cd6edfaae
  1. 267
      application/src/main/data/json/demo/dashboards/firmware.json
  2. 234
      application/src/main/data/json/demo/dashboards/gateways.json
  3. 82
      application/src/main/data/json/demo/dashboards/rule_engine_statistics.json
  4. 267
      application/src/main/data/json/demo/dashboards/software.json
  5. 229
      application/src/main/data/json/demo/dashboards/thermostats.json
  6. 2
      application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json
  7. 2
      application/src/main/data/json/tenant/device_profile/rule_chain_template.json
  8. 2
      application/src/main/data/json/tenant/rule_chains/root_rule_chain.json
  9. 49
      application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java
  10. 8
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  11. 54
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  12. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java
  13. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java
  14. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java
  15. 5
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java
  16. 21
      application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
  17. 68
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  18. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java
  19. 4
      application/src/main/resources/thingsboard.yml
  20. 4
      application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerTest.java
  21. 144
      application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerWithDefaultPortTest.java
  22. 306
      application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java
  23. 92
      application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AbstractAssetProcessorTest.java
  24. 44
      application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorTest.java
  25. 43
      application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessorTest.java
  26. 100
      application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/AbstractDeviceProcessorTest.java
  27. 43
      application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessorTest.java
  28. 45
      application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessorTest.java
  29. 119
      application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java
  30. 23
      common/cluster-api/src/main/proto/queue.proto
  31. 9
      common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java
  32. 6
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpDeviceProfileTransportConfiguration.java
  33. 4
      common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/importing/csv/BulkImportColumnType.java
  34. 18
      common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpCommunicationSpec.java
  35. 3
      common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpMethod.java
  36. 3
      common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/SnmpCommunicationConfig.java
  37. 29
      common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/ToServerRpcRequestSnmpCommunicationConfig.java
  38. 32
      common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java
  39. 2
      common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java
  40. 1
      common/edge-api/src/main/proto/edge.proto
  41. 50
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DateFormattingOptions.java
  42. 84
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java
  43. 182
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java
  44. 74
      common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java
  45. 97
      common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/PduService.java
  46. 221
      common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java
  47. 31
      common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/DeviceSessionContext.java
  48. 23
      common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpDeviceSimulatorV2.java
  49. 43
      common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpTestV2.java
  50. 7
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/TransportService.java
  51. 49
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
  52. 7
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceConnectivityServiceImpl.java
  53. 23
      dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java
  54. 3
      dao/src/test/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidatorTest.java
  55. 2
      docker/docker-compose.yml
  56. 1
      docker/tb-snmp-transport.env
  57. 4
      transport/snmp/src/main/resources/tb-snmp-transport.yml
  58. 13
      ui-ngx/src/app/core/translate/missing-translate-handler.ts
  59. 8
      ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts
  60. 4
      ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.ts
  61. 330
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html
  62. 72
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss
  63. 66
      ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts
  64. 31
      ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html
  65. 18
      ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts
  66. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.html
  67. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.ts
  68. 2
      ui-ngx/src/app/shared/components/color-input.component.html
  69. 83
      ui-ngx/src/app/shared/components/color-input.component.ts
  70. 12
      ui-ngx/src/app/shared/models/device.models.ts
  71. 848
      ui-ngx/src/assets/dashboard/api_usage.json
  72. 45
      ui-ngx/src/assets/dashboard/customer_user_home_page.json
  73. 250
      ui-ngx/src/assets/dashboard/sys_admin_home_page.json
  74. 144
      ui-ngx/src/assets/dashboard/tenant_admin_home_page.json
  75. 15
      ui-ngx/src/assets/locale/locale.constant-en_US.json

267
application/src/main/data/json/demo/dashboards/firmware.json

@ -1,24 +1,34 @@
{
"title": "Firmware",
"image": null,
"mobileHide": false,
"mobileOrder": null,
"configuration": {
"description": "",
"widgets": {
"cd03188e-cd9d-9601-fd57-da4cb95fc016": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "entities_table",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5,
"sizeY": 6.5,
"config": {
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 86400000
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085363133,
"endTimeMs": 1694171763133
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "NONE",
@ -299,16 +309,11 @@
},
"row": 0,
"col": 0,
"id": "cd03188e-cd9d-9601-fd57-da4cb95fc016"
"id": "cd03188e-cd9d-9601-fd57-da4cb95fc016",
"typeFullFqn": "system.cards.entities_table"
},
"100b756c-0082-6505-3ae1-3603e6deea48": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "timeseries_table",
"type": "timeseries",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 8,
"sizeY": 6.5,
"config": {
@ -461,14 +466,11 @@
},
"row": 0,
"col": 0,
"id": "100b756c-0082-6505-3ae1-3603e6deea48"
"id": "100b756c-0082-6505-3ae1-3603e6deea48",
"typeFullFqn": "system.cards.timeseries_table"
},
"17543c57-af4a-2c1e-bf12-53a7b46791e6": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "html_value_card",
"type": "latest",
"title": "New widget",
"sizeX": 8,
"sizeY": 3,
"config": {
@ -496,8 +498,27 @@
}
],
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"timewindowMs": 60000
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085363133,
"endTimeMs": 1694171763133
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
}
},
"showTitle": false,
@ -545,14 +566,11 @@
"enableDataExport": false,
"displayTimewindow": true
},
"id": "17543c57-af4a-2c1e-bf12-53a7b46791e6"
"id": "17543c57-af4a-2c1e-bf12-53a7b46791e6",
"typeFullFqn": "system.cards.html_value_card"
},
"6c1c4e1a-bce0-f5ad-ff8b-ba1dfc5a4ec6": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "html_value_card",
"type": "latest",
"title": "New widget",
"sizeX": 8,
"sizeY": 3,
"config": {
@ -580,8 +598,27 @@
}
],
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"timewindowMs": 60000
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085363133,
"endTimeMs": 1694171763133
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
}
},
"showTitle": false,
@ -629,14 +666,11 @@
"enableDataExport": false,
"displayTimewindow": true
},
"id": "6c1c4e1a-bce0-f5ad-ff8b-ba1dfc5a4ec6"
"id": "6c1c4e1a-bce0-f5ad-ff8b-ba1dfc5a4ec6",
"typeFullFqn": "system.cards.html_value_card"
},
"e6674227-9cf3-a2f6-ecac-5ccfc38a3c81": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "html_value_card",
"type": "latest",
"title": "New widget",
"sizeX": 8,
"sizeY": 3,
"config": {
@ -664,8 +698,27 @@
}
],
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"timewindowMs": 60000
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085363133,
"endTimeMs": 1694171763133
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
}
},
"showTitle": false,
@ -713,14 +766,11 @@
"enableDataExport": false,
"displayTimewindow": true
},
"id": "e6674227-9cf3-a2f6-ecac-5ccfc38a3c81"
"id": "e6674227-9cf3-a2f6-ecac-5ccfc38a3c81",
"typeFullFqn": "system.cards.html_value_card"
},
"77b10144-b904-edd5-8c7c-8fb75616c6d8": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "html_value_card",
"type": "latest",
"title": "New widget",
"sizeX": 8,
"sizeY": 3,
"config": {
@ -748,8 +798,27 @@
}
],
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"timewindowMs": 60000
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085363133,
"endTimeMs": 1694171763133
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
}
},
"showTitle": false,
@ -797,23 +866,32 @@
"enableDataExport": false,
"displayTimewindow": true
},
"id": "77b10144-b904-edd5-8c7c-8fb75616c6d8"
"id": "77b10144-b904-edd5-8c7c-8fb75616c6d8",
"typeFullFqn": "system.cards.html_value_card"
},
"21be08bb-ec90-f760-ad6f-e7678f12c401": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "entities_table",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5,
"sizeY": 6.5,
"config": {
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 86400000
"timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085363133,
"endTimeMs": 1694171763133
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "NONE",
@ -1094,23 +1172,32 @@
},
"row": 0,
"col": 0,
"id": "21be08bb-ec90-f760-ad6f-e7678f12c401"
"id": "21be08bb-ec90-f760-ad6f-e7678f12c401",
"typeFullFqn": "system.cards.entities_table"
},
"e8280043-d3dc-7acb-c2ff-a4522972ff91": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "entities_table",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5,
"sizeY": 6.5,
"config": {
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 86400000
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085363133,
"endTimeMs": 1694171763133
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "NONE",
@ -1391,23 +1478,32 @@
},
"row": 0,
"col": 0,
"id": "e8280043-d3dc-7acb-c2ff-a4522972ff91"
"id": "e8280043-d3dc-7acb-c2ff-a4522972ff91",
"typeFullFqn": "system.cards.entities_table"
},
"3624013b-378c-f110-5eba-ae95c25a4dcc": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "entities_table",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5,
"sizeY": 6.5,
"config": {
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 86400000
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085363133,
"endTimeMs": 1694171763133
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "NONE",
@ -1688,23 +1784,32 @@
},
"row": 0,
"col": 0,
"id": "3624013b-378c-f110-5eba-ae95c25a4dcc"
"id": "3624013b-378c-f110-5eba-ae95c25a4dcc",
"typeFullFqn": "system.cards.entities_table"
},
"d2d13e0d-4e71-889f-9343-ad2f0af9f176": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "entities_table",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5,
"sizeY": 6.5,
"config": {
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 86400000
"timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085363133,
"endTimeMs": 1694171763133
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "NONE",
@ -1985,7 +2090,8 @@
},
"row": 0,
"col": 0,
"id": "d2d13e0d-4e71-889f-9343-ad2f0af9f176"
"id": "d2d13e0d-4e71-889f-9343-ad2f0af9f176",
"typeFullFqn": "system.cards.entities_table"
}
},
"states": {
@ -2035,7 +2141,8 @@
"autoFillHeight": true,
"backgroundImageUrl": null,
"mobileAutoFillHeight": true,
"mobileRowHeight": 70
"mobileRowHeight": 70,
"outerMargin": true
}
}
}
@ -2062,7 +2169,8 @@
"autoFillHeight": true,
"backgroundImageUrl": null,
"mobileAutoFillHeight": false,
"mobileRowHeight": 70
"mobileRowHeight": 70,
"outerMargin": true
}
}
}
@ -2089,7 +2197,8 @@
"autoFillHeight": true,
"backgroundImageUrl": null,
"mobileAutoFillHeight": false,
"mobileRowHeight": 70
"mobileRowHeight": 70,
"outerMargin": true
}
}
}
@ -2116,7 +2225,8 @@
"autoFillHeight": true,
"backgroundImageUrl": null,
"mobileAutoFillHeight": false,
"mobileRowHeight": 70
"mobileRowHeight": 70,
"outerMargin": true
}
}
}
@ -2143,7 +2253,8 @@
"autoFillHeight": true,
"backgroundImageUrl": null,
"mobileAutoFillHeight": false,
"mobileRowHeight": 70
"mobileRowHeight": 70,
"outerMargin": true
}
}
}
@ -2170,7 +2281,8 @@
"autoFillHeight": true,
"backgroundImageUrl": null,
"mobileAutoFillHeight": false,
"mobileRowHeight": 70
"mobileRowHeight": 70,
"outerMargin": true
}
}
}
@ -2488,5 +2600,6 @@
"showUpdateDashboardImage": false
}
},
"externalId": null,
"name": "Firmware"
}

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

@ -1,20 +1,33 @@
{
"title": "Gateways",
"image": null,
"mobileHide": false,
"mobileOrder": null,
"configuration": {
"widgets": {
"94715984-ae74-76e4-20b7-2f956b01ed80": {
"isSystemType": true,
"bundleAlias": "entity_admin_widgets",
"typeAlias": "device_admin_table",
"type": "latest",
"title": "New widget",
"sizeX": 24,
"sizeY": 12,
"config": {
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 86400000
"timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085270425,
"endTimeMs": 1694171670425
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "NONE",
@ -187,14 +200,11 @@
]
}
},
"id": "94715984-ae74-76e4-20b7-2f956b01ed80"
"id": "94715984-ae74-76e4-20b7-2f956b01ed80",
"typeFullFqn": "system.entity_admin_widgets.device_admin_table"
},
"eadabbc7-519e-76fc-ba10-b3fe8c18da10": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "timeseries_table",
"type": "timeseries",
"title": "New widget",
"sizeX": 14,
"sizeY": 13,
"config": {
@ -255,14 +265,11 @@
"titleTooltip": "",
"displayTimewindow": true
},
"id": "eadabbc7-519e-76fc-ba10-b3fe8c18da10"
"id": "eadabbc7-519e-76fc-ba10-b3fe8c18da10",
"typeFullFqn": "system.cards.timeseries_table"
},
"f928afc4-30d1-8d0c-e3cf-777f9f9d1155": {
"isSystemType": true,
"bundleAlias": "charts",
"typeAlias": "basic_timeseries",
"type": "timeseries",
"title": "New widget",
"sizeX": 17,
"sizeY": 4,
"config": {
@ -375,6 +382,15 @@
"xaxisSecond": {
"axisPosition": "top",
"showLabels": true
},
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "right",
"showMin": true,
"showMax": true,
"showAvg": true,
"showTotal": true
}
},
"title": "Real time information",
@ -393,25 +409,13 @@
"widgetStyle": {},
"useDashboardTimewindow": false,
"displayTimewindow": true,
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "right",
"showMin": true,
"showMax": true,
"showAvg": true,
"showTotal": true
},
"actions": {}
},
"id": "f928afc4-30d1-8d0c-e3cf-777f9f9d1155"
"id": "f928afc4-30d1-8d0c-e3cf-777f9f9d1155",
"typeFullFqn": "system.charts.basic_timeseries"
},
"2a95b473-042d-59d0-2da2-40d0cccb6c8a": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "timeseries_table",
"type": "timeseries",
"title": "New widget",
"sizeX": 7,
"sizeY": 7,
"config": {
@ -491,14 +495,11 @@
"showTotal": false
}
},
"id": "2a95b473-042d-59d0-2da2-40d0cccb6c8a"
"id": "2a95b473-042d-59d0-2da2-40d0cccb6c8a",
"typeFullFqn": "system.cards.timeseries_table"
},
"aaa69366-aacc-9028-65aa-645c0f8533ec": {
"isSystemType": true,
"bundleAlias": "charts",
"typeAlias": "basic_timeseries",
"type": "timeseries",
"title": "New widget",
"sizeX": 17,
"sizeY": 4,
"config": {
@ -604,6 +605,15 @@
"xaxisSecond": {
"axisPosition": "top",
"showLabels": true
},
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "right",
"showMin": true,
"showMax": true,
"showAvg": true,
"showTotal": true
}
},
"title": "History information",
@ -622,25 +632,13 @@
"widgetStyle": {},
"useDashboardTimewindow": true,
"displayTimewindow": true,
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "right",
"showMin": true,
"showMax": true,
"showAvg": true,
"showTotal": true
},
"actions": {}
},
"id": "aaa69366-aacc-9028-65aa-645c0f8533ec"
"id": "aaa69366-aacc-9028-65aa-645c0f8533ec",
"typeFullFqn": "system.charts.basic_timeseries"
},
"ce5c7d01-a3ef-5cf0-4578-8505135c23a0": {
"isSystemType": true,
"bundleAlias": "charts",
"typeAlias": "basic_timeseries",
"type": "timeseries",
"title": "New widget",
"sizeX": 17,
"sizeY": 4,
"config": {
@ -751,6 +749,15 @@
"xaxisSecond": {
"axisPosition": "top",
"showLabels": true
},
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "right",
"showMin": true,
"showMax": true,
"showAvg": true,
"showTotal": true
}
},
"title": "Real time information",
@ -769,25 +776,13 @@
"widgetStyle": {},
"useDashboardTimewindow": false,
"displayTimewindow": true,
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "right",
"showMin": true,
"showMax": true,
"showAvg": true,
"showTotal": true
},
"actions": {}
},
"id": "ce5c7d01-a3ef-5cf0-4578-8505135c23a0"
"id": "ce5c7d01-a3ef-5cf0-4578-8505135c23a0",
"typeFullFqn": "system.charts.basic_timeseries"
},
"466f046d-6005-a168-b107-60fcb2469cd5": {
"isSystemType": true,
"bundleAlias": "gateway_widgets",
"typeAlias": "attributes_card",
"type": "latest",
"title": "New widget",
"sizeX": 7,
"sizeY": 5,
"config": {
@ -799,8 +794,27 @@
}
],
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"timewindowMs": 60000
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085270425,
"endTimeMs": 1694171670425
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
}
},
"showTitle": true,
@ -832,14 +846,11 @@
"showLegend": false,
"actions": {}
},
"id": "466f046d-6005-a168-b107-60fcb2469cd5"
"id": "466f046d-6005-a168-b107-60fcb2469cd5",
"typeFullFqn": "system.gateway_widgets.attributes_card"
},
"8fc32225-164f-3258-73f7-e6b6d959cf0b": {
"isSystemType": true,
"bundleAlias": "gateway_widgets",
"typeAlias": "config_form_latest",
"type": "latest",
"title": "New widget",
"sizeX": 10,
"sizeY": 9,
"config": {
@ -851,8 +862,27 @@
}
],
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"timewindowMs": 60000
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085270425,
"endTimeMs": 1694171670425
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
}
},
"showTitle": true,
@ -881,14 +911,11 @@
"showLegend": false,
"actions": {}
},
"id": "8fc32225-164f-3258-73f7-e6b6d959cf0b"
"id": "8fc32225-164f-3258-73f7-e6b6d959cf0b",
"typeFullFqn": "system.gateway_widgets.config_form_latest"
},
"063fc179-c9fd-f952-e714-f24e9c43c05c": {
"isSystemType": true,
"bundleAlias": "control_widgets",
"typeAlias": "rpcbutton",
"type": "rpc",
"title": "New widget",
"sizeX": 4,
"sizeY": 2,
"config": {
@ -930,14 +957,11 @@
"b2487e75-2fa4-f211-142c-434dfd50c70c"
]
},
"id": "063fc179-c9fd-f952-e714-f24e9c43c05c"
"id": "063fc179-c9fd-f952-e714-f24e9c43c05c",
"typeFullFqn": "system.control_widgets.rpcbutton"
},
"3c2134cc-27a0-93e1-dbe1-2fa7c1ce16b7": {
"isSystemType": true,
"bundleAlias": "control_widgets",
"typeAlias": "rpcbutton",
"type": "rpc",
"title": "New widget",
"sizeX": 4,
"sizeY": 2,
"config": {
@ -979,14 +1003,11 @@
"b2487e75-2fa4-f211-142c-434dfd50c70c"
]
},
"id": "3c2134cc-27a0-93e1-dbe1-2fa7c1ce16b7"
"id": "3c2134cc-27a0-93e1-dbe1-2fa7c1ce16b7",
"typeFullFqn": "system.control_widgets.rpcbutton"
},
"6770b6ba-eff8-df05-75f8-c1f9326d4842": {
"isSystemType": true,
"bundleAlias": "input_widgets",
"typeAlias": "markers_placement_openstreetmap",
"type": "latest",
"title": "New widget",
"sizeX": 6,
"sizeY": 4,
"config": {
@ -1015,8 +1036,27 @@
}
],
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"timewindowMs": 60000
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085270425,
"endTimeMs": 1694171670425
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
}
},
"showTitle": false,
@ -1083,7 +1123,8 @@
"titleTooltip": "",
"displayTimewindow": true
},
"id": "6770b6ba-eff8-df05-75f8-c1f9326d4842"
"id": "6770b6ba-eff8-df05-75f8-c1f9326d4842",
"typeFullFqn": "system.input_widgets.markers_placement_openstreetmap"
}
},
"states": {
@ -1108,7 +1149,8 @@
"autoFillHeight": true,
"mobileAutoFillHeight": false,
"mobileRowHeight": 70,
"margin": 10
"margin": 10,
"outerMargin": true
}
}
}
@ -1158,7 +1200,8 @@
"autoFillHeight": true,
"mobileAutoFillHeight": false,
"mobileRowHeight": 70,
"margin": 10
"margin": 10,
"outerMargin": true
}
}
}
@ -1211,7 +1254,8 @@
"autoFillHeight": true,
"mobileAutoFillHeight": true,
"mobileRowHeight": 70,
"margin": 10
"margin": 10,
"outerMargin": true
}
}
}
@ -1224,8 +1268,10 @@
"filter": {
"type": "deviceType",
"resolveMultiple": true,
"deviceType": "gateway",
"deviceNameFilter": ""
"deviceNameFilter": "",
"deviceTypes": [
"gateway"
]
}
},
"b2487e75-2fa4-f211-142c-434dfd50c70c": {
@ -1261,7 +1307,9 @@
"showDashboardExport": true,
"toolbarAlwaysOpen": true,
"titleColor": "rgba(0,0,0,0.870588)"
}
},
"filters": {}
},
"externalId": null,
"name": "Gateways"
}
}

82
application/src/main/data/json/demo/dashboards/rule_engine_statistics.json

@ -1,13 +1,12 @@
{
"title": "Rule Engine Statistics",
"image": null,
"mobileHide": false,
"mobileOrder": null,
"configuration": {
"widgets": {
"81987f19-3eac-e4ce-b790-d96e9b54d9a0": {
"isSystemType": true,
"bundleAlias": "charts",
"typeAlias": "basic_timeseries",
"type": "timeseries",
"title": "New widget",
"sizeX": 12,
"sizeY": 7,
"config": {
@ -151,6 +150,15 @@
"xaxisSecond": {
"axisPosition": "top",
"showLabels": true
},
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "bottom",
"showMin": true,
"showMax": true,
"showAvg": false,
"showTotal": true
}
},
"title": "Queue Stats",
@ -169,25 +177,13 @@
"widgetStyle": {},
"useDashboardTimewindow": false,
"displayTimewindow": true,
"showLegend": true,
"actions": {},
"legendConfig": {
"direction": "column",
"position": "bottom",
"showMin": true,
"showMax": true,
"showAvg": false,
"showTotal": true
}
"actions": {}
},
"id": "81987f19-3eac-e4ce-b790-d96e9b54d9a0"
"id": "81987f19-3eac-e4ce-b790-d96e9b54d9a0",
"typeFullFqn": "system.charts.basic_timeseries"
},
"5eb79712-5c24-3060-7e4f-6af36b8f842d": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "timeseries_table",
"type": "timeseries",
"title": "New widget",
"sizeX": 24,
"sizeY": 5,
"config": {
@ -272,14 +268,11 @@
"titleTooltip": "",
"displayTimewindow": true
},
"id": "5eb79712-5c24-3060-7e4f-6af36b8f842d"
"id": "5eb79712-5c24-3060-7e4f-6af36b8f842d",
"typeFullFqn": "system.cards.timeseries_table"
},
"ad3f1417-87a8-750e-fc67-49a2de1466d4": {
"isSystemType": true,
"bundleAlias": "charts",
"typeAlias": "basic_timeseries",
"type": "timeseries",
"title": "New widget",
"sizeX": 12,
"sizeY": 7,
"config": {
@ -393,6 +386,15 @@
"xaxisSecond": {
"axisPosition": "top",
"showLabels": true
},
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "bottom",
"showMin": true,
"showMax": true,
"showAvg": false,
"showTotal": true
}
},
"title": "Processing Failures and Timeouts",
@ -411,18 +413,10 @@
"widgetStyle": {},
"useDashboardTimewindow": false,
"displayTimewindow": true,
"showLegend": true,
"actions": {},
"legendConfig": {
"direction": "column",
"position": "bottom",
"showMin": true,
"showMax": true,
"showAvg": false,
"showTotal": true
}
"actions": {}
},
"id": "ad3f1417-87a8-750e-fc67-49a2de1466d4"
"id": "ad3f1417-87a8-750e-fc67-49a2de1466d4",
"typeFullFqn": "system.charts.basic_timeseries"
}
},
"states": {
@ -457,14 +451,12 @@
"backgroundColor": "#eeeeee",
"color": "rgba(0,0,0,0.870588)",
"columns": 24,
"margins": [
10,
10
],
"backgroundSizeMode": "100%",
"autoFillHeight": true,
"mobileAutoFillHeight": false,
"mobileRowHeight": 70
"mobileRowHeight": 70,
"margin": 10,
"outerMargin": true
}
}
}
@ -477,8 +469,10 @@
"filter": {
"type": "assetType",
"resolveMultiple": true,
"assetType": "TbServiceQueue",
"assetNameFilter": ""
"assetNameFilter": "",
"assetTypes": [
"TbServiceQueue"
]
}
}
},
@ -514,7 +508,9 @@
"showDashboardTimewindow": true,
"showDashboardExport": true,
"toolbarAlwaysOpen": true
}
},
"filters": {}
},
"externalId": null,
"name": "Rule Engine Statistics"
}

267
application/src/main/data/json/demo/dashboards/software.json

@ -1,24 +1,34 @@
{
"title": "Software",
"image": null,
"mobileHide": false,
"mobileOrder": null,
"configuration": {
"description": "",
"widgets": {
"cd03188e-cd9d-9601-fd57-da4cb95fc016": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "entities_table",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5,
"sizeY": 6.5,
"config": {
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 86400000
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085343279,
"endTimeMs": 1694171743279
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "NONE",
@ -299,16 +309,11 @@
},
"row": 0,
"col": 0,
"id": "cd03188e-cd9d-9601-fd57-da4cb95fc016"
"id": "cd03188e-cd9d-9601-fd57-da4cb95fc016",
"typeFullFqn": "system.cards.entities_table"
},
"100b756c-0082-6505-3ae1-3603e6deea48": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "timeseries_table",
"type": "timeseries",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 8,
"sizeY": 6.5,
"config": {
@ -461,14 +466,11 @@
},
"row": 0,
"col": 0,
"id": "100b756c-0082-6505-3ae1-3603e6deea48"
"id": "100b756c-0082-6505-3ae1-3603e6deea48",
"typeFullFqn": "system.cards.timeseries_table"
},
"17543c57-af4a-2c1e-bf12-53a7b46791e6": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "html_value_card",
"type": "latest",
"title": "New widget",
"sizeX": 8,
"sizeY": 3,
"config": {
@ -496,8 +498,27 @@
}
],
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"timewindowMs": 60000
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085343280,
"endTimeMs": 1694171743280
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
}
},
"showTitle": false,
@ -545,14 +566,11 @@
"enableDataExport": false,
"displayTimewindow": true
},
"id": "17543c57-af4a-2c1e-bf12-53a7b46791e6"
"id": "17543c57-af4a-2c1e-bf12-53a7b46791e6",
"typeFullFqn": "system.cards.html_value_card"
},
"6c1c4e1a-bce0-f5ad-ff8b-ba1dfc5a4ec6": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "html_value_card",
"type": "latest",
"title": "New widget",
"sizeX": 8,
"sizeY": 3,
"config": {
@ -580,8 +598,27 @@
}
],
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"timewindowMs": 60000
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085343280,
"endTimeMs": 1694171743280
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
}
},
"showTitle": false,
@ -629,14 +666,11 @@
"enableDataExport": false,
"displayTimewindow": true
},
"id": "6c1c4e1a-bce0-f5ad-ff8b-ba1dfc5a4ec6"
"id": "6c1c4e1a-bce0-f5ad-ff8b-ba1dfc5a4ec6",
"typeFullFqn": "system.cards.html_value_card"
},
"e6674227-9cf3-a2f6-ecac-5ccfc38a3c81": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "html_value_card",
"type": "latest",
"title": "New widget",
"sizeX": 8,
"sizeY": 3,
"config": {
@ -664,8 +698,27 @@
}
],
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"timewindowMs": 60000
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085343280,
"endTimeMs": 1694171743280
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
}
},
"showTitle": false,
@ -713,14 +766,11 @@
"enableDataExport": false,
"displayTimewindow": true
},
"id": "e6674227-9cf3-a2f6-ecac-5ccfc38a3c81"
"id": "e6674227-9cf3-a2f6-ecac-5ccfc38a3c81",
"typeFullFqn": "system.cards.html_value_card"
},
"77b10144-b904-edd5-8c7c-8fb75616c6d8": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "html_value_card",
"type": "latest",
"title": "New widget",
"sizeX": 8,
"sizeY": 3,
"config": {
@ -748,8 +798,27 @@
}
],
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"timewindowMs": 60000
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085343280,
"endTimeMs": 1694171743280
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
}
},
"showTitle": false,
@ -797,23 +866,32 @@
"enableDataExport": false,
"displayTimewindow": true
},
"id": "77b10144-b904-edd5-8c7c-8fb75616c6d8"
"id": "77b10144-b904-edd5-8c7c-8fb75616c6d8",
"typeFullFqn": "system.cards.html_value_card"
},
"21be08bb-ec90-f760-ad6f-e7678f12c401": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "entities_table",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5,
"sizeY": 6.5,
"config": {
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 86400000
"timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085343280,
"endTimeMs": 1694171743280
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "NONE",
@ -1094,23 +1172,32 @@
},
"row": 0,
"col": 0,
"id": "21be08bb-ec90-f760-ad6f-e7678f12c401"
"id": "21be08bb-ec90-f760-ad6f-e7678f12c401",
"typeFullFqn": "system.cards.entities_table"
},
"e8280043-d3dc-7acb-c2ff-a4522972ff91": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "entities_table",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5,
"sizeY": 6.5,
"config": {
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 86400000
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085343280,
"endTimeMs": 1694171743280
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "NONE",
@ -1391,23 +1478,32 @@
},
"row": 0,
"col": 0,
"id": "e8280043-d3dc-7acb-c2ff-a4522972ff91"
"id": "e8280043-d3dc-7acb-c2ff-a4522972ff91",
"typeFullFqn": "system.cards.entities_table"
},
"3624013b-378c-f110-5eba-ae95c25a4dcc": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "entities_table",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5,
"sizeY": 6.5,
"config": {
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 86400000
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085343280,
"endTimeMs": 1694171743280
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "NONE",
@ -1688,23 +1784,32 @@
},
"row": 0,
"col": 0,
"id": "3624013b-378c-f110-5eba-ae95c25a4dcc"
"id": "3624013b-378c-f110-5eba-ae95c25a4dcc",
"typeFullFqn": "system.cards.entities_table"
},
"d2d13e0d-4e71-889f-9343-ad2f0af9f176": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "entities_table",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5,
"sizeY": 6.5,
"config": {
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 86400000
"timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085343280,
"endTimeMs": 1694171743280
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "NONE",
@ -1985,7 +2090,8 @@
},
"row": 0,
"col": 0,
"id": "d2d13e0d-4e71-889f-9343-ad2f0af9f176"
"id": "d2d13e0d-4e71-889f-9343-ad2f0af9f176",
"typeFullFqn": "system.cards.entities_table"
}
},
"states": {
@ -2035,7 +2141,8 @@
"autoFillHeight": true,
"backgroundImageUrl": null,
"mobileAutoFillHeight": true,
"mobileRowHeight": 70
"mobileRowHeight": 70,
"outerMargin": true
}
}
}
@ -2062,7 +2169,8 @@
"autoFillHeight": true,
"backgroundImageUrl": null,
"mobileAutoFillHeight": false,
"mobileRowHeight": 70
"mobileRowHeight": 70,
"outerMargin": true
}
}
}
@ -2089,7 +2197,8 @@
"autoFillHeight": true,
"backgroundImageUrl": null,
"mobileAutoFillHeight": false,
"mobileRowHeight": 70
"mobileRowHeight": 70,
"outerMargin": true
}
}
}
@ -2116,7 +2225,8 @@
"autoFillHeight": true,
"backgroundImageUrl": null,
"mobileAutoFillHeight": false,
"mobileRowHeight": 70
"mobileRowHeight": 70,
"outerMargin": true
}
}
}
@ -2143,7 +2253,8 @@
"autoFillHeight": true,
"backgroundImageUrl": null,
"mobileAutoFillHeight": false,
"mobileRowHeight": 70
"mobileRowHeight": 70,
"outerMargin": true
}
}
}
@ -2170,7 +2281,8 @@
"autoFillHeight": true,
"backgroundImageUrl": null,
"mobileAutoFillHeight": false,
"mobileRowHeight": 70
"mobileRowHeight": 70,
"outerMargin": true
}
}
}
@ -2488,5 +2600,6 @@
"showUpdateDashboardImage": false
}
},
"externalId": null,
"name": "Software"
}

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

@ -1,20 +1,33 @@
{
"title": "Thermostats",
"image": null,
"mobileHide": false,
"mobileOrder": null,
"configuration": {
"widgets": {
"f33c746c-0dfc-c212-395b-b448c8a17209": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "entities_table",
"type": "latest",
"title": "New widget",
"sizeX": 11,
"sizeY": 11,
"config": {
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 86400000
"timewindowMs": 86400000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085177686,
"endTimeMs": 1694171577686
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "NONE",
@ -184,14 +197,11 @@
"rowClick": []
}
},
"id": "f33c746c-0dfc-c212-395b-b448c8a17209"
"id": "f33c746c-0dfc-c212-395b-b448c8a17209",
"typeFullFqn": "system.cards.entities_table"
},
"7943196b-eedb-d422-f9c3-b32d379ad172": {
"isSystemType": true,
"bundleAlias": "alarm_widgets",
"typeAlias": "alarms_table",
"type": "alarm",
"title": "New widget",
"sizeX": 13,
"sizeY": 5,
"config": {
@ -230,7 +240,7 @@
"titleStyle": {
"fontSize": "16px",
"fontWeight": 400,
"padding": "5px 10px 5px 10px"
"padding": "5px 10px 5px 2px"
},
"useDashboardTimewindow": false,
"showLegend": false,
@ -290,7 +300,6 @@
}
]
},
"alarmSearchStatus": "ANY",
"alarmsPollingInterval": 5,
"showTitleIcon": false,
"titleIcon": null,
@ -302,16 +311,22 @@
"actions": {},
"datasources": [],
"alarmsMaxCountLoad": 0,
"alarmsFetchSize": 100
"alarmsFetchSize": 100,
"alarmFilterConfig": {
"statusList": [],
"severityList": [],
"typeList": [],
"searchPropagatedAlarms": true
},
"widgetCss": "",
"pageSize": 1024,
"noDataDisplayMessage": ""
},
"id": "7943196b-eedb-d422-f9c3-b32d379ad172"
"id": "7943196b-eedb-d422-f9c3-b32d379ad172",
"typeFullFqn": "system.alarm_widgets.alarms_table"
},
"14a19183-f0b2-d6be-0f62-9863f0a51111": {
"isSystemType": true,
"bundleAlias": "charts",
"typeAlias": "basic_timeseries",
"type": "timeseries",
"title": "New widget",
"sizeX": 18,
"sizeY": 6,
"config": {
@ -395,7 +410,16 @@
"axisPosition": "top",
"showLabels": true
},
"smoothLines": true
"smoothLines": true,
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "bottom",
"showMin": true,
"showMax": true,
"showAvg": true,
"showTotal": false
}
},
"title": "Temperature",
"dropShadow": true,
@ -413,25 +437,13 @@
"widgetStyle": {},
"useDashboardTimewindow": false,
"displayTimewindow": true,
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "bottom",
"showMin": true,
"showMax": true,
"showAvg": true,
"showTotal": false
},
"actions": {}
},
"id": "14a19183-f0b2-d6be-0f62-9863f0a51111"
"id": "14a19183-f0b2-d6be-0f62-9863f0a51111",
"typeFullFqn": "system.charts.basic_timeseries"
},
"07f49fd5-a73b-d74c-c220-362c20af81f4": {
"isSystemType": true,
"bundleAlias": "charts",
"typeAlias": "basic_timeseries",
"type": "timeseries",
"title": "New widget",
"sizeX": 18,
"sizeY": 6,
"config": {
@ -515,7 +527,16 @@
"axisPosition": "top",
"showLabels": true
},
"smoothLines": true
"smoothLines": true,
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "bottom",
"showMin": true,
"showMax": true,
"showAvg": true,
"showTotal": false
}
},
"title": "Humidity",
"dropShadow": true,
@ -533,25 +554,13 @@
"widgetStyle": {},
"useDashboardTimewindow": false,
"displayTimewindow": true,
"showLegend": true,
"legendConfig": {
"direction": "column",
"position": "bottom",
"showMin": true,
"showMax": true,
"showAvg": true,
"showTotal": false
},
"actions": {}
},
"id": "07f49fd5-a73b-d74c-c220-362c20af81f4"
"id": "07f49fd5-a73b-d74c-c220-362c20af81f4",
"typeFullFqn": "system.charts.basic_timeseries"
},
"c4631f94-2db3-523b-4d09-2a1a0a75d93f": {
"isSystemType": true,
"bundleAlias": "input_widgets",
"typeAlias": "update_multiple_attributes",
"type": "latest",
"title": "New widget",
"sizeX": 6,
"sizeY": 6,
"config": {
@ -626,8 +635,27 @@
}
],
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"timewindowMs": 60000
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085177686,
"endTimeMs": 1694171577686
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
}
},
"showTitle": true,
@ -661,14 +689,11 @@
"titleTooltip": "",
"displayTimewindow": true
},
"id": "c4631f94-2db3-523b-4d09-2a1a0a75d93f"
"id": "c4631f94-2db3-523b-4d09-2a1a0a75d93f",
"typeFullFqn": "system.input_widgets.update_multiple_attributes"
},
"3da9a9a1-0b9a-2e1f-0dcb-0ff34a695abb": {
"isSystemType": true,
"bundleAlias": "maps_v2",
"typeAlias": "openstreetmap",
"type": "latest",
"title": "New widget",
"sizeX": 13,
"sizeY": 6,
"config": {
@ -725,8 +750,27 @@
}
],
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"timewindowMs": 60000
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085177686,
"endTimeMs": 1694171577686
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
}
},
"showTitle": false,
@ -802,14 +846,11 @@
"titleTooltip": "",
"displayTimewindow": true
},
"id": "3da9a9a1-0b9a-2e1f-0dcb-0ff34a695abb"
"id": "3da9a9a1-0b9a-2e1f-0dcb-0ff34a695abb",
"typeFullFqn": "system.maps_v2.openstreetmap"
},
"00fb2742-ba1f-7e43-673f-d6c08b72ed06": {
"isSystemType": true,
"bundleAlias": "input_widgets",
"typeAlias": "markers_placement_openstreetmap",
"type": "latest",
"title": "New widget",
"sizeX": 24,
"sizeY": 12,
"config": {
@ -864,8 +905,27 @@
}
],
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"timewindowMs": 60000
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085177686,
"endTimeMs": 1694171577686
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
}
},
"showTitle": false,
@ -937,14 +997,11 @@
"titleTooltip": "",
"displayTimewindow": true
},
"id": "00fb2742-ba1f-7e43-673f-d6c08b72ed06"
"id": "00fb2742-ba1f-7e43-673f-d6c08b72ed06",
"typeFullFqn": "system.input_widgets.markers_placement_openstreetmap"
},
"0a430429-9078-9ae6-2b67-e4a15a2bf8bf": {
"isSystemType": true,
"bundleAlias": "input_widgets",
"typeAlias": "markers_placement_openstreetmap",
"type": "latest",
"title": "New widget",
"sizeX": 6,
"sizeY": 6,
"config": {
@ -999,8 +1056,27 @@
}
],
"timewindow": {
"displayValue": "",
"selectedTab": 0,
"realtime": {
"timewindowMs": 60000
"realtimeType": 1,
"interval": 1000,
"timewindowMs": 60000,
"quickInterval": "CURRENT_DAY"
},
"history": {
"historyType": 0,
"interval": 1000,
"timewindowMs": 60000,
"fixedTimewindow": {
"startTimeMs": 1694085177686,
"endTimeMs": 1694171577686
},
"quickInterval": "CURRENT_DAY"
},
"aggregation": {
"type": "AVG",
"limit": 25000
}
},
"showTitle": false,
@ -1073,7 +1149,8 @@
"titleTooltip": "",
"displayTimewindow": true
},
"id": "0a430429-9078-9ae6-2b67-e4a15a2bf8bf"
"id": "0a430429-9078-9ae6-2b67-e4a15a2bf8bf",
"typeFullFqn": "system.input_widgets.markers_placement_openstreetmap"
}
},
"states": {
@ -1110,7 +1187,8 @@
"autoFillHeight": true,
"mobileAutoFillHeight": false,
"mobileRowHeight": 70,
"margin": 10
"margin": 10,
"outerMargin": true
}
}
}
@ -1136,7 +1214,8 @@
"autoFillHeight": true,
"mobileAutoFillHeight": false,
"mobileRowHeight": 70,
"margin": 10
"margin": 10,
"outerMargin": true
}
}
}
@ -1182,7 +1261,8 @@
"autoFillHeight": true,
"mobileAutoFillHeight": false,
"mobileRowHeight": 70,
"margin": 10
"margin": 10,
"outerMargin": true
}
}
}
@ -1195,8 +1275,10 @@
"filter": {
"type": "deviceType",
"resolveMultiple": true,
"deviceType": "thermostat",
"deviceNameFilter": ""
"deviceNameFilter": "",
"deviceTypes": [
"thermostat"
]
}
},
"12ae98c7-1ea2-52cf-64d5-763e9d993547": {
@ -1245,5 +1327,6 @@
},
"filters": {}
},
"externalId": null,
"name": "Thermostats"
}

2
application/src/main/data/json/edge/rule_chains/edge_root_rule_chain.json

@ -48,8 +48,8 @@
"type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode",
"name": "Save Client Attributes",
"debugMode": false,
"configurationVersion": 1,
"configuration": {
"version": 1,
"scope": "CLIENT_SCOPE",
"notifyDevice": "false",
"sendAttributesUpdatedNotification": "false",

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

@ -32,8 +32,8 @@
"type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode",
"name": "Save Client Attributes",
"debugMode": false,
"configurationVersion": 1,
"configuration": {
"version": 1,
"scope": "CLIENT_SCOPE",
"notifyDevice": "false",
"sendAttributesUpdatedNotification": "false",

2
application/src/main/data/json/tenant/rule_chains/root_rule_chain.json

@ -31,8 +31,8 @@
"type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode",
"name": "Save Client Attributes",
"debugMode": false,
"configurationVersion": 1,
"configuration": {
"version": 1,
"scope": "CLIENT_SCOPE",
"notifyDevice": "false",
"sendAttributesUpdatedNotification": "false",

49
application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java

@ -33,7 +33,10 @@ import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredential;
import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode;
import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration;
import org.thingsboard.server.common.data.device.data.DeviceData;
import org.thingsboard.server.common.data.device.data.PowerMode;
import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfileProvisionConfiguration;
@ -45,6 +48,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportColumnType;
import org.thingsboard.server.common.data.transport.snmp.SnmpProtocolVersion;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
@ -76,18 +80,18 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
private final Lock findOrCreateDeviceProfileLock = new ReentrantLock();
@Override
protected void setEntityFields(Device entity, Map<BulkImportColumnType, String> fields) {
ObjectNode additionalInfo = getOrCreateAdditionalInfoObj(entity);
protected void setEntityFields(Device device, Map<BulkImportColumnType, String> fields) {
ObjectNode additionalInfo = getOrCreateAdditionalInfoObj(device);
fields.forEach((columnType, value) -> {
switch (columnType) {
case NAME:
entity.setName(value);
device.setName(value);
break;
case TYPE:
entity.setType(value);
device.setType(value);
break;
case LABEL:
entity.setLabel(value);
device.setLabel(value);
break;
case DESCRIPTION:
additionalInfo.set("description", new TextNode(value));
@ -96,16 +100,17 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
additionalInfo.set("gateway", BooleanNode.valueOf(Boolean.parseBoolean(value)));
break;
}
entity.setAdditionalInfo(additionalInfo);
device.setAdditionalInfo(additionalInfo);
});
setUpDeviceConfiguration(device, fields);
}
@Override
@SneakyThrows
protected Device saveEntity(SecurityUser user, Device entity, Map<BulkImportColumnType, String> fields) {
protected Device saveEntity(SecurityUser user, Device device, Map<BulkImportColumnType, String> fields) {
DeviceCredentials deviceCredentials;
try {
deviceCredentials = createDeviceCredentials(entity.getTenantId(), entity.getId(), fields);
deviceCredentials = createDeviceCredentials(device.getTenantId(), device.getId(), fields);
deviceCredentialsService.formatCredentials(deviceCredentials);
} catch (Exception e) {
throw new DeviceCredentialsValidationException("Invalid device credentials: " + e.getMessage());
@ -113,15 +118,15 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
DeviceProfile deviceProfile;
if (deviceCredentials.getCredentialsType() == DeviceCredentialsType.LWM2M_CREDENTIALS) {
deviceProfile = setUpLwM2mDeviceProfile(entity.getTenantId(), entity);
} else if (StringUtils.isNotEmpty(entity.getType())) {
deviceProfile = deviceProfileService.findOrCreateDeviceProfile(entity.getTenantId(), entity.getType());
deviceProfile = setUpLwM2mDeviceProfile(device.getTenantId(), device);
} else if (StringUtils.isNotEmpty(device.getType())) {
deviceProfile = deviceProfileService.findOrCreateDeviceProfile(device.getTenantId(), device.getType());
} else {
deviceProfile = deviceProfileService.findDefaultDeviceProfile(entity.getTenantId());
deviceProfile = deviceProfileService.findDefaultDeviceProfile(device.getTenantId());
}
entity.setDeviceProfileId(deviceProfile.getId());
device.setDeviceProfileId(deviceProfile.getId());
return tbDeviceService.saveDeviceWithCredentials(entity, deviceCredentials, user);
return tbDeviceService.saveDeviceWithCredentials(device, deviceCredentials, user);
}
@Override
@ -136,6 +141,22 @@ public class DeviceBulkImportService extends AbstractBulkImportService<Device> {
entity.setCustomerId(user.getCustomerId());
}
private void setUpDeviceConfiguration(Device device, Map<BulkImportColumnType, String> fields) {
if (fields.containsKey(BulkImportColumnType.SNMP_HOST)) {
SnmpDeviceTransportConfiguration transportConfiguration = new SnmpDeviceTransportConfiguration();
transportConfiguration.setHost(fields.get(BulkImportColumnType.SNMP_HOST));
transportConfiguration.setPort(Optional.ofNullable(fields.get(BulkImportColumnType.SNMP_PORT))
.map(Integer::parseInt).orElse(161));
transportConfiguration.setProtocolVersion(Optional.ofNullable(fields.get(BulkImportColumnType.SNMP_VERSION))
.map(version -> SnmpProtocolVersion.valueOf(version.toUpperCase())).orElse(SnmpProtocolVersion.V2C));
transportConfiguration.setCommunity(fields.getOrDefault(BulkImportColumnType.SNMP_COMMUNITY_STRING, "public"));
DeviceData deviceData = new DeviceData();
deviceData.setTransportConfiguration(transportConfiguration);
device.setDeviceData(deviceData);
}
}
@SneakyThrows
private DeviceCredentials createDeviceCredentials(TenantId tenantId, DeviceId deviceId, Map<BulkImportColumnType, String> fields) {
DeviceCredentials credentials = new DeviceCredentials();

8
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -614,13 +614,13 @@ public final class EdgeGrpcSession implements Closeable {
case EDGE:
return ctx.getEdgeProcessor().convertEdgeEventToDownlink(edgeEvent);
case DEVICE:
return ctx.getDeviceProcessor().convertDeviceEventToDownlink(edgeEvent);
return ctx.getDeviceProcessor().convertDeviceEventToDownlink(edgeEvent, this.edge.getId(), this.edgeVersion);
case DEVICE_PROFILE:
return ctx.getDeviceProfileProcessor().convertDeviceProfileEventToDownlink(edgeEvent);
return ctx.getDeviceProfileProcessor().convertDeviceProfileEventToDownlink(edgeEvent, this.edge.getId(), this.edgeVersion);
case ASSET_PROFILE:
return ctx.getAssetProfileProcessor().convertAssetProfileEventToDownlink(edgeEvent);
return ctx.getAssetProfileProcessor().convertAssetProfileEventToDownlink(edgeEvent, this.edge.getId(), this.edgeVersion);
case ASSET:
return ctx.getAssetProcessor().convertAssetEventToDownlink(edgeEvent);
return ctx.getAssetProcessor().convertAssetEventToDownlink(edgeEvent, this.edge.getId(), this.edgeVersion);
case ENTITY_VIEW:
return ctx.getEntityViewProcessor().convertEntityViewEventToDownlink(edgeEvent);
case DASHBOARD:

54
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java

@ -79,6 +79,7 @@ import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.TbQueueCallback;
@ -596,4 +597,57 @@ public abstract class BaseEdgeProcessor {
}
});
}
protected AssetProfile checkIfAssetProfileDefaultFieldsAssignedToEdge(TenantId tenantId, EdgeId edgeId, AssetProfile assetProfile, EdgeVersion edgeVersion) {
switch (edgeVersion) {
case V_3_3_3:
case V_3_3_0:
case V_3_4_0:
if (assetProfile.getDefaultDashboardId() != null
&& isEntityNotAssignedToEdge(tenantId, assetProfile.getDefaultDashboardId(), edgeId)) {
assetProfile.setDefaultDashboardId(null);
}
if (assetProfile.getDefaultEdgeRuleChainId() != null
&& isEntityNotAssignedToEdge(tenantId, assetProfile.getDefaultEdgeRuleChainId(), edgeId)) {
assetProfile.setDefaultEdgeRuleChainId(null);
}
break;
}
return assetProfile;
}
protected DeviceProfile checkIfDeviceProfileDefaultFieldsAssignedToEdge(TenantId tenantId, EdgeId edgeId, DeviceProfile deviceProfile, EdgeVersion edgeVersion) {
switch (edgeVersion) {
case V_3_3_3:
case V_3_3_0:
case V_3_4_0:
if (deviceProfile.getDefaultDashboardId() != null
&& isEntityNotAssignedToEdge(tenantId, deviceProfile.getDefaultDashboardId(), edgeId)) {
deviceProfile.setDefaultDashboardId(null);
}
if (deviceProfile.getDefaultEdgeRuleChainId() != null
&& isEntityNotAssignedToEdge(tenantId, deviceProfile.getDefaultEdgeRuleChainId(), edgeId)) {
deviceProfile.setDefaultEdgeRuleChainId(null);
}
break;
}
return deviceProfile;
}
private boolean isEntityNotAssignedToEdge(TenantId tenantId, EntityId entityId, EdgeId edgeId) {
PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE);
PageData<EdgeId> pageData;
do {
pageData = edgeService.findRelatedEdgeIdsByEntityId(tenantId, entityId, pageLink);
if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) {
if (pageData.getData().contains(edgeId)) {
return false;
}
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
}
}
} while (pageData != null && pageData.hasNext());
return true;
}
}

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java

@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -37,6 +38,7 @@ import org.thingsboard.server.dao.asset.BaseAssetService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -106,7 +108,7 @@ public class AssetEdgeProcessor extends BaseAssetProcessor {
}
}
public DownlinkMsg convertAssetEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertAssetEventToDownlink(EdgeEvent edgeEvent, EdgeId edgeId, EdgeVersion edgeVersion) {
AssetId assetId = new AssetId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null;
switch (edgeEvent.getAction()) {
@ -125,6 +127,7 @@ public class AssetEdgeProcessor extends BaseAssetProcessor {
.addAssetUpdateMsg(assetUpdateMsg);
if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) {
AssetProfile assetProfile = assetProfileService.findAssetProfileById(edgeEvent.getTenantId(), asset.getAssetProfileId());
assetProfile = checkIfAssetProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, assetProfile, edgeVersion);
builder.addAssetProfileUpdateMsg(assetProfileMsgConstructor.constructAssetProfileUpdatedMsg(msgType, assetProfile));
}
downlinkMsg = builder.build();

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessor.java

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
@ -36,6 +37,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -94,7 +96,7 @@ public class AssetProfileEdgeProcessor extends BaseAssetProfileProcessor {
}
}
public DownlinkMsg convertAssetProfileEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertAssetProfileEventToDownlink(EdgeEvent edgeEvent, EdgeId edgeId, EdgeVersion edgeVersion) {
AssetProfileId assetProfileId = new AssetProfileId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null;
switch (edgeEvent.getAction()) {
@ -103,6 +105,7 @@ public class AssetProfileEdgeProcessor extends BaseAssetProfileProcessor {
AssetProfile assetProfile = assetProfileService.findAssetProfileById(edgeEvent.getTenantId(), assetProfileId);
if (assetProfile != null) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
assetProfile = checkIfAssetProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, assetProfile, edgeVersion);
AssetProfileUpdateMsg assetProfileUpdateMsg =
assetProfileMsgConstructor.constructAssetProfileUpdatedMsg(msgType, assetProfile);
downlinkMsg = DownlinkMsg.newBuilder()

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java

@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.rpc.RpcError;
@ -49,6 +50,7 @@ import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg;
import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.TbQueueCallback;
import org.thingsboard.server.queue.TbQueueMsgMetadata;
@ -202,7 +204,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor {
return Futures.immediateFuture(null);
}
public DownlinkMsg convertDeviceEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertDeviceEventToDownlink(EdgeEvent edgeEvent, EdgeId edgeId, EdgeVersion edgeVersion) {
DeviceId deviceId = new DeviceId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null;
switch (edgeEvent.getAction()) {
@ -221,6 +223,7 @@ public class DeviceEdgeProcessor extends BaseDeviceProcessor {
.addDeviceUpdateMsg(deviceUpdateMsg);
if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) {
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId());
deviceProfile = checkIfDeviceProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, deviceProfile, edgeVersion);
builder.addDeviceProfileUpdateMsg(deviceProfileMsgConstructor.constructDeviceProfileUpdatedMsg(msgType, deviceProfile));
}
downlinkMsg = builder.build();

5
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessor.java

@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.msg.TbMsgType;
@ -36,6 +37,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -94,7 +96,7 @@ public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor {
}
}
public DownlinkMsg convertDeviceProfileEventToDownlink(EdgeEvent edgeEvent) {
public DownlinkMsg convertDeviceProfileEventToDownlink(EdgeEvent edgeEvent, EdgeId edgeId, EdgeVersion edgeVersion) {
DeviceProfileId deviceProfileId = new DeviceProfileId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null;
switch (edgeEvent.getAction()) {
@ -103,6 +105,7 @@ public class DeviceProfileEdgeProcessor extends BaseDeviceProfileProcessor {
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), deviceProfileId);
if (deviceProfile != null) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
deviceProfile = checkIfDeviceProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, deviceProfile, edgeVersion);
DeviceProfileUpdateMsg deviceProfileUpdateMsg =
deviceProfileMsgConstructor.constructDeviceProfileUpdatedMsg(msgType, deviceProfile);
downlinkMsg = DownlinkMsg.newBuilder()

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

@ -28,7 +28,6 @@ import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
@ -104,15 +103,15 @@ public class InstallScripts {
@Autowired
private ResourceService resourceService;
private Path getTenantRuleChainsDir() {
Path getTenantRuleChainsDir() {
return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR);
}
private Path getDeviceProfileDefaultRuleChainTemplateFilePath() {
Path getDeviceProfileDefaultRuleChainTemplateFilePath() {
return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DEVICE_PROFILE_DIR, "rule_chain_template.json");
}
private Path getEdgeRuleChainsDir() {
Path getEdgeRuleChainsDir() {
return Paths.get(getDataDir(), JSON_DIR, EDGE_DIR, RULE_CHAINS_DIR);
}
@ -148,8 +147,8 @@ public class InstallScripts {
}
private void loadRuleChainsFromPath(TenantId tenantId, Path ruleChainsPath) throws IOException {
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(ruleChainsPath, path -> path.toString().endsWith(InstallScripts.JSON_EXT))) {
dirStream.forEach(
findRuleChainsFromPath(ruleChainsPath)
.forEach(
path -> {
try {
createRuleChainFromFile(tenantId, path, null);
@ -157,9 +156,15 @@ public class InstallScripts {
log.error("Unable to load rule chain from json: [{}]", path.toString());
throw new RuntimeException("Unable to load rule chain from json", e);
}
}
);
});
}
List<Path> findRuleChainsFromPath(Path ruleChainsPath) throws IOException {
List<Path> paths = new ArrayList<>();
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(ruleChainsPath, path -> path.toString().endsWith(InstallScripts.JSON_EXT))) {
dirStream.forEach(paths::add);
}
return paths;
}
public RuleChain createDefaultRuleChain(TenantId tenantId, String ruleChainName) throws IOException {

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

@ -24,10 +24,14 @@ import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.event.ErrorEvent;
import org.thingsboard.server.common.data.event.Event;
import org.thingsboard.server.common.data.event.LifecycleEvent;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.NotificationRequestId;
import org.thingsboard.server.common.data.id.TenantId;
@ -45,7 +49,9 @@ import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto;
import org.thingsboard.server.gen.transport.TransportProtos.EdgeNotificationMsgProto;
import org.thingsboard.server.gen.transport.TransportProtos.ErrorEventProto;
import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto;
import org.thingsboard.server.gen.transport.TransportProtos.LifecycleEventProto;
import org.thingsboard.server.gen.transport.TransportProtos.LocalSubscriptionServiceMsgProto;
import org.thingsboard.server.gen.transport.TransportProtos.SubscriptionMgrMsgProto;
import org.thingsboard.server.gen.transport.TransportProtos.TbAlarmDeleteProto;
@ -274,6 +280,10 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
TransportProtos.NotificationSchedulerServiceMsg notificationSchedulerServiceMsg = toCoreMsg.getNotificationSchedulerServiceMsg();
log.trace("[{}] Forwarding message to notification scheduler service {}", id, toCoreMsg.getNotificationSchedulerServiceMsg());
forwardToNotificationSchedulerService(notificationSchedulerServiceMsg, callback);
} else if (toCoreMsg.hasErrorEventMsg()) {
forwardToEventService(toCoreMsg.getErrorEventMsg(), callback);
} else if (toCoreMsg.hasLifecycleEventMsg()) {
forwardToEventService(toCoreMsg.getLifecycleEventMsg(), callback);
}
} catch (Throwable e) {
log.warn("[{}] Failed to process message: {}", id, msg, e);
@ -519,50 +529,52 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
subscriptionManagerService.cancelSubscription(closeProto.getSessionId(), closeProto.getSubscriptionId(), callback);
} else if (msg.hasTsUpdate()) {
TbTimeSeriesUpdateProto proto = msg.getTsUpdate();
long tenantIdMSB = proto.getTenantIdMSB();
long tenantIdLSB = proto.getTenantIdLSB();
subscriptionManagerService.onTimeSeriesUpdate(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())),
toTenantId(tenantIdMSB, tenantIdLSB),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
TbSubscriptionUtils.toTsKvEntityList(proto.getDataList()), callback);
} else if (msg.hasAttrUpdate()) {
TbAttributeUpdateProto proto = msg.getAttrUpdate();
subscriptionManagerService.onAttributesUpdate(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())),
toTenantId(proto.getTenantIdMSB(), proto.getTenantIdLSB()),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
proto.getScope(), TbSubscriptionUtils.toAttributeKvList(proto.getDataList()), callback);
} else if (msg.hasAttrDelete()) {
TbAttributeDeleteProto proto = msg.getAttrDelete();
subscriptionManagerService.onAttributesDelete(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())),
toTenantId(proto.getTenantIdMSB(), proto.getTenantIdLSB()),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
proto.getScope(), proto.getKeysList(), proto.getNotifyDevice(), callback);
} else if (msg.hasTsDelete()) {
TbTimeSeriesDeleteProto proto = msg.getTsDelete();
subscriptionManagerService.onTimeSeriesDelete(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())),
toTenantId(proto.getTenantIdMSB(), proto.getTenantIdLSB()),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
proto.getKeysList(), callback);
} else if (msg.hasAlarmUpdate()) {
TbAlarmUpdateProto proto = msg.getAlarmUpdate();
subscriptionManagerService.onAlarmUpdate(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())),
toTenantId(proto.getTenantIdMSB(), proto.getTenantIdLSB()),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
JacksonUtil.fromString(proto.getAlarm(), AlarmInfo.class),
callback);
} else if (msg.hasAlarmDelete()) {
TbAlarmDeleteProto proto = msg.getAlarmDelete();
subscriptionManagerService.onAlarmDeleted(
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())),
toTenantId(proto.getTenantIdMSB(), proto.getTenantIdLSB()),
TbSubscriptionUtils.toEntityId(proto.getEntityType(), proto.getEntityIdMSB(), proto.getEntityIdLSB()),
JacksonUtil.fromString(proto.getAlarm(), AlarmInfo.class), callback);
} else if (msg.hasNotificationUpdate()) {
TransportProtos.NotificationUpdateProto updateProto = msg.getNotificationUpdate();
TenantId tenantId = TenantId.fromUUID(new UUID(updateProto.getTenantIdMSB(), updateProto.getTenantIdLSB()));
TenantId tenantId = toTenantId(updateProto.getTenantIdMSB(), updateProto.getTenantIdLSB());
UserId recipientId = new UserId(new UUID(updateProto.getRecipientIdMSB(), updateProto.getRecipientIdLSB()));
NotificationUpdate update = JacksonUtil.fromString(updateProto.getUpdate(), NotificationUpdate.class);
subscriptionManagerService.onNotificationUpdate(tenantId, recipientId, update, callback);
} else if (msg.hasNotificationRequestUpdate()) {
TransportProtos.NotificationRequestUpdateProto updateProto = msg.getNotificationRequestUpdate();
TenantId tenantId = TenantId.fromUUID(new UUID(updateProto.getTenantIdMSB(), updateProto.getTenantIdLSB()));
TenantId tenantId = toTenantId(updateProto.getTenantIdMSB(), updateProto.getTenantIdLSB());
NotificationRequestUpdate update = JacksonUtil.fromString(updateProto.getUpdate(), NotificationRequestUpdate.class);
subscriptionManagerService.onNotificationRequestUpdate(tenantId, update, callback);
} else {
@ -584,7 +596,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
if (statsEnabled) {
stats.log(deviceActivityMsg);
}
TenantId tenantId = TenantId.fromUUID(new UUID(deviceActivityMsg.getTenantIdMSB(), deviceActivityMsg.getTenantIdLSB()));
TenantId tenantId = toTenantId(deviceActivityMsg.getTenantIdMSB(), deviceActivityMsg.getTenantIdLSB());
DeviceId deviceId = new DeviceId(new UUID(deviceActivityMsg.getDeviceIdMSB(), deviceActivityMsg.getDeviceIdLSB()));
try {
stateService.onDeviceActivity(tenantId, deviceId, deviceActivityMsg.getLastActivityTime());
@ -595,7 +607,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
}
private void forwardToNotificationSchedulerService(TransportProtos.NotificationSchedulerServiceMsg msg, TbCallback callback) {
TenantId tenantId = TenantId.fromUUID(new UUID(msg.getTenantIdMSB(), msg.getTenantIdLSB()));
TenantId tenantId = toTenantId(msg.getTenantIdMSB(), msg.getTenantIdLSB());
NotificationRequestId notificationRequestId = new NotificationRequestId(new UUID(msg.getRequestIdMSB(), msg.getRequestIdLSB()));
try {
notificationSchedulerService.scheduleNotificationRequest(tenantId, notificationRequestId, msg.getTs());
@ -627,11 +639,47 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
callback.onSuccess();
}
private void forwardToEventService(ErrorEventProto eventProto, TbCallback callback) {
Event event = ErrorEvent.builder()
.tenantId(toTenantId(eventProto.getTenantIdMSB(), eventProto.getTenantIdLSB()))
.entityId(new UUID(eventProto.getEntityIdMSB(), eventProto.getEntityIdLSB()))
.serviceId(eventProto.getServiceId())
.ts(System.currentTimeMillis())
.method(eventProto.getMethod())
.error(eventProto.getError())
.build();
forwardToEventService(event, callback);
}
private void forwardToEventService(LifecycleEventProto eventProto, TbCallback callback) {
Event event = LifecycleEvent.builder()
.tenantId(toTenantId(eventProto.getTenantIdMSB(), eventProto.getTenantIdLSB()))
.entityId(new UUID(eventProto.getEntityIdMSB(), eventProto.getEntityIdLSB()))
.serviceId(eventProto.getServiceId())
.ts(System.currentTimeMillis())
.lcEventType(eventProto.getLcEventType())
.success(eventProto.getSuccess())
.error(StringUtils.isNotEmpty(eventProto.getError()) ? eventProto.getError() : null)
.build();
forwardToEventService(event, callback);
}
private void forwardToEventService(Event event, TbCallback callback) {
DonAsynchron.withCallback(actorContext.getEventService().saveAsync(event),
result -> callback.onSuccess(),
callback::onFailure,
actorContext.getDbCallbackExecutor());
}
private void throwNotHandled(Object msg, TbCallback callback) {
log.warn("Message not handled: {}", msg);
callback.onFailure(new RuntimeException("Message not handled!"));
}
private TenantId toTenantId(long tenantIdMSB, long tenantIdLSB) {
return TenantId.fromUUID(new UUID(tenantIdMSB, tenantIdLSB));
}
@Override
protected void stopMainConsumers() {
if (mainConsumer != null) {

5
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java

@ -22,6 +22,7 @@ import com.google.gson.JsonPrimitive;
import lombok.Data;
import lombok.SneakyThrows;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
@ -57,7 +58,7 @@ import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import org.thingsboard.server.utils.CsvUtils;
import org.thingsboard.server.utils.TypeCastUtil;
import org.thingsboard.server.common.data.util.TypeCastUtil;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
@ -269,7 +270,7 @@ public abstract class AbstractBulkImportService<E extends HasId<? extends Entity
if (!entry.getKey().getType().isKv()) {
entityData.getFields().put(entry.getKey().getType(), entry.getValue());
} else {
Map.Entry<DataType, Object> castResult = TypeCastUtil.castValue(entry.getValue());
Pair<DataType, Object> castResult = TypeCastUtil.castValue(entry.getValue());
entityData.getKvs().put(entry.getKey(), new ParsedValue(castResult.getValue(), castResult.getKey()));
}
});

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

@ -991,11 +991,15 @@ transport:
# value: "${LWM2M_PROTOCOL_STAGE_THREAD_COUNT:4}"
snmp:
enabled: "${SNMP_ENABLED:true}"
bind_port: "${SNMP_BIND_PORT:1620}"
response_processing:
# parallelism level for executor (workStealingPool) that is responsible for handling responses from SNMP devices
parallelism_level: "${SNMP_RESPONSE_PROCESSING_PARALLELISM_LEVEL:20}"
# to configure SNMP to work over UDP or TCP
underlying_protocol: "${SNMP_UNDERLYING_PROTOCOL:udp}"
max_request_oids: "${SNMP_MAX_REQUEST_OIDS:100}"
response:
ignore_type_cast_errors: "${SNMP_RESPONSE_IGNORE_TYPE_CAST_ERRORS:false}"
stats:
enabled: "${TB_TRANSPORT_STATS_ENABLED:true}"
print-interval-ms: "${TB_TRANSPORT_STATS_PRINT_INTERVAL_MS:60000}"

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

@ -66,6 +66,8 @@ import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.PEM_CERT_FI
@TestPropertySource(properties = {
"device.connectivity.https.enabled=true",
"device.connectivity.http.port=8080",
"device.connectivity.https.port=444",
"device.connectivity.mqtts.enabled=true",
"device.connectivity.mqtts.pem_cert_file=/tmp/" + PEM_CERT_FILE_NAME,
"device.connectivity.coaps.enabled=true",
@ -191,7 +193,7 @@ public class DeviceConnectivityControllerTest extends AbstractControllerTest {
assertThat(httpCommands.get(HTTP).asText()).isEqualTo(String.format("curl -v -X POST http://localhost:8080/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId()));
assertThat(httpCommands.get(HTTPS).asText()).isEqualTo(String.format("curl -v -X POST https://localhost:443/api/v1/%s/telemetry " +
assertThat(httpCommands.get(HTTPS).asText()).isEqualTo(String.format("curl -v -X POST https://localhost:444/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId()));

144
application/src/test/java/org/thingsboard/server/controller/DeviceConnectivityControllerWithDefaultPortTest.java

@ -0,0 +1,144 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.device.profile.CoapDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.dao.device.DeviceDao;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAP;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.COAPS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.DOCKER;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTP;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.HTTPS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTT;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.MQTTS;
import static org.thingsboard.server.dao.util.DeviceConnectivityUtil.PEM_CERT_FILE_NAME;
@TestPropertySource(properties = {
"device.connectivity.https.enabled=true",
"device.connectivity.http.port=80",
"device.connectivity.mqtt.enabled=false",
"device.connectivity.mqtts.enabled=false",
"device.connectivity.coap.enabled=false",
"device.connectivity.coaps.enabled=false",
})
@ContextConfiguration(classes = {DeviceConnectivityControllerWithDefaultPortTest.Config.class})
@DaoSqlTest
public class DeviceConnectivityControllerWithDefaultPortTest extends AbstractControllerTest {
ListeningExecutorService executor;
private Tenant savedTenant;
static class Config {
@Bean
@Primary
public DeviceDao deviceDao(DeviceDao deviceDao) {
return Mockito.mock(DeviceDao.class, AdditionalAnswers.delegatesTo(deviceDao));
}
}
@Before
public void beforeTest() throws Exception {
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass()));
loginSysAdmin();
Tenant tenant = new Tenant();
tenant.setTitle("My tenant");
savedTenant = doPost("/api/tenant", tenant, Tenant.class);
Assert.assertNotNull(savedTenant);
User tenantAdmin = new User();
tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin.setTenantId(savedTenant.getId());
tenantAdmin.setEmail("tenant2@thingsboard.org");
tenantAdmin.setFirstName("Joe");
tenantAdmin.setLastName("Downs");
createUserAndLogin(tenantAdmin, "testPassword1");
}
@After
public void afterTest() throws Exception {
executor.shutdownNow();
loginSysAdmin();
doDelete("/api/tenant/" + savedTenant.getId().getId())
.andExpect(status().isOk());
}
@Test
public void testFetchPublishTelemetryCommandsForDefaultDevice() throws Exception {
Device device = new Device();
device.setName("My device");
device.setType("default");
Device savedDevice = doPost("/api/device", device, Device.class);
JsonNode commands =
doGetTyped("/api/device-connectivity/" + savedDevice.getId().getId(), new TypeReference<>() {
});
DeviceCredentials credentials =
doGet("/api/device/" + savedDevice.getId().getId() + "/credentials", DeviceCredentials.class);
assertThat(commands).hasSize(1);
JsonNode httpCommands = commands.get(HTTP);
assertThat(httpCommands.get(HTTP).asText()).isEqualTo(String.format("curl -v -X POST http://localhost/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId()));
assertThat(httpCommands.get(HTTPS).asText()).isEqualTo(String.format("curl -v -X POST https://localhost/api/v1/%s/telemetry " +
"--header Content-Type:application/json --data \"{temperature:25}\"",
credentials.getCredentialsId()));
}
}

306
application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessorTest.java

@ -0,0 +1,306 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor;
import org.junit.jupiter.params.provider.Arguments;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.context.annotation.Lazy;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeEventService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.edge.EdgeSynchronizationManager;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.service.edge.rpc.constructor.AdminSettingsMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.AlarmMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.AssetMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.AssetProfileMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.CustomerMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.DashboardMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.DeviceMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.DeviceProfileMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.EdgeMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.EntityDataMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.EntityViewMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.OtaPackageMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.QueueMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.RelationMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.RuleChainMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.TenantMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.TenantProfileMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.UserMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.WidgetTypeMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.WidgetsBundleMsgConstructor;
import org.thingsboard.server.service.entitiy.TbNotificationEntityService;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.util.UUID;
import java.util.stream.Stream;
public abstract class BaseEdgeProcessorTest {
@MockBean
protected TelemetrySubscriptionService tsSubService;
@MockBean
protected TbNotificationEntityService notificationEntityService;
@MockBean
protected RuleChainService ruleChainService;
@MockBean
protected AlarmService alarmService;
@MockBean
protected DeviceService deviceService;
@MockBean
protected TbDeviceProfileCache deviceProfileCache;
@MockBean
protected TbAssetProfileCache assetProfileCache;
@MockBean
protected DashboardService dashboardService;
@MockBean
protected AssetService assetService;
@MockBean
protected EntityViewService entityViewService;
@MockBean
protected TenantService tenantService;
@MockBean
protected TenantProfileService tenantProfileService;
@MockBean
protected EdgeService edgeService;
@MockBean
protected CustomerService customerService;
@MockBean
protected UserService userService;
@MockBean
protected DeviceProfileService deviceProfileService;
@MockBean
protected AssetProfileService assetProfileService;
@MockBean
protected RelationService relationService;
@MockBean
protected DeviceCredentialsService deviceCredentialsService;
@MockBean
protected AttributesService attributesService;
@MockBean
protected TbClusterService tbClusterService;
@MockBean
protected DeviceStateService deviceStateService;
@MockBean
protected EdgeEventService edgeEventService;
@MockBean
protected WidgetsBundleService widgetsBundleService;
@MockBean
protected WidgetTypeService widgetTypeService;
@MockBean
protected OtaPackageService otaPackageService;
@MockBean
protected QueueService queueService;
@MockBean
protected PartitionService partitionService;
@MockBean
@Lazy
protected TbQueueProducerProvider producerProvider;
@MockBean
protected DataValidator<Device> deviceValidator;
@MockBean
protected DataValidator<DeviceProfile> deviceProfileValidator;
@MockBean
protected DataValidator<Asset> assetValidator;
@MockBean
protected DataValidator<AssetProfile> assetProfileValidator;
@MockBean
protected DataValidator<Dashboard> dashboardValidator;
@MockBean
protected DataValidator<EntityView> entityViewValidator;
@MockBean
protected EdgeMsgConstructor edgeMsgConstructor;
@MockBean
protected EntityDataMsgConstructor entityDataMsgConstructor;
@MockBean
protected RuleChainMsgConstructor ruleChainMsgConstructor;
@MockBean
protected AlarmMsgConstructor alarmMsgConstructor;
@SpyBean
protected DeviceMsgConstructor deviceMsgConstructor;
@SpyBean
protected AssetMsgConstructor assetMsgConstructor;
@MockBean
protected EntityViewMsgConstructor entityViewMsgConstructor;
@MockBean
protected DashboardMsgConstructor dashboardMsgConstructor;
@MockBean
protected RelationMsgConstructor relationMsgConstructor;
@MockBean
protected UserMsgConstructor userMsgConstructor;
@MockBean
protected CustomerMsgConstructor customerMsgConstructor;
@SpyBean
protected DeviceProfileMsgConstructor deviceProfileMsgConstructor;
@SpyBean
protected AssetProfileMsgConstructor assetProfileMsgConstructor;
@MockBean
protected TenantMsgConstructor tenantMsgConstructor;
@MockBean
protected TenantProfileMsgConstructor tenantProfileMsgConstructor;
@MockBean
protected WidgetsBundleMsgConstructor widgetsBundleMsgConstructor;
@MockBean
protected WidgetTypeMsgConstructor widgetTypeMsgConstructor;
@MockBean
protected AdminSettingsMsgConstructor adminSettingsMsgConstructor;
@MockBean
protected OtaPackageMsgConstructor otaPackageMsgConstructor;
@MockBean
protected QueueMsgConstructor queueMsgConstructor;
@MockBean
protected EdgeSynchronizationManager edgeSynchronizationManager;
@MockBean
protected DbCallbackExecutorService dbCallbackExecutorService;
@MockBean
protected DataDecodingEncodingService dataDecodingEncodingService;
protected EdgeId edgeId;
protected TenantId tenantId;
protected EdgeEvent edgeEvent;
protected DashboardId getDashboardId(long expectedDashboardIdMSB, long expectedDashboardIdLSB) {
DashboardId dashboardId;
if (expectedDashboardIdMSB != 0 && expectedDashboardIdLSB != 0) {
dashboardId = new DashboardId(new UUID(expectedDashboardIdMSB, expectedDashboardIdLSB));
} else {
dashboardId = new DashboardId(UUID.randomUUID());
}
return dashboardId;
}
protected RuleChainId getRuleChainId(long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) {
RuleChainId ruleChainId;
if (expectedRuleChainIdMSB != 0 && expectedRuleChainIdLSB != 0) {
ruleChainId = new RuleChainId(new UUID(expectedRuleChainIdMSB, expectedRuleChainIdLSB));
} else {
ruleChainId = new RuleChainId(UUID.randomUUID());
}
return ruleChainId;
}
protected static Stream<Arguments> provideParameters() {
UUID dashoboardUUID = UUID.randomUUID();
UUID ruleChaindUUID = UUID.randomUUID();
return Stream.of(
Arguments.of(EdgeVersion.V_3_3_0, 0, 0, 0, 0),
Arguments.of(EdgeVersion.V_3_3_3, 0, 0, 0, 0),
Arguments.of(EdgeVersion.V_3_4_0, 0, 0, 0, 0),
Arguments.of(EdgeVersion.V_3_6_0,
dashoboardUUID.getMostSignificantBits(),
dashoboardUUID.getLeastSignificantBits(),
ruleChaindUUID.getMostSignificantBits(),
ruleChaindUUID.getLeastSignificantBits())
);
}
}

92
application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AbstractAssetProcessorTest.java

@ -0,0 +1,92 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.asset;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessorTest;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.BDDMockito.willReturn;
public abstract class AbstractAssetProcessorTest extends BaseEdgeProcessorTest {
protected AssetId assetId;
protected AssetProfileId assetProfileId;
protected AssetProfile assetProfile;
@BeforeEach
public void setUp() {
edgeId = new EdgeId(UUID.randomUUID());
tenantId = new TenantId(UUID.randomUUID());
assetId = new AssetId(UUID.randomUUID());
assetProfileId = new AssetProfileId(UUID.randomUUID());
assetProfile = new AssetProfile();
assetProfile.setId(assetProfileId);
assetProfile.setName("AssetProfile");
assetProfile.setDefault(true);
Asset asset = new Asset();
asset.setAssetProfileId(assetProfileId);
asset.setId(assetId);
asset.setName("Asset");
asset.setType(assetProfile.getName());
edgeEvent = new EdgeEvent();
edgeEvent.setTenantId(tenantId);
edgeEvent.setAction(EdgeEventActionType.ADDED);
willReturn(asset).given(assetService).findAssetById(tenantId, assetId);
willReturn(assetProfile).given(assetProfileService).findAssetProfileById(tenantId, assetProfileId);
}
protected void updateAssetProfileDefaultFields(long expectedDashboardIdMSB, long expectedDashboardIdLSB,
long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) {
DashboardId dashboardId = getDashboardId(expectedDashboardIdMSB, expectedDashboardIdLSB);
RuleChainId ruleChainId = getRuleChainId(expectedRuleChainIdMSB, expectedRuleChainIdLSB);
assetProfile.setDefaultDashboardId(dashboardId);
assetProfile.setDefaultEdgeRuleChainId(ruleChainId);
}
protected void verify(DownlinkMsg downlinkMsg, long expectedDashboardIdMSB, long expectedDashboardIdLSB,
long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) {
AssetProfileUpdateMsg assetProfileUpdateMsg = downlinkMsg.getAssetProfileUpdateMsgList().get(0);
assertNotNull(assetProfileUpdateMsg);
Assertions.assertThat(assetProfileUpdateMsg.getDefaultDashboardIdMSB()).isEqualTo(expectedDashboardIdMSB);
Assertions.assertThat(assetProfileUpdateMsg.getDefaultDashboardIdLSB()).isEqualTo(expectedDashboardIdLSB);
Assertions.assertThat(assetProfileUpdateMsg.getDefaultRuleChainIdMSB()).isEqualTo(expectedRuleChainIdMSB);
Assertions.assertThat(assetProfileUpdateMsg.getDefaultRuleChainIdLSB()).isEqualTo(expectedRuleChainIdLSB);
}
}

44
application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessorTest.java

@ -0,0 +1,44 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.asset;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
@SpringBootTest(classes = {AssetEdgeProcessor.class})
class AssetEdgeProcessorTest extends AbstractAssetProcessorTest {
@SpyBean
AssetEdgeProcessor assetEdgeProcessor;
@ParameterizedTest
@MethodSource("provideParameters")
public void testAssetProfileDefaultFields_notSendToEdgeOlder3_6_0IfNotAssigned(EdgeVersion edgeVersion, long expectedDashboardIdMSB, long expectedDashboardIdLSB,
long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) {
updateAssetProfileDefaultFields(expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB);
edgeEvent.setEntityId(assetId.getId());
DownlinkMsg downlinkMsg = assetEdgeProcessor.convertAssetEventToDownlink(edgeEvent, edgeId, edgeVersion);
verify(downlinkMsg, expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB);
}
}

43
application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetProfileEdgeProcessorTest.java

@ -0,0 +1,43 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.asset;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
@SpringBootTest(classes = {AssetProfileEdgeProcessor.class})
class AssetProfileEdgeProcessorTest extends AbstractAssetProcessorTest{
@SpyBean
AssetProfileEdgeProcessor assetProfileEdgeProcessor;
@ParameterizedTest
@MethodSource("provideParameters")
public void testAssetProfileDefaultFields_notSendToEdgeOlder3_6_0IfNotAssigned(EdgeVersion edgeVersion, long expectedDashboardIdMSB, long expectedDashboardIdLSB,
long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) {
updateAssetProfileDefaultFields(expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB);
edgeEvent.setEntityId(assetProfileId.getId());
DownlinkMsg downlinkMsg = assetProfileEdgeProcessor.convertAssetProfileEventToDownlink(edgeEvent, edgeId, edgeVersion);
verify(downlinkMsg, expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB);
}
}

100
application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/AbstractDeviceProcessorTest.java

@ -0,0 +1,100 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.device;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessorTest;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.BDDMockito.willReturn;
public abstract class AbstractDeviceProcessorTest extends BaseEdgeProcessorTest {
protected DeviceId deviceId;
protected DeviceProfileId deviceProfileId;
protected DeviceProfile deviceProfile;
@BeforeEach
public void setUp() {
edgeId = new EdgeId(UUID.randomUUID());
tenantId = new TenantId(UUID.randomUUID());
deviceId = new DeviceId(UUID.randomUUID());
deviceProfileId = new DeviceProfileId(UUID.randomUUID());
deviceProfile = new DeviceProfile();
deviceProfile.setId(deviceProfileId);
deviceProfile.setName("DeviceProfile");
deviceProfile.setDefault(true);
deviceProfile.setType(DeviceProfileType.DEFAULT);
DeviceProfileData deviceProfileData = new DeviceProfileData();
deviceProfile.setProfileData(deviceProfileData);
deviceProfile.setTransportType(DeviceTransportType.DEFAULT);
Device device = new Device();
device.setDeviceProfileId(deviceProfileId);
device.setId(deviceId);
device.setName("Device");
device.setType(deviceProfile.getName());
edgeEvent = new EdgeEvent();
edgeEvent.setTenantId(tenantId);
edgeEvent.setAction(EdgeEventActionType.ADDED);
willReturn(device).given(deviceService).findDeviceById(tenantId, deviceId);
willReturn(deviceProfile).given(deviceProfileService).findDeviceProfileById(tenantId, deviceProfileId);
willReturn(new byte[]{0x00}).given(dataDecodingEncodingService).encode(deviceProfileData);
}
protected void updateDeviceProfileDefaultFields(long expectedDashboardIdMSB, long expectedDashboardIdLSB,
long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) {
DashboardId dashboardId = getDashboardId(expectedDashboardIdMSB, expectedDashboardIdLSB);
RuleChainId ruleChainId = getRuleChainId(expectedRuleChainIdMSB, expectedRuleChainIdLSB);
deviceProfile.setDefaultDashboardId(dashboardId);
deviceProfile.setDefaultEdgeRuleChainId(ruleChainId);
}
protected void verify(DownlinkMsg downlinkMsg, long expectedDashboardIdMSB, long expectedDashboardIdLSB,
long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) {
DeviceProfileUpdateMsg deviceProfileUpdateMsg = downlinkMsg.getDeviceProfileUpdateMsgList().get(0);
assertNotNull(deviceProfileUpdateMsg);
Assertions.assertThat(deviceProfileUpdateMsg.getDefaultDashboardIdMSB()).isEqualTo(expectedDashboardIdMSB);
Assertions.assertThat(deviceProfileUpdateMsg.getDefaultDashboardIdLSB()).isEqualTo(expectedDashboardIdLSB);
Assertions.assertThat(deviceProfileUpdateMsg.getDefaultRuleChainIdMSB()).isEqualTo(expectedRuleChainIdMSB);
Assertions.assertThat(deviceProfileUpdateMsg.getDefaultRuleChainIdLSB()).isEqualTo(expectedRuleChainIdLSB);
}
}

43
application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessorTest.java

@ -0,0 +1,43 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.device;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
@SpringBootTest(classes = {DeviceEdgeProcessor.class})
class DeviceEdgeProcessorTest extends AbstractDeviceProcessorTest {
@SpyBean
DeviceEdgeProcessor deviceEdgeProcessor;
@ParameterizedTest
@MethodSource("provideParameters")
public void testDeviceProfileDefaultFields_notSendToEdgeOlder3_6_0IfNotAssigned(EdgeVersion edgeVersion, long expectedDashboardIdMSB, long expectedDashboardIdLSB,
long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) {
updateDeviceProfileDefaultFields(expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB);
edgeEvent.setEntityId(deviceId.getId());
DownlinkMsg downlinkMsg = deviceEdgeProcessor.convertDeviceEventToDownlink(edgeEvent, edgeId, edgeVersion);
verify(downlinkMsg, expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB);
}
}

45
application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceProfileEdgeProcessorTest.java

@ -0,0 +1,45 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor.device;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
@SpringBootTest(classes = {DeviceProfileEdgeProcessor.class})
class DeviceProfileEdgeProcessorTest extends AbstractDeviceProcessorTest {
@SpyBean
DeviceProfileEdgeProcessor deviceProfileEdgeProcessor;
@ParameterizedTest
@MethodSource("provideParameters")
public void testDeviceProfileDefaultFields_notSendToEdgeOlder3_6_0IfNotAssigned(EdgeVersion edgeVersion, long expectedDashboardIdMSB, long expectedDashboardIdLSB,
long expectedRuleChainIdMSB, long expectedRuleChainIdLSB) {
updateDeviceProfileDefaultFields(expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB);
edgeEvent.setEntityId(deviceProfileId.getId());
DownlinkMsg downlinkMsg = deviceProfileEdgeProcessor.convertDeviceProfileEventToDownlink(edgeEvent, edgeId, edgeVersion);
verify(downlinkMsg, expectedDashboardIdMSB, expectedDashboardIdLSB, expectedRuleChainIdMSB, expectedRuleChainIdLSB);
}
}

119
application/src/test/java/org/thingsboard/server/service/install/InstallScriptsTest.java

@ -0,0 +1,119 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.install;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService;
import org.thingsboard.server.dao.resource.ResourceService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.validator.RuleChainDataValidator;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.willReturn;
@Slf4j
@SpringBootTest(classes = {InstallScripts.class, RuleChainDataValidator.class})
class InstallScriptsTest {
@MockBean
RuleChainService ruleChainService;
@MockBean
DashboardService dashboardService;
@MockBean
WidgetTypeService widgetTypeService;
@MockBean
WidgetsBundleService widgetsBundleService;
@MockBean
OAuth2ConfigTemplateService oAuth2TemplateService;
@MockBean
ResourceService resourceService;
@SpyBean
InstallScripts installScripts;
@MockBean
TenantService tenantService;
@MockBean
ApiLimitService apiLimitService;
@SpyBean
RuleChainDataValidator ruleChainValidator;
TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da"));
@BeforeEach
void setUp() {
willReturn(true).given(tenantService).tenantExists(tenantId);
willReturn(true).given(apiLimitService).checkEntitiesLimit(any(), any());
}
@Test
void testDefaultRuleChainsTemplates() throws IOException {
Path dir = installScripts.getTenantRuleChainsDir();
installScripts.findRuleChainsFromPath(dir)
.forEach(this::validateRuleChainTemplate);
}
@Test
void testDefaultEdgeRuleChainsTemplates() throws IOException {
Path dir = installScripts.getEdgeRuleChainsDir();
installScripts.findRuleChainsFromPath(dir)
.forEach(this::validateRuleChainTemplate);
}
@Test
void testDeviceProfileDefaultRuleChainTemplate() {
validateRuleChainTemplate(installScripts.getDeviceProfileDefaultRuleChainTemplateFilePath());
}
private void validateRuleChainTemplate(Path templateFilePath) {
log.warn("validateRuleChainTemplate {}", templateFilePath);
JsonNode ruleChainJson = JacksonUtil.toJsonNode(templateFilePath.toFile());
RuleChain ruleChain = JacksonUtil.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class);
ruleChain.setTenantId(tenantId);
ruleChainValidator.validate(ruleChain, RuleChain::getTenantId);
ruleChain.setId(new RuleChainId(UUID.randomUUID()));
RuleChainMetaData ruleChainMetaData = JacksonUtil.treeToValue(ruleChainJson.get("metadata"), RuleChainMetaData.class);
ruleChainMetaData.setRuleChainId(ruleChain.getId());
List<Throwable> throwables = RuleChainDataValidator.validateMetaData(ruleChainMetaData);
assertThat(throwables).as("templateFilePath " + templateFilePath)
.containsExactlyInAnyOrderElementsOf(Collections.emptyList());
}
}

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

@ -975,6 +975,8 @@ message ToCoreMsg {
EdgeNotificationMsgProto edgeNotificationMsg = 5;
DeviceActivityProto deviceActivityMsg = 6;
NotificationSchedulerServiceMsg notificationSchedulerServiceMsg = 7;
LifecycleEventProto lifecycleEventMsg = 8;
ErrorEventProto errorEventMsg = 9;
}
/* High priority messages with low latency are handled by ThingsBoard Core Service separately */
@ -1066,3 +1068,24 @@ message NotificationSchedulerServiceMsg {
message NotificationRuleProcessorMsg {
bytes trigger = 1;
}
message ErrorEventProto {
int64 tenantIdMSB = 1;
int64 tenantIdLSB = 2;
int64 entityIdMSB = 3;
int64 entityIdLSB = 4;
string serviceId = 5;
string method = 6;
string error = 7;
}
message LifecycleEventProto {
int64 tenantIdMSB = 1;
int64 tenantIdLSB = 2;
int64 entityIdMSB = 3;
int64 entityIdLSB = 4;
string serviceId = 5;
string lcEventType = 6;
bool success = 7;
string error = 8;
}

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

@ -163,6 +163,15 @@ public class StringUtils {
return false;
}
public static boolean equalsAnyIgnoreCase(String string, String... otherStrings) {
for (String otherString : otherStrings) {
if (equalsIgnoreCase(string, otherString)) {
return true;
}
}
return false;
}
public static String substringAfterLast(String str, String sep) {
return org.apache.commons.lang3.StringUtils.substringAfterLast(str, sep);
}

6
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpDeviceProfileTransportConfiguration.java

@ -18,7 +18,6 @@ package org.thingsboard.server.common.data.device.profile;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.transport.snmp.SnmpMapping;
import org.thingsboard.server.common.data.transport.snmp.config.SnmpCommunicationConfig;
import java.util.List;
@ -45,8 +44,7 @@ public class SnmpDeviceProfileTransportConfiguration implements DeviceProfileTra
private boolean isValid() {
return timeoutMs != null && timeoutMs >= 0 && retries != null && retries >= 0
&& communicationConfigs != null
&& communicationConfigs.stream().allMatch(config -> config != null && config.isValid())
&& communicationConfigs.stream().flatMap(config -> config.getAllMappings().stream()).map(SnmpMapping::getOid)
.distinct().count() == communicationConfigs.stream().mapToInt(config -> config.getAllMappings().size()).sum();
&& communicationConfigs.stream().allMatch(config -> config != null && config.isValid());
}
}

4
common/data/src/main/java/org/thingsboard/server/common/data/sync/ie/importing/csv/BulkImportColumnType.java

@ -43,6 +43,10 @@ public enum BulkImportColumnType {
LWM2M_SERVER_SECURITY_MODE("securityMode", LwM2MSecurityMode.NO_SEC.name()),
LWM2M_SERVER_CLIENT_PUBLIC_KEY_OR_ID("clientPublicKeyOrId"),
LWM2M_SERVER_CLIENT_SECRET_KEY("clientSecretKey"),
SNMP_HOST,
SNMP_PORT,
SNMP_VERSION,
SNMP_COMMUNITY_STRING,
IS_GATEWAY,
DESCRIPTION,
ROUTING_KEY,

18
common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpCommunicationSpec.java

@ -15,11 +15,21 @@
*/
package org.thingsboard.server.common.data.transport.snmp;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public enum SnmpCommunicationSpec {
TELEMETRY_QUERYING,
CLIENT_ATTRIBUTES_QUERYING,
SHARED_ATTRIBUTES_SETTING,
TELEMETRY_QUERYING("telemetryQuerying"),
CLIENT_ATTRIBUTES_QUERYING("clientAttributesQuerying"),
SHARED_ATTRIBUTES_SETTING("sharedAttributesSetting"),
TO_DEVICE_RPC_REQUEST("toDeviceRpcRequest"),
TO_SERVER_RPC_REQUEST("toServerRpcRequest");
private final String label;
TO_DEVICE_RPC_REQUEST,
}

3
common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpMethod.java

@ -17,7 +17,8 @@ package org.thingsboard.server.common.data.transport.snmp;
public enum SnmpMethod {
GET(-96),
SET(-93);
SET(-93),
TRAP(-89);
// codes taken from org.snmp4j.PDU class
private final int code;

3
common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/SnmpCommunicationConfig.java

@ -37,7 +37,8 @@ import java.util.List;
@Type(value = TelemetryQueryingSnmpCommunicationConfig.class, name = "TELEMETRY_QUERYING"),
@Type(value = ClientAttributesQueryingSnmpCommunicationConfig.class, name = "CLIENT_ATTRIBUTES_QUERYING"),
@Type(value = SharedAttributesSettingSnmpCommunicationConfig.class, name = "SHARED_ATTRIBUTES_SETTING"),
@Type(value = ToDeviceRpcRequestSnmpCommunicationConfig.class, name = "TO_DEVICE_RPC_REQUEST")
@Type(value = ToDeviceRpcRequestSnmpCommunicationConfig.class, name = "TO_DEVICE_RPC_REQUEST"),
@Type(value = ToServerRpcRequestSnmpCommunicationConfig.class, name = "TO_SERVER_RPC_REQUEST")
})
public interface SnmpCommunicationConfig extends Serializable {

29
common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/ToServerRpcRequestSnmpCommunicationConfig.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.transport.snmp.config;
import org.thingsboard.server.common.data.transport.snmp.SnmpCommunicationSpec;
public class ToServerRpcRequestSnmpCommunicationConfig extends MultipleMappingsSnmpCommunicationConfig {
private static final long serialVersionUID = 4851028734093214L;
@Override
public SnmpCommunicationSpec getSpec() {
return SnmpCommunicationSpec.TO_SERVER_RPC_REQUEST;
}
}

32
application/src/main/java/org/thingsboard/server/utils/TypeCastUtil.java → common/data/src/main/java/org/thingsboard/server/common/data/util/TypeCastUtil.java

@ -13,35 +13,53 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.utils;
package org.thingsboard.server.common.data.util;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.thingsboard.server.common.data.kv.DataType;
import java.math.BigDecimal;
import java.util.Map;
public class TypeCastUtil {
private TypeCastUtil() {}
public static Map.Entry<DataType, Object> castValue(String value) {
public static Pair<DataType, Object> castValue(String value) {
if (isNumber(value)) {
String formattedValue = value.replace(',', '.');
try {
BigDecimal bd = new BigDecimal(formattedValue);
if (bd.stripTrailingZeros().scale() > 0 || isSimpleDouble(formattedValue)) {
if (bd.scale() <= 16) {
return Map.entry(DataType.DOUBLE, bd.doubleValue());
return Pair.of(DataType.DOUBLE, bd.doubleValue());
}
} else {
return Map.entry(DataType.LONG, bd.longValueExact());
return Pair.of(DataType.LONG, bd.longValueExact());
}
} catch (RuntimeException ignored) {}
} else if (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) {
return Map.entry(DataType.BOOLEAN, Boolean.parseBoolean(value));
return Pair.of(DataType.BOOLEAN, Boolean.parseBoolean(value));
}
return Pair.of(DataType.STRING, value);
}
public static Pair<DataType, Number> castToNumber(String value) {
if (isNumber(value)) {
String formattedValue = value.replace(',', '.');
BigDecimal bd = new BigDecimal(formattedValue);
if (bd.stripTrailingZeros().scale() > 0 || isSimpleDouble(formattedValue)) {
if (bd.scale() <= 16) {
return Pair.of(DataType.DOUBLE, bd.doubleValue());
} else {
return Pair.of(DataType.DOUBLE, bd);
}
} else {
return Pair.of(DataType.LONG, bd.longValueExact());
}
} else {
throw new IllegalArgumentException("'" + value + "' can't be parsed as number");
}
return Map.entry(DataType.STRING, value);
}
private static boolean isNumber(String value) {

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

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

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

@ -34,6 +34,7 @@ enum EdgeVersion {
V_3_3_0 = 0;
V_3_3_3 = 1;
V_3_4_0 = 2;
V_3_6_0 = 3;
}
/**

50
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/DateFormattingOptions.java

@ -0,0 +1,50 @@
package org.thingsboard.script.api.tbel;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.StringUtils;
import java.time.format.FormatStyle;
import java.util.TimeZone;
@NoArgsConstructor
@Data
class DateFormattingOptions {
private static final TimeZone DEFAULT_TZ = TimeZone.getDefault();
private String timeZone;
private String dateStyle;
private String timeStyle;
@Getter
private String pattern;
public DateFormattingOptions(String timeZone) {
this.timeZone = timeZone;
}
TimeZone getTimeZone() {
return StringUtils.isNotEmpty(timeZone) ? TimeZone.getTimeZone(timeZone) : TimeZone.getDefault();
}
FormatStyle getDateStyle() {
return getFormatStyle(dateStyle, FormatStyle.SHORT);
}
FormatStyle getTimeStyle() {
return getFormatStyle(timeStyle, FormatStyle.MEDIUM);
}
private static FormatStyle getFormatStyle(String style, FormatStyle defaultStyle) {
if (StringUtils.isNotEmpty(style)) {
try {
return FormatStyle.valueOf(style.toUpperCase());
} catch (IllegalArgumentException e) {
return defaultStyle;
}
} else {
return defaultStyle;
}
}
}

84
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbDate.java

@ -5,7 +5,7 @@
* 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
* 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,
@ -15,6 +15,9 @@
*/
package org.thingsboard.script.api.tbel;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.StringUtils;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Instant;
@ -22,13 +25,16 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.chrono.IsoChronology;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAccessor;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
import java.util.function.BiFunction;
import java.util.function.Function;
public class TbDate extends Date {
@ -77,15 +83,77 @@ public class TbDate extends Date {
return isoDateFormat.get().format(this);
}
public String toLocaleDateString() {
return toLocaleDateString(null, null);
}
public String toLocaleDateString(String locale) {
return toLocaleDateString(locale, null);
}
public String toLocaleDateString(String localeStr, String optionsStr) {
return toLocaleString(localeStr, optionsStr, (locale, options) -> DateTimeFormatter.ofLocalizedDate(options.getDateStyle()).withLocale(locale));
}
public String toLocaleTimeString() {
return toLocaleTimeString(null, null);
}
public String toLocaleTimeString(String locale) {
return toLocaleTimeString(locale, null);
}
public String toLocaleTimeString(String localeStr, String optionsStr) {
return toLocaleString(localeStr, optionsStr, (locale, options) -> DateTimeFormatter.ofLocalizedTime(options.getTimeStyle()).withLocale(locale));
}
@Override
public String toLocaleString() {
return toLocaleString(null, null);
}
public String toLocaleString(String locale) {
DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.forLanguageTag(locale));
return formatter.format(this);
return toLocaleString(locale, null);
}
public String toLocaleString(String locale, String tz) {
DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.forLanguageTag(locale));
formatter.setTimeZone(TimeZone.getTimeZone(tz));
return formatter.format(this);
public String toLocaleString(String localeStr, String optionsStr) {
return toLocaleString(localeStr, optionsStr, (locale, options) -> {
String formatPattern =
DateTimeFormatterBuilder.getLocalizedDateTimePattern(
options.getDateStyle(),
options.getTimeStyle(),
IsoChronology.INSTANCE,
locale);
return DateTimeFormatter.ofPattern(formatPattern, locale);
});
}
public String toLocaleString(String localeStr, String optionsStr, BiFunction<Locale, DateFormattingOptions, DateTimeFormatter> formatterBuilder) {
Locale locale = StringUtils.isNotEmpty(localeStr) ? Locale.forLanguageTag(localeStr) : Locale.getDefault();
DateFormattingOptions options = getDateFormattingOptions(optionsStr);
ZonedDateTime zdt = this.toInstant().atZone(options.getTimeZone().toZoneId());
DateTimeFormatter formatter;
if (StringUtils.isNotEmpty(options.getPattern())) {
formatter = new DateTimeFormatterBuilder().appendPattern(options.getPattern()).toFormatter(locale);
} else {
formatter = formatterBuilder.apply(locale, options);
}
return formatter.format(zdt);
}
private static DateFormattingOptions getDateFormattingOptions(String options) {
DateFormattingOptions opt = null;
if (StringUtils.isNotEmpty(options)) {
try {
opt = JacksonUtil.fromString(options, DateFormattingOptions.class);
} catch (IllegalArgumentException iae) {
opt = new DateFormattingOptions(options);
}
}
if (opt == null) {
opt = new DateFormattingOptions();
}
return opt;
}
public static long now() {

182
common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbDateTest.java

@ -21,12 +21,24 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.thingsboard.common.util.JacksonUtil;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.chrono.IsoChronology;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
@ -124,4 +136,174 @@ class TbDateTest {
.startsWith(datePrefix);
}
@Test
void testToLocaleDateString() {
TbDate d = new TbDate(1693962245000L);
// Depends on time zone, so we just check it works;
Assert.assertNotNull(d.toLocaleDateString());
Assert.assertNotNull(d.toLocaleDateString("en-US"));
Assert.assertEquals("9/5/23", d.toLocaleDateString("en-US", "America/New_York"));
Assert.assertEquals("23. 9. 5.", d.toLocaleDateString("ko-KR", "America/New_York"));
Assert.assertEquals("06.09.23", d.toLocaleDateString( "uk-UA", "Europe/Kiev"));
Assert.assertEquals("5\u200F/9\u200F/2023", d.toLocaleDateString( "ar-EG", "America/New_York"));
Assert.assertEquals("Tuesday, September 5, 2023", d.toLocaleDateString("en-US", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("dateStyle", "full")
.toString()));
Assert.assertEquals("2023년 9월 5일 화요일", d.toLocaleDateString("ko-KR", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("dateStyle", "full")
.toString()));
Assert.assertEquals("середа, 6 вересня 2023 р.", d.toLocaleDateString("uk-UA", JacksonUtil.newObjectNode()
.put("timeZone", "Europe/Kiev")
.put("dateStyle", "full")
.toString()));
Assert.assertEquals("الثلاثاء، 5 سبتمبر 2023", d.toLocaleDateString("ar-EG", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("dateStyle", "full")
.toString()));
Assert.assertEquals("Tuesday 9/5/2023", d.toLocaleDateString("en-US", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("pattern", "EEEE M/d/yyyy")
.toString()));
Assert.assertEquals("화요일 9/5/2023", d.toLocaleDateString("ko-KR", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("pattern", "EEEE M/d/yyyy")
.toString()));
Assert.assertEquals("середа 9/6/2023", d.toLocaleDateString("uk-UA", JacksonUtil.newObjectNode()
.put("timeZone", "Europe/Kiev")
.put("pattern", "EEEE M/d/yyyy")
.toString()));
Assert.assertEquals("الثلاثاء 9/5/2023", d.toLocaleDateString("ar-EG", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("pattern", "EEEE M/d/yyyy")
.toString()));
}
@Test
void testToLocaleTimeString() {
TbDate d = new TbDate(1693962245000L);
// Depends on time zone, so we just check it works;
Assert.assertNotNull(d.toLocaleTimeString());
Assert.assertNotNull(d.toLocaleTimeString("en-US"));
Assert.assertEquals("9:04:05 PM", d.toLocaleTimeString("en-US", "America/New_York"));
Assert.assertEquals("오후 9:04:05", d.toLocaleTimeString("ko-KR", "America/New_York"));
Assert.assertEquals("04:04:05", d.toLocaleTimeString( "uk-UA", "Europe/Kiev"));
Assert.assertEquals("9:04:05 م", d.toLocaleTimeString( "ar-EG", "America/New_York"));
Assert.assertEquals("9:04:05 PM Eastern Daylight Time", d.toLocaleTimeString("en-US", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("timeStyle", "full")
.toString()));
Assert.assertEquals("오후 9시 4분 5초 미 동부 하계 표준시", d.toLocaleTimeString("ko-KR", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("timeStyle", "full")
.toString()));
Assert.assertEquals("04:04:05 за східноєвропейським літнім часом", d.toLocaleTimeString("uk-UA", JacksonUtil.newObjectNode()
.put("timeZone", "Europe/Kiev")
.put("timeStyle", "full")
.toString()));
Assert.assertEquals("9:04:05 م التوقيت الصيفي الشرقي لأمريكا الشمالية", d.toLocaleTimeString("ar-EG", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("timeStyle", "full")
.toString()));
Assert.assertEquals("9:04:05 PM", d.toLocaleTimeString("en-US", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("pattern", "h:mm:ss a")
.toString()));
Assert.assertEquals("9:04:05 오후", d.toLocaleTimeString("ko-KR", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("pattern", "h:mm:ss a")
.toString()));
Assert.assertEquals("4:04:05 дп", d.toLocaleTimeString("uk-UA", JacksonUtil.newObjectNode()
.put("timeZone", "Europe/Kiev")
.put("pattern", "h:mm:ss a")
.toString()));
Assert.assertEquals("9:04:05 م", d.toLocaleTimeString("ar-EG", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("pattern", "h:mm:ss a")
.toString()));
}
@Test
void testToLocaleString() {
TbDate d = new TbDate(1693962245000L);
// Depends on time zone, so we just check it works;
Assert.assertNotNull(d.toLocaleString());
Assert.assertNotNull(d.toLocaleString("en-US"));
Assert.assertEquals("9/5/23, 9:04:05 PM", d.toLocaleString("en-US", "America/New_York"));
Assert.assertEquals("23. 9. 5. 오후 9:04:05", d.toLocaleString("ko-KR", "America/New_York"));
Assert.assertEquals("06.09.23, 04:04:05", d.toLocaleString( "uk-UA", "Europe/Kiev"));
Assert.assertEquals("5\u200F/9\u200F/2023 9:04:05 م", d.toLocaleString( "ar-EG", "America/New_York"));
Assert.assertEquals("Tuesday, September 5, 2023 at 9:04:05 PM Eastern Daylight Time", d.toLocaleString("en-US", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("dateStyle", "full")
.put("timeStyle", "full")
.toString()));
Assert.assertEquals("2023년 9월 5일 화요일 오후 9시 4분 5초 미 동부 하계 표준시", d.toLocaleString("ko-KR", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("dateStyle", "full")
.put("timeStyle", "full")
.toString()));
Assert.assertEquals("середа, 6 вересня 2023 р. о 04:04:05 за східноєвропейським літнім часом", d.toLocaleString("uk-UA", JacksonUtil.newObjectNode()
.put("timeZone", "Europe/Kiev")
.put("dateStyle", "full")
.put("timeStyle", "full")
.toString()));
Assert.assertEquals("الثلاثاء، 5 سبتمبر 2023 9:04:05 م التوقيت الصيفي الشرقي لأمريكا الشمالية", d.toLocaleString("ar-EG", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("dateStyle", "full")
.put("timeStyle", "full")
.toString()));
Assert.assertEquals("9/5/2023, 9:04:05 PM", d.toLocaleString("en-US", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("pattern", "M/d/yyyy, h:mm:ss a")
.toString()));
Assert.assertEquals("9/5/2023, 9:04:05 오후", d.toLocaleString("ko-KR", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("pattern", "M/d/yyyy, h:mm:ss a")
.toString()));
Assert.assertEquals("9/6/2023, 4:04:05 дп", d.toLocaleString("uk-UA", JacksonUtil.newObjectNode()
.put("timeZone", "Europe/Kiev")
.put("pattern", "M/d/yyyy, h:mm:ss a")
.toString()));
Assert.assertEquals("9/5/2023, 9:04:05 م", d.toLocaleString("ar-EG", JacksonUtil.newObjectNode()
.put("timeZone", "America/New_York")
.put("pattern", "M/d/yyyy, h:mm:ss a")
.toString()));
}
private static String toLocalString(TbDate d, Locale locale, ZoneId tz) {
LocalDateTime ldt = d.toInstant().atZone(tz).toLocalDateTime();
// new DateTimeFormatterBuilder().appendPattern(pattern).toFormatter(locale)
String formatPattern =
DateTimeFormatterBuilder.getLocalizedDateTimePattern(
FormatStyle.SHORT,
FormatStyle.MEDIUM,
IsoChronology.INSTANCE,
locale);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatPattern, locale);
return formatter.format(ldt);
}
private static String toLocalString2(TbDate d, Locale locale, ZoneId tz) {
DateFormat dateFormat = SimpleDateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, locale);
dateFormat.setTimeZone(TimeZone.getTimeZone(tz));
return dateFormat.format(d);
}
}

74
common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfigu
import org.thingsboard.server.common.data.device.profile.SnmpDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.transport.DeviceUpdatedEvent;
@ -113,19 +114,25 @@ public class SnmpTransportContext extends TransportContext {
SnmpDeviceProfileTransportConfiguration profileTransportConfiguration = (SnmpDeviceProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration();
SnmpDeviceTransportConfiguration deviceTransportConfiguration = (SnmpDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration();
DeviceSessionContext deviceSessionContext;
DeviceSessionContext sessionContext;
try {
deviceSessionContext = new DeviceSessionContext(
device, deviceProfile, credentials.getCredentialsId(),
profileTransportConfiguration, deviceTransportConfiguration, this
);
registerSessionMsgListener(deviceSessionContext);
sessionContext = DeviceSessionContext.builder()
.tenantId(deviceProfile.getTenantId())
.device(device)
.deviceProfile(deviceProfile)
.token(credentials.getCredentialsId())
.profileTransportConfiguration(profileTransportConfiguration)
.deviceTransportConfiguration(deviceTransportConfiguration)
.snmpTransportContext(this)
.build();
registerSessionMsgListener(sessionContext);
} catch (Exception e) {
log.error("Failed to establish session for SNMP device {}: {}", device.getId(), e.toString());
transportService.errorEvent(device.getTenantId(), device.getId(), "sessionEstablishing", e);
return;
}
sessions.put(device.getId(), deviceSessionContext);
snmpTransportService.createQueryingTasks(deviceSessionContext);
sessions.put(device.getId(), sessionContext);
snmpTransportService.createQueryingTasks(sessionContext);
log.info("Established SNMP device session for device {}", device.getId());
}
@ -148,14 +155,17 @@ public class SnmpTransportContext extends TransportContext {
sessionContext.initializeTarget(newProfileTransportConfiguration, newDeviceTransportConfiguration);
snmpTransportService.cancelQueryingTasks(sessionContext);
snmpTransportService.createQueryingTasks(sessionContext);
transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.UPDATED, true, null);
} else if (!newDeviceTransportConfiguration.equals(sessionContext.getDeviceTransportConfiguration())) {
sessionContext.setDeviceTransportConfiguration(newDeviceTransportConfiguration);
sessionContext.initializeTarget(newProfileTransportConfiguration, newDeviceTransportConfiguration);
transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.UPDATED, true, null);
} else {
log.trace("Configuration of the device {} was not updated", device);
}
} catch (Exception e) {
log.error("Failed to update session for SNMP device {}: {}", sessionContext.getDeviceId(), e.getMessage());
transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.UPDATED, false, e);
destroyDeviceSession(sessionContext);
}
}
@ -168,43 +178,58 @@ public class SnmpTransportContext extends TransportContext {
transportService.deregisterSession(sessionContext.getSessionInfo());
snmpTransportService.cancelQueryingTasks(sessionContext);
sessions.remove(sessionContext.getDeviceId());
transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.STOPPED, true, null);
log.trace("Unregistered and removed session");
}
private void registerSessionMsgListener(DeviceSessionContext deviceSessionContext) {
private void registerSessionMsgListener(DeviceSessionContext sessionContext) {
transportService.process(DeviceTransportType.SNMP,
TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceSessionContext.getToken()).build(),
TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(sessionContext.getToken()).build(),
new TransportServiceCallback<>() {
@Override
public void onSuccess(ValidateDeviceCredentialsResponse msg) {
if (msg.hasDeviceInfo()) {
SessionInfoProto sessionInfo = SessionInfoCreator.create(
msg, SnmpTransportContext.this, UUID.randomUUID()
);
transportService.registerAsyncSession(sessionInfo, deviceSessionContext);
transportService.process(sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder().build(), TransportServiceCallback.EMPTY);
transportService.process(sessionInfo, TransportProtos.SubscribeToRPCMsg.newBuilder().build(), TransportServiceCallback.EMPTY);
deviceSessionContext.setSessionInfo(sessionInfo);
deviceSessionContext.setDeviceInfo(msg.getDeviceInfo());
deviceSessionContext.setConnected(true);
registerTransportSession(sessionContext, msg);
sessionContext.setSessionTimeoutHandler(() -> {
registerTransportSession(sessionContext, msg);
});
transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.STARTED, true, null);
} else {
log.warn("[{}] Failed to process device auth", deviceSessionContext.getDeviceId());
log.warn("[{}] Failed to process device auth", sessionContext.getDeviceId());
}
}
@Override
public void onError(Throwable e) {
log.warn("[{}] Failed to process device auth: {}", deviceSessionContext.getDeviceId(), e);
log.warn("[{}] Failed to process device auth: {}", sessionContext.getDeviceId(), e);
transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.STARTED, false, e);
}
});
}
private void registerTransportSession(DeviceSessionContext deviceSessionContext, ValidateDeviceCredentialsResponse msg) {
SessionInfoProto sessionInfo = SessionInfoCreator.create(
msg, SnmpTransportContext.this, UUID.randomUUID()
);
log.debug("Registering transport session: {}", sessionInfo);
transportService.registerAsyncSession(sessionInfo, deviceSessionContext);
transportService.process(sessionInfo, TransportProtos.SubscribeToAttributeUpdatesMsg.newBuilder()
.setSessionType(TransportProtos.SessionType.ASYNC)
.build(), TransportServiceCallback.EMPTY);
transportService.process(sessionInfo, TransportProtos.SubscribeToRPCMsg.newBuilder()
.setSessionType(TransportProtos.SessionType.ASYNC)
.build(), TransportServiceCallback.EMPTY);
deviceSessionContext.setSessionInfo(sessionInfo);
deviceSessionContext.setDeviceInfo(msg.getDeviceInfo());
deviceSessionContext.setConnected(true);
}
@EventListener(DeviceUpdatedEvent.class)
public void onDeviceUpdatedOrCreated(DeviceUpdatedEvent deviceUpdatedEvent) {
Device device = deviceUpdatedEvent.getDevice();
log.trace("Got creating or updating device event for device {}", device);
log.debug("Got creating or updating device event for device {}", device);
DeviceTransportType transportType = Optional.ofNullable(device.getDeviceData().getTransportConfiguration())
.map(DeviceTransportConfiguration::getType)
.orElse(null);
@ -238,6 +263,7 @@ public class SnmpTransportContext extends TransportContext {
}
public void onDeviceProfileUpdated(DeviceProfile deviceProfile, DeviceSessionContext sessionContext) {
log.debug("Handling device profile {} update event for device {}", deviceProfile.getId(), sessionContext.getDeviceId());
updateDeviceSession(sessionContext, sessionContext.getDevice(), deviceProfile);
}

97
common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/PduService.java

@ -15,7 +15,9 @@
*/
package org.thingsboard.server.transport.snmp.service;
import com.google.common.collect.Lists;
import com.google.gson.JsonObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.snmp4j.PDU;
import org.snmp4j.ScopedPDU;
@ -25,43 +27,61 @@ import org.snmp4j.smi.OID;
import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.Variable;
import org.snmp4j.smi.VariableBinding;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration;
import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.common.data.transport.snmp.SnmpMapping;
import org.thingsboard.server.common.data.transport.snmp.SnmpMethod;
import org.thingsboard.server.common.data.transport.snmp.SnmpProtocolVersion;
import org.thingsboard.server.common.data.transport.snmp.config.SnmpCommunicationConfig;
import org.thingsboard.server.common.data.util.TypeCastUtil;
import org.thingsboard.server.queue.util.TbSnmpTransportComponent;
import org.thingsboard.server.transport.snmp.session.DeviceSessionContext;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@TbSnmpTransportComponent
@Service
@Slf4j
@RequiredArgsConstructor
public class PduService {
public PDU createPdu(DeviceSessionContext sessionContext, SnmpCommunicationConfig communicationConfig, Map<String, String> values) {
PDU pdu = setUpPdu(sessionContext);
pdu.setType(communicationConfig.getMethod().getCode());
pdu.addAll(communicationConfig.getAllMappings().stream()
.filter(mapping -> values.isEmpty() || values.containsKey(mapping.getKey()))
.map(mapping -> Optional.ofNullable(values.get(mapping.getKey()))
.map(value -> {
Variable variable = toSnmpVariable(value, mapping.getDataType());
return new VariableBinding(new OID(mapping.getOid()), variable);
})
.orElseGet(() -> new VariableBinding(new OID(mapping.getOid()))))
.collect(Collectors.toList()));
@Value("${transport.snmp.max_request_oids:100}")
private int maxRequestOids;
@Value("${transport.snmp.response.ignore_type_cast_errors:false}")
private boolean ignoreTypeCastErrors;
public List<PDU> createPdus(DeviceSessionContext sessionContext, SnmpCommunicationConfig communicationConfig, Map<String, String> values) {
List<PDU> pdus = new ArrayList<>();
List<SnmpMapping> allMappings = communicationConfig.getAllMappings();
for (List<SnmpMapping> mappings : Lists.partition(allMappings, maxRequestOids)) {
PDU pdu = setUpPdu(sessionContext);
pdu.setType(communicationConfig.getMethod().getCode());
pdu.addAll(mappings.stream()
.filter(mapping -> values.isEmpty() || values.containsKey(mapping.getKey()))
.map(mapping -> Optional.ofNullable(values.get(mapping.getKey()))
.map(value -> {
Variable variable = toSnmpVariable(value, mapping.getDataType());
return new VariableBinding(new OID(mapping.getOid()), variable);
})
.orElseGet(() -> new VariableBinding(new OID(mapping.getOid()))))
.collect(Collectors.toList()));
if (pdu.size() > 0) {
pdus.add(pdu);
}
}
return pdu;
return pdus;
}
public PDU createSingleVariablePdu(DeviceSessionContext sessionContext, SnmpMethod snmpMethod, String oid, String value, DataType dataType) {
@ -116,8 +136,8 @@ public class PduService {
}
public JsonObject processPdu(PDU pdu, List<SnmpMapping> responseMappings) {
Map<OID, String> values = processPdu(pdu);
public JsonObject processPdus(List<PDU> pdus, List<SnmpMapping> responseMappings) {
Map<OID, String> values = processPdus(pdus);
Map<OID, SnmpMapping> mappings = new HashMap<>();
if (responseMappings != null) {
@ -143,29 +163,40 @@ public class PduService {
return data;
}
public Map<OID, String> processPdu(PDU pdu) {
return IntStream.range(0, pdu.size())
.mapToObj(pdu::get)
public Map<OID, String> processPdus(List<PDU> pdus) {
return pdus.stream()
.flatMap(pdu -> pdu.getVariableBindings().stream())
.filter(Objects::nonNull)
.filter(variableBinding -> !(variableBinding.getVariable() instanceof Null))
.collect(Collectors.toMap(VariableBinding::getOid, VariableBinding::toValueString));
}
public void processValue(String key, DataType dataType, String value, JsonObject result) {
switch (dataType) {
case LONG:
result.addProperty(key, Long.parseLong(value));
break;
case BOOLEAN:
result.addProperty(key, Boolean.parseBoolean(value));
break;
case DOUBLE:
result.addProperty(key, Double.parseDouble(value));
break;
case STRING:
case JSON:
default:
result.addProperty(key, value);
try {
switch (dataType) {
case STRING:
case JSON:
result.addProperty(key, value);
break;
case LONG:
case DOUBLE:
result.addProperty(key, TypeCastUtil.castToNumber(value).getValue());
break;
case BOOLEAN:
if (StringUtils.equalsAnyIgnoreCase(value, "true", "false")) {
result.addProperty(key, Boolean.parseBoolean(value));
} else {
throw new IllegalArgumentException("Can't parse '" + value + "' as boolean");
}
break;
}
} catch (IllegalArgumentException e) {
if (ignoreTypeCastErrors) {
log.debug("Ignoring value '{}' for key '{}' because of data type mismatch ({} required)", value, key, dataType);
} else {
throw e;
}
}
}
}

221
common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java

@ -17,10 +17,13 @@ package org.thingsboard.server.transport.snmp.service;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.snmp4j.CommandResponder;
import org.snmp4j.CommandResponderEvent;
import org.snmp4j.PDU;
import org.snmp4j.Snmp;
import org.snmp4j.TransportMapping;
@ -29,10 +32,15 @@ import org.snmp4j.mp.MPv3;
import org.snmp4j.security.SecurityModels;
import org.snmp4j.security.SecurityProtocols;
import org.snmp4j.security.USM;
import org.snmp4j.smi.Address;
import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.TcpAddress;
import org.snmp4j.smi.UdpAddress;
import org.snmp4j.transport.DefaultTcpTransportMapping;
import org.snmp4j.transport.DefaultUdpTransportMapping;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
@ -45,15 +53,16 @@ import org.thingsboard.server.common.data.transport.snmp.SnmpMethod;
import org.thingsboard.server.common.data.transport.snmp.config.RepeatingQueryingSnmpCommunicationConfig;
import org.thingsboard.server.common.data.transport.snmp.config.SnmpCommunicationConfig;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.util.TbSnmpTransportComponent;
import org.thingsboard.server.transport.snmp.SnmpTransportContext;
import org.thingsboard.server.transport.snmp.session.DeviceSessionContext;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
@ -71,9 +80,11 @@ import java.util.stream.Collectors;
@Service
@Slf4j
@RequiredArgsConstructor
public class SnmpTransportService implements TbTransportService {
public class SnmpTransportService implements TbTransportService, CommandResponder {
private final TransportService transportService;
private final PduService pduService;
@Autowired @Lazy
private SnmpTransportContext transportContext;
@Getter
private Snmp snmp;
@ -83,6 +94,8 @@ public class SnmpTransportService implements TbTransportService {
private final Map<SnmpCommunicationSpec, ResponseDataMapper> responseDataMappers = new EnumMap<>(SnmpCommunicationSpec.class);
private final Map<SnmpCommunicationSpec, ResponseProcessor> responseProcessors = new EnumMap<>(SnmpCommunicationSpec.class);
@Value("${transport.snmp.bind_port:1620}")
private Integer snmpBindPort;
@Value("${transport.snmp.response_processing.parallelism_level}")
private Integer responseProcessingParallelismLevel;
@Value("${transport.snmp.underlying_protocol}")
@ -114,15 +127,16 @@ public class SnmpTransportService implements TbTransportService {
TransportMapping<?> transportMapping;
switch (snmpUnderlyingProtocol) {
case "udp":
transportMapping = new DefaultUdpTransportMapping();
transportMapping = new DefaultUdpTransportMapping(new UdpAddress(snmpBindPort));
break;
case "tcp":
transportMapping = new DefaultTcpTransportMapping();
transportMapping = new DefaultTcpTransportMapping(new TcpAddress(snmpBindPort));
break;
default:
throw new IllegalArgumentException("Underlying protocol " + snmpUnderlyingProtocol + " for SNMP is not supported");
}
snmp = new Snmp(transportMapping);
snmp.addNotificationListener(transportMapping, transportMapping.getListenAddress(), this);
snmp.listen();
USM usm = new USM(SecurityProtocols.getInstance(), new OctetString(MPv3.createLocalEngineID()), 0);
@ -143,6 +157,7 @@ public class SnmpTransportService implements TbTransportService {
}
} catch (Exception e) {
log.error("Failed to send SNMP request for device {}: {}", sessionContext.getDeviceId(), e.toString());
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), config.getSpec().getLabel(), e);
}
}, queryingFrequency, queryingFrequency, TimeUnit.MILLISECONDS);
})
@ -161,18 +176,24 @@ public class SnmpTransportService implements TbTransportService {
}
private void sendRequest(DeviceSessionContext sessionContext, SnmpCommunicationConfig communicationConfig, Map<String, String> values) {
PDU request = pduService.createPdu(sessionContext, communicationConfig, values);
RequestInfo requestInfo = new RequestInfo(communicationConfig.getSpec(), communicationConfig.getAllMappings());
sendRequest(sessionContext, request, requestInfo);
List<PDU> request = pduService.createPdus(sessionContext, communicationConfig, values);
RequestContext requestContext = RequestContext.builder()
.communicationSpec(communicationConfig.getSpec())
.method(communicationConfig.getMethod())
.responseMappings(communicationConfig.getAllMappings())
.requestSize(request.size())
.build();
sendRequest(sessionContext, request, requestContext);
}
private void sendRequest(DeviceSessionContext sessionContext, PDU request, RequestInfo requestInfo) {
if (request.size() > 0) {
log.trace("Executing SNMP request for device {}. Variables bindings: {}", sessionContext.getDeviceId(), request.getVariableBindings());
private void sendRequest(DeviceSessionContext sessionContext, List<PDU> request, RequestContext requestContext) {
for (PDU pdu : request) {
log.debug("Executing SNMP request for device {} with {} variable bindings", sessionContext.getDeviceId(), pdu.size());
try {
snmp.send(request, sessionContext.getTarget(), requestInfo, sessionContext);
snmp.send(pdu, sessionContext.getTarget(), requestContext, sessionContext);
} catch (IOException e) {
log.error("Failed to send SNMP request to device {}: {}", sessionContext.getDeviceId(), e.toString());
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), e);
}
}
}
@ -215,51 +236,135 @@ public class SnmpTransportService implements TbTransportService {
DataType dataType = snmpMapping.getDataType();
PDU request = pduService.createSingleVariablePdu(sessionContext, snmpMethod, oid, value, dataType);
RequestInfo requestInfo = new RequestInfo(toDeviceRpcRequestMsg.getRequestId(), communicationConfig.getSpec(), communicationConfig.getAllMappings());
sendRequest(sessionContext, request, requestInfo);
RequestContext requestContext = RequestContext.builder()
.requestId(toDeviceRpcRequestMsg.getRequestId())
.communicationSpec(communicationConfig.getSpec())
.method(snmpMethod)
.responseMappings(communicationConfig.getAllMappings())
.requestSize(1)
.build();
sendRequest(sessionContext, List.of(request), requestContext);
}
public void processResponseEvent(DeviceSessionContext sessionContext, ResponseEvent event) {
((Snmp) event.getSource()).cancel(event.getRequest(), sessionContext);
RequestContext requestContext = (RequestContext) event.getUserObject();
if (event.getError() != null) {
log.warn("SNMP response error: {}", event.getError().toString());
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), new RuntimeException(event.getError()));
return;
}
PDU response = event.getResponse();
if (response == null) {
log.debug("No response from SNMP device {}, requestId: {}", sessionContext.getDeviceId(), event.getRequest().getRequestID());
PDU responsePdu = event.getResponse();
if (log.isTraceEnabled()) {
log.trace("Received PDU for device {}: {}", sessionContext.getDeviceId(), responsePdu);
}
List<PDU> response;
if (requestContext.getRequestSize() == 1) {
if (responsePdu == null) {
log.debug("No response from SNMP device {}, requestId: {}", sessionContext.getDeviceId(), event.getRequest().getRequestID());
if (requestContext.getMethod() == SnmpMethod.GET) {
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), new RuntimeException("No response from device"));
}
return;
}
response = List.of(responsePdu);
} else {
List<PDU> responseParts = requestContext.getResponseParts();
responseParts.add(responsePdu);
if (responseParts.size() == requestContext.getRequestSize()) {
response = new ArrayList<>();
for (PDU responsePart : responseParts) {
if (responsePart != null) {
response.add(responsePart);
}
}
log.debug("All response parts are collected for request to device {}", sessionContext.getDeviceId());
} else {
log.trace("Awaiting other response parts for request to device {}", sessionContext.getDeviceId());
return;
}
}
responseProcessingExecutor.execute(() -> {
try {
processResponse(sessionContext, response, requestContext);
} catch (Exception e) {
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), e);
}
});
}
/*
* SNMP notifications handler
*
* TODO: add check for host uniqueness when saving device (for backward compatibility - only for the ones using from-device RPC requests)
*
* NOTE: SNMP TRAPs support won't work properly when there is more than one SNMP transport,
* due to load-balancing of requests from devices: session might not be on this instance
* */
@Override
public void processPdu(CommandResponderEvent event) {
Address sourceAddress = event.getPeerAddress();
DeviceSessionContext sessionContext = transportContext.getSessions().stream()
.filter(session -> session.getTarget().getAddress().equals(sourceAddress))
.findFirst().orElse(null);
if (sessionContext == null) {
log.warn("SNMP TRAP processing failed: couldn't find device session for address {}", sourceAddress);
return;
}
RequestInfo requestInfo = (RequestInfo) event.getUserObject();
try {
processIncomingTrap(sessionContext, event);
} catch (Throwable e) {
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(),
SnmpCommunicationSpec.TO_SERVER_RPC_REQUEST.getLabel(), e);
}
}
private void processIncomingTrap(DeviceSessionContext sessionContext, CommandResponderEvent event) {
PDU pdu = event.getPDU();
if (pdu == null) {
log.warn("Got empty trap from device {}", sessionContext.getDeviceId());
throw new IllegalArgumentException("Received TRAP with no data");
}
log.debug("Processing SNMP trap from device {} (PDU: {}}", sessionContext.getDeviceId(), pdu);
SnmpCommunicationConfig communicationConfig = sessionContext.getProfileTransportConfiguration().getCommunicationConfigs().stream()
.filter(config -> config.getSpec() == SnmpCommunicationSpec.TO_SERVER_RPC_REQUEST).findFirst()
.orElseThrow(() -> new IllegalArgumentException("No config found for to-server RPC requests"));
RequestContext requestContext = RequestContext.builder()
.communicationSpec(communicationConfig.getSpec())
.responseMappings(communicationConfig.getAllMappings())
.method(SnmpMethod.TRAP)
.build();
responseProcessingExecutor.execute(() -> {
processResponse(sessionContext, response, requestInfo);
processResponse(sessionContext, List.of(pdu), requestContext);
});
}
private void processResponse(DeviceSessionContext sessionContext, PDU response, RequestInfo requestInfo) {
ResponseProcessor responseProcessor = responseProcessors.get(requestInfo.getCommunicationSpec());
private void processResponse(DeviceSessionContext sessionContext, List<PDU> response, RequestContext requestContext) {
ResponseProcessor responseProcessor = responseProcessors.get(requestContext.getCommunicationSpec());
if (responseProcessor == null) return;
JsonObject responseData = responseDataMappers.get(requestInfo.getCommunicationSpec()).map(response, requestInfo);
if (responseData.entrySet().isEmpty()) {
log.debug("No values is the SNMP response for device {}. Request id: {}", sessionContext.getDeviceId(), response.getRequestID());
return;
JsonObject responseData = responseDataMappers.get(requestContext.getCommunicationSpec()).map(response, requestContext);
if (responseData.size() == 0) {
log.warn("No values in the SNMP response for device {}", sessionContext.getDeviceId());
throw new IllegalArgumentException("No values in the response");
}
responseProcessor.process(responseData, requestInfo, sessionContext);
responseProcessor.process(responseData, requestContext, sessionContext);
reportActivity(sessionContext.getSessionInfo());
}
private void configureResponseDataMappers() {
responseDataMappers.put(SnmpCommunicationSpec.TO_DEVICE_RPC_REQUEST, (pdu, requestInfo) -> {
responseDataMappers.put(SnmpCommunicationSpec.TO_DEVICE_RPC_REQUEST, (pdus, requestContext) -> {
JsonObject responseData = new JsonObject();
pduService.processPdu(pdu).forEach((oid, value) -> {
requestInfo.getResponseMappings().stream()
pduService.processPdus(pdus).forEach((oid, value) -> {
requestContext.getResponseMappings().stream()
.filter(snmpMapping -> snmpMapping.getOid().equals(oid.toDottedString()))
.findFirst()
.ifPresent(snmpMapping -> {
@ -269,8 +374,8 @@ public class SnmpTransportService implements TbTransportService {
return responseData;
});
ResponseDataMapper defaultResponseDataMapper = (pdu, requestInfo) -> {
return pduService.processPdu(pdu, requestInfo.getResponseMappings());
ResponseDataMapper defaultResponseDataMapper = (pdus, requestContext) -> {
return pduService.processPdus(pdus, requestContext.getResponseMappings());
};
Arrays.stream(SnmpCommunicationSpec.values())
.forEach(communicationSpec -> {
@ -279,34 +384,39 @@ public class SnmpTransportService implements TbTransportService {
}
private void configureResponseProcessors() {
responseProcessors.put(SnmpCommunicationSpec.TELEMETRY_QUERYING, (responseData, requestInfo, sessionContext) -> {
responseProcessors.put(SnmpCommunicationSpec.TELEMETRY_QUERYING, (responseData, requestContext, sessionContext) -> {
TransportProtos.PostTelemetryMsg postTelemetryMsg = JsonConverter.convertToTelemetryProto(responseData);
transportService.process(sessionContext.getSessionInfo(), postTelemetryMsg, null);
log.debug("Posted telemetry for SNMP device {}: {}", sessionContext.getDeviceId(), responseData);
});
responseProcessors.put(SnmpCommunicationSpec.CLIENT_ATTRIBUTES_QUERYING, (responseData, requestInfo, sessionContext) -> {
responseProcessors.put(SnmpCommunicationSpec.CLIENT_ATTRIBUTES_QUERYING, (responseData, requestContext, sessionContext) -> {
TransportProtos.PostAttributeMsg postAttributesMsg = JsonConverter.convertToAttributesProto(responseData);
transportService.process(sessionContext.getSessionInfo(), postAttributesMsg, null);
log.debug("Posted attributes for SNMP device {}: {}", sessionContext.getDeviceId(), responseData);
});
responseProcessors.put(SnmpCommunicationSpec.TO_DEVICE_RPC_REQUEST, (responseData, requestInfo, sessionContext) -> {
responseProcessors.put(SnmpCommunicationSpec.TO_DEVICE_RPC_REQUEST, (responseData, requestContext, sessionContext) -> {
TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder()
.setRequestId(requestInfo.getRequestId())
.setRequestId(requestContext.getRequestId())
.setPayload(JsonConverter.toJson(responseData))
.build();
transportService.process(sessionContext.getSessionInfo(), rpcResponseMsg, null);
log.debug("Posted RPC response {} for device {}", responseData, sessionContext.getDeviceId());
});
responseProcessors.put(SnmpCommunicationSpec.TO_SERVER_RPC_REQUEST, (responseData, requestContext, sessionContext) -> {
TransportProtos.ToServerRpcRequestMsg toServerRpcRequestMsg = TransportProtos.ToServerRpcRequestMsg.newBuilder()
.setRequestId(0)
.setMethodName(requestContext.getMethod().name())
.setParams(JsonConverter.toJson(responseData))
.build();
transportService.process(sessionContext.getSessionInfo(), toServerRpcRequestMsg, null);
});
}
private void reportActivity(TransportProtos.SessionInfoProto sessionInfo) {
transportService.process(sessionInfo, TransportProtos.SubscriptionInfoProto.newBuilder()
.setAttributeSubscription(true)
.setRpcSubscription(true)
.setLastActivityTime(System.currentTimeMillis())
.build(), TransportServiceCallback.EMPTY);
transportService.reportActivity(sessionInfo);
}
@ -335,29 +445,34 @@ public class SnmpTransportService implements TbTransportService {
}
@Data
private static class RequestInfo {
private Integer requestId;
private SnmpCommunicationSpec communicationSpec;
private List<SnmpMapping> responseMappings;
private static class RequestContext {
private final Integer requestId;
private final SnmpCommunicationSpec communicationSpec;
private final SnmpMethod method;
private final List<SnmpMapping> responseMappings;
public RequestInfo(Integer requestId, SnmpCommunicationSpec communicationSpec, List<SnmpMapping> responseMappings) {
this.requestId = requestId;
this.communicationSpec = communicationSpec;
this.responseMappings = responseMappings;
}
private final int requestSize;
private List<PDU> responseParts;
public RequestInfo(SnmpCommunicationSpec communicationSpec, List<SnmpMapping> responseMappings) {
@Builder
public RequestContext(Integer requestId, SnmpCommunicationSpec communicationSpec, SnmpMethod method, List<SnmpMapping> responseMappings, int requestSize) {
this.requestId = requestId;
this.communicationSpec = communicationSpec;
this.method = method;
this.responseMappings = responseMappings;
this.requestSize = requestSize;
if (requestSize > 1) {
this.responseParts = Collections.synchronizedList(new ArrayList<>());
}
}
}
private interface ResponseDataMapper {
JsonObject map(PDU pdu, RequestInfo requestInfo);
JsonObject map(List<PDU> pdus, RequestContext requestContext);
}
private interface ResponseProcessor {
void process(JsonObject responseData, RequestInfo requestInfo, DeviceSessionContext sessionContext);
void process(JsonObject responseData, RequestContext requestContext, DeviceSessionContext sessionContext);
}
}

31
common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/DeviceSessionContext.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.transport.snmp.session;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@ -26,9 +27,12 @@ import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.SnmpDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.common.data.transport.snmp.SnmpCommunicationSpec;
import org.thingsboard.server.common.transport.SessionMsgListener;
import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.common.transport.service.DefaultTransportService;
import org.thingsboard.server.common.transport.session.DeviceAwareSessionContext;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg;
@ -57,17 +61,22 @@ public class DeviceSessionContext extends DeviceAwareSessionContext implements S
private SnmpDeviceTransportConfiguration deviceTransportConfiguration;
@Getter
private final Device device;
@Getter
private final TenantId tenantId;
private final SnmpTransportContext snmpTransportContext;
private final AtomicInteger msgIdSeq = new AtomicInteger(0);
@Getter
private boolean isActive = true;
@Setter
private Runnable sessionTimeoutHandler;
@Getter
private final List<ScheduledFuture<?>> queryingTasks = new LinkedList<>();
public DeviceSessionContext(Device device, DeviceProfile deviceProfile, String token,
@Builder
public DeviceSessionContext(TenantId tenantId, Device device, DeviceProfile deviceProfile, String token,
SnmpDeviceProfileTransportConfiguration profileTransportConfiguration,
SnmpDeviceTransportConfiguration deviceTransportConfiguration,
SnmpTransportContext snmpTransportContext) throws Exception {
@ -75,6 +84,7 @@ public class DeviceSessionContext extends DeviceAwareSessionContext implements S
super.setDeviceId(device.getId());
super.setDeviceProfile(deviceProfile);
this.device = device;
this.tenantId = tenantId;
this.token = token;
this.snmpTransportContext = snmpTransportContext;
@ -131,19 +141,32 @@ public class DeviceSessionContext extends DeviceAwareSessionContext implements S
@Override
public void onAttributeUpdate(UUID sessionId, AttributeUpdateNotificationMsg attributeUpdateNotification) {
log.trace("[{}] Received attributes update notification to device", sessionId);
snmpTransportContext.getSnmpTransportService().onAttributeUpdate(this, attributeUpdateNotification);
try {
snmpTransportContext.getSnmpTransportService().onAttributeUpdate(this, attributeUpdateNotification);
} catch (Exception e) {
snmpTransportContext.getTransportService().errorEvent(getTenantId(), getDeviceId(), SnmpCommunicationSpec.SHARED_ATTRIBUTES_SETTING.getLabel(), e);
}
}
@Override
public void onRemoteSessionCloseCommand(UUID sessionId, SessionCloseNotificationProto sessionCloseNotification) {
log.trace("[{}] Received the remote command to close the session: {}", sessionId, sessionCloseNotification.getMessage());
if (sessionCloseNotification.getMessage().equals(DefaultTransportService.SESSION_EXPIRED_MESSAGE)) {
if (sessionTimeoutHandler != null) {
sessionTimeoutHandler.run();
}
}
}
@Override
public void onToDeviceRpcRequest(UUID sessionId, ToDeviceRpcRequestMsg toDeviceRequest) {
log.trace("[{}] Received RPC command to device", sessionId);
snmpTransportContext.getSnmpTransportService().onToDeviceRpcRequest(this, toDeviceRequest);
snmpTransportContext.getTransportService().process(getSessionInfo(), toDeviceRequest, RpcStatus.DELIVERED, TransportServiceCallback.EMPTY);
try {
snmpTransportContext.getSnmpTransportService().onToDeviceRpcRequest(this, toDeviceRequest);
snmpTransportContext.getTransportService().process(getSessionInfo(), toDeviceRequest, RpcStatus.DELIVERED, TransportServiceCallback.EMPTY);
} catch (Exception e) {
snmpTransportContext.getTransportService().errorEvent(getTenantId(), getDeviceId(), SnmpCommunicationSpec.TO_DEVICE_RPC_REQUEST.getLabel(), e);
}
}
@Override

23
common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpDeviceSimulatorV2.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.transport.snmp;
import org.snmp4j.CommandResponderEvent;
import org.snmp4j.CommunityTarget;
import org.snmp4j.PDU;
import org.snmp4j.Snmp;
@ -35,7 +34,6 @@ import org.snmp4j.agent.mo.snmp.SnmpTargetMIB;
import org.snmp4j.agent.mo.snmp.StorageType;
import org.snmp4j.agent.mo.snmp.VacmMIB;
import org.snmp4j.agent.security.MutableVACM;
import org.snmp4j.mp.MPv3;
import org.snmp4j.mp.SnmpConstants;
import org.snmp4j.security.SecurityLevel;
import org.snmp4j.security.SecurityModel;
@ -53,27 +51,11 @@ import org.snmp4j.transport.TransportMappings;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@SuppressWarnings("deprecation")
public class SnmpDeviceSimulatorV2 extends BaseAgent {
public static class RequestProcessor extends CommandProcessor {
private final Consumer<CommandResponderEvent> processor;
public RequestProcessor(Consumer<CommandResponderEvent> processor) {
super(new OctetString(MPv3.createLocalEngineID()));
this.processor = processor;
}
@Override
public void processPdu(CommandResponderEvent event) {
processor.accept(event);
}
}
private final Target target;
private final Address address;
private Snmp snmp;
@ -81,10 +63,7 @@ public class SnmpDeviceSimulatorV2 extends BaseAgent {
private final String password;
public SnmpDeviceSimulatorV2(int port, String password) throws IOException {
super(new File("conf.agent"), new File("bootCounter.agent"), new RequestProcessor(event -> {
System.out.println("aboba");
((Snmp) event.getSource()).cancel(event.getPDU(), event1 -> System.out.println("canceled"));
}));
super(new File("conf.agent"), new File("bootCounter.agent"), new CommandProcessor(new OctetString("12312")));
CommunityTarget target = new CommunityTarget();
target.setCommunity(new OctetString(password));
this.address = GenericAddress.parse("udp:0.0.0.0/" + port);

43
common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpTestV2.java

@ -16,34 +16,41 @@
package org.thingsboard.server.transport.snmp;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
public class SnmpTestV2 {
public static void main(String[] args) throws IOException {
SnmpDeviceSimulatorV2 device = new SnmpDeviceSimulatorV2(1610, "public");
device.start();
device.setUpMappings(Map.of(
".1.3.6.1.2.1.1.1.50", "12",
".1.3.6.1.2.1.2.1.52", "56",
".1.3.6.1.2.1.3.1.54", "yes",
".1.3.6.1.2.1.7.1.58", ""
));
private static final Scanner scanner = new Scanner(System.in);
public static void main(String[] args) throws IOException {
SnmpDeviceSimulatorV2 client = new SnmpDeviceSimulatorV2(1610, "public");
// while (true) {
// new Scanner(System.in).nextLine();
// device.sendTrap("127.0.0.1", 1062, Map.of(".1.3.6.1.2.87.1.56", "12"));
// System.out.println("sent");
client.start();
Map<String, String> mappings = new HashMap<>();
// for (int i = 1; i <= 500; i++) {
// String oid = String.format(".1.3.6.1.2.1.%s.1.52", i);
// mappings.put(oid, "value_" + i);
// }
mappings.put("1.3.6.1.2.1.266.1.52", "****");
// Snmp snmp = new Snmp(device.transportMappings[0]);
// device.snmp.addCommandResponder(event -> {
// System.out.println(event);
// });
client.setUpMappings(mappings);
inputTraps(client);
scanner.nextLine();
}
new Scanner(System.in).nextLine();
private static void inputTraps(SnmpDeviceSimulatorV2 client) throws IOException {
while (true) {
String data = scanner.nextLine();
if (!data.isEmpty()) {
client.sendTrap("127.0.0.1", 1620, Map.of(
"1.3.6.1.2.1.266.1.52", data + " (266)",
"1.3.6.1.2.1.267.1.52", data + " (267)"
));
}
}
}
}

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

@ -17,6 +17,9 @@ package org.thingsboard.server.common.transport;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.rpc.RpcStatus;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse;
@ -144,6 +147,10 @@ public interface TransportService {
void reportActivity(SessionInfoProto sessionInfo);
void lifecycleEvent(TenantId tenantId, DeviceId deviceId, ComponentLifecycleEvent eventType, boolean success, Throwable error);
void errorEvent(TenantId tenantId, DeviceId deviceId, String method, Throwable error);
void deregisterSession(SessionInfoProto sessionInfo);
void log(SessionInfoProto sessionInfo, String msg);

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

@ -22,6 +22,7 @@ import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.protobuf.ByteString;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
@ -49,6 +50,8 @@ import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.notification.rule.trigger.RateLimitsTrigger;
import org.thingsboard.server.common.data.rpc.RpcStatus;
@ -827,6 +830,37 @@ public class DefaultTransportService implements TransportService {
sessionsToRemove.forEach(sessionsActivity::remove);
}
@Override
public void lifecycleEvent(TenantId tenantId, DeviceId deviceId, ComponentLifecycleEvent eventType, boolean success, Throwable error) {
ToCoreMsg msg = ToCoreMsg.newBuilder()
.setLifecycleEventMsg(TransportProtos.LifecycleEventProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setEntityIdMSB(deviceId.getId().getMostSignificantBits())
.setEntityIdLSB(deviceId.getId().getLeastSignificantBits())
.setServiceId(serviceInfoProvider.getServiceId())
.setLcEventType(eventType.name())
.setSuccess(success)
.setError(error != null ? ExceptionUtils.getStackTrace(error) : ""))
.build();
sendToCore(tenantId, deviceId, msg, deviceId.getId(), TransportServiceCallback.EMPTY);
}
@Override
public void errorEvent(TenantId tenantId, DeviceId deviceId, String method, Throwable error) {
ToCoreMsg msg = ToCoreMsg.newBuilder()
.setErrorEventMsg(TransportProtos.ErrorEventProto.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setEntityIdMSB(deviceId.getId().getMostSignificantBits())
.setEntityIdLSB(deviceId.getId().getLeastSignificantBits())
.setServiceId(serviceInfoProvider.getServiceId())
.setMethod(method)
.setError(ExceptionUtils.getStackTrace(error)))
.build();
sendToCore(tenantId, deviceId, msg, deviceId.getId(), TransportServiceCallback.EMPTY);
}
@Override
public SessionMetaData registerSyncSession(TransportProtos.SessionInfoProto sessionInfo, SessionMsgListener listener, long timeout) {
SessionMetaData currentSession = new SessionMetaData(sessionInfo, TransportProtos.SessionType.SYNC, listener);
@ -1118,18 +1152,21 @@ public class DefaultTransportService implements TransportService {
}
protected void sendToDeviceActor(TransportProtos.SessionInfoProto sessionInfo, TransportToDeviceActorMsg toDeviceActorMsg, TransportServiceCallback<Void> callback) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, getTenantId(sessionInfo), getDeviceId(sessionInfo));
ToCoreMsg toCoreMsg = ToCoreMsg.newBuilder().setToDeviceActorMsg(toDeviceActorMsg).build();
sendToCore(getTenantId(sessionInfo), getDeviceId(sessionInfo), toCoreMsg, getRoutingKey(sessionInfo), callback);
}
private void sendToCore(TenantId tenantId, EntityId entityId, ToCoreMsg msg, UUID routingKey, TransportServiceCallback<Void> callback) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId);
if (log.isTraceEnabled()) {
log.trace("[{}][{}] Pushing to topic {} message {}", getTenantId(sessionInfo), getDeviceId(sessionInfo), tpi.getFullTopicName(), toDeviceActorMsg);
log.trace("[{}][{}] Pushing to topic {} message {}", tenantId, entityId, tpi.getFullTopicName(), msg);
}
TransportTbQueueCallback transportTbQueueCallback = callback != null ?
new TransportTbQueueCallback(callback) : null;
tbCoreProducerStats.incrementTotal();
StatsCallback wrappedCallback = new StatsCallback(transportTbQueueCallback, tbCoreProducerStats);
tbCoreMsgProducer.send(tpi,
new TbProtoQueueMsg<>(getRoutingKey(sessionInfo),
ToCoreMsg.newBuilder().setToDeviceActorMsg(toDeviceActorMsg).build()),
wrappedCallback);
tbCoreMsgProducer.send(tpi, new TbProtoQueueMsg<>(routingKey, msg), wrappedCallback);
}
private void sendToRuleEngine(TenantId tenantId, TbMsg tbMsg, TbQueueCallback callback) {

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

@ -67,6 +67,8 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
public static final String INCORRECT_TENANT_ID = "Incorrect tenantId ";
public static final String INCORRECT_DEVICE_ID = "Incorrect deviceId ";
public static final String DEFAULT_DEVICE_TELEMETRY_TOPIC = "v1/devices/me/telemetry";
public static final String HTTP_DEFAULT_PORT = "80";
public static final String HTTPS_DEFAULT_PORT = "443";
private final Map<String, Resource> certs = new ConcurrentHashMap<>();
@ -203,8 +205,9 @@ public class DeviceConnectivityServiceImpl implements DeviceConnectivityService
return null;
}
String hostName = getHost(baseUrl, properties);
String port = properties.getPort().isEmpty() ? "" : ":" + properties.getPort();
String propertiesPort = properties.getPort();
String port = (propertiesPort.isEmpty() || HTTP_DEFAULT_PORT.equals(propertiesPort) || HTTPS_DEFAULT_PORT.equals(propertiesPort))
? "" : ":" + propertiesPort;
return DeviceConnectivityUtil.getHttpPublishCommand(protocol, hostName, port, deviceCredentials);
}

23
dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java

@ -31,7 +31,6 @@ import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.util.ReflectionUtils;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.rule.RuleChainDao;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.ConstraintValidator;
import org.thingsboard.server.dao.service.DataValidator;
@ -41,15 +40,14 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@Component
@Slf4j
public class RuleChainDataValidator extends DataValidator<RuleChain> {
@Autowired
private RuleChainDao ruleChainDao;
@Autowired
@Lazy
private RuleChainService ruleChainService;
@ -88,15 +86,19 @@ public class RuleChainDataValidator extends DataValidator<RuleChain> {
}
}
public static void validateMetaData(RuleChainMetaData ruleChainMetaData) {
public static List<Throwable> validateMetaData(RuleChainMetaData ruleChainMetaData) {
ConstraintValidator.validateFields(ruleChainMetaData);
ruleChainMetaData.getNodes().forEach(RuleChainDataValidator::validateRuleNode);
List<Throwable> throwables = ruleChainMetaData.getNodes().stream()
.map(RuleChainDataValidator::validateRuleNode)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(ruleChainMetaData.getConnections())) {
validateCircles(ruleChainMetaData.getConnections());
}
return throwables;
}
public static void validateRuleNode(RuleNode ruleNode) {
public static Throwable validateRuleNode(RuleNode ruleNode) {
String errorPrefix = "'" + ruleNode.getName() + "' node configuration is invalid: ";
ConstraintValidator.validateFields(ruleNode, errorPrefix);
Object nodeConfig;
@ -104,11 +106,12 @@ public class RuleChainDataValidator extends DataValidator<RuleChain> {
Class<Object> nodeConfigType = ReflectionUtils.getAnnotationProperty(ruleNode.getType(),
"org.thingsboard.rule.engine.api.RuleNode", "configClazz");
nodeConfig = JacksonUtil.treeToValue(ruleNode.getConfiguration(), nodeConfigType);
} catch (Exception e) {
log.warn("Failed to validate node configuration: {}", ExceptionUtils.getRootCauseMessage(e));
return;
} catch (Throwable t) {
log.warn("Failed to validate node configuration: {}", ExceptionUtils.getRootCauseMessage(t));
return t;
}
ConstraintValidator.validateFields(nodeConfig, errorPrefix);
return null;
}
private static void validateCircles(List<NodeConnectionInfo> connectionInfos) {

3
dao/src/test/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidatorTest.java

@ -23,7 +23,6 @@ import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.dao.rule.RuleChainDao;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService;
@ -35,8 +34,6 @@ import static org.mockito.Mockito.verify;
@SpringBootTest(classes = RuleChainDataValidator.class)
class RuleChainDataValidatorTest {
@MockBean
RuleChainDao ruleChainDao;
@MockBean
RuleChainService ruleChainService;
@MockBean

2
docker/docker-compose.yml

@ -235,6 +235,8 @@ services:
tb-snmp-transport:
restart: always
image: "${DOCKER_REPO}/${SNMP_TRANSPORT_DOCKER_NAME}:${TB_VERSION}"
ports:
- "1620:1620/udp"
environment:
TB_SERVICE_ID: tb-snmp-transport
JAVA_OPTS: "${JAVA_OPTS}"

1
docker/tb-snmp-transport.env

@ -1,6 +1,7 @@
ZOOKEEPER_ENABLED=true
ZOOKEEPER_URL=zookeeper:2181
SNMP_BIND_PORT=1620
METRICS_ENABLED=true
METRICS_ENDPOINTS_EXPOSE=prometheus
WEB_APPLICATION_ENABLE=true

4
transport/snmp/src/main/resources/tb-snmp-transport.yml

@ -99,11 +99,15 @@ redis:
transport:
snmp:
enabled: "${SNMP_ENABLED:true}"
bind_port: "${SNMP_BIND_PORT:1620}"
response_processing:
# parallelism level for executor (workStealingPool) that is responsible for handling responses from SNMP devices
parallelism_level: "${SNMP_RESPONSE_PROCESSING_PARALLELISM_LEVEL:20}"
# to configure SNMP to work over UDP or TCP
underlying_protocol: "${SNMP_UNDERLYING_PROTOCOL:udp}"
max_request_oids: "${SNMP_MAX_REQUEST_OIDS:100}"
response:
ignore_type_cast_errors: "${SNMP_RESPONSE_IGNORE_TYPE_CAST_ERRORS:false}"
sessions:
inactivity_timeout: "${TB_TRANSPORT_SESSIONS_INACTIVITY_TIMEOUT:300000}"
report_timeout: "${TB_TRANSPORT_SESSIONS_REPORT_TIMEOUT:3000}"

13
ui-ngx/src/app/core/translate/missing-translate-handler.ts

@ -21,7 +21,18 @@ export class TbMissingTranslationHandler implements MissingTranslationHandler {
handle(params: MissingTranslationHandlerParams) {
if (params.key && !params.key.startsWith(customTranslationsPrefix)) {
console.warn('Translation for \'' + params.key + '\' doesn\'t exist');
params.translateService.set(params.key, params.key);
let translations: any;
const parts = params.key.split('.');
for (let i=parts.length-1; i>=0; i--) {
const newTranslations = {};
if (i === parts.length-1) {
newTranslations[parts[i]] = params.key;
} else {
newTranslations[parts[i]] = translations;
}
translations = newTranslations;
}
params.translateService.setTranslation(params.translateService.currentLang, translations, true);
}
}
}

8
ui-ngx/src/app/modules/home/components/import-export/import-export.models.ts

@ -65,6 +65,10 @@ export enum ImportEntityColumnType {
lwm2mServerSecurityMode = 'LWM2M_SERVER_SECURITY_MODE',
lwm2mServerClientPublicKeyOrId = 'LWM2M_SERVER_CLIENT_PUBLIC_KEY_OR_ID',
lwm2mServerClientSecretKey = 'LWM2M_SERVER_CLIENT_SECRET_KEY',
snmpHost = 'SNMP_HOST',
snmpPort = 'SNMP_PORT',
snmpVersion = 'SNMP_VERSION',
snmpCommunityString = 'SNMP_COMMUNITY_STRING',
isGateway = 'IS_GATEWAY',
description = 'DESCRIPTION',
routingKey = 'ROUTING_KEY',
@ -96,6 +100,10 @@ export const importEntityColumnTypeTranslations = new Map<ImportEntityColumnType
[ImportEntityColumnType.lwm2mServerSecurityMode, 'import.column-type.lwm2m.lwm2m-server-security-mode'],
[ImportEntityColumnType.lwm2mServerClientPublicKeyOrId, 'import.column-type.lwm2m.lwm2m-server-public-key-id'],
[ImportEntityColumnType.lwm2mServerClientSecretKey, 'import.column-type.lwm2m.lwm2m-server-secret-key'],
[ImportEntityColumnType.snmpHost, 'import.column-type.snmp.host'],
[ImportEntityColumnType.snmpPort, 'import.column-type.snmp.port'],
[ImportEntityColumnType.snmpVersion, 'import.column-type.snmp.version'],
[ImportEntityColumnType.snmpCommunityString, 'import.column-type.snmp.community-string'],
[ImportEntityColumnType.isGateway, 'import.column-type.isgateway'],
[ImportEntityColumnType.description, 'import.column-type.description'],
[ImportEntityColumnType.routingKey, 'import.column-type.routing-key'],

4
ui-ngx/src/app/modules/home/components/import-export/table-columns-assignment.component.ts

@ -105,6 +105,10 @@ export class TableColumnsAssignmentComponent implements OnInit, ControlValueAcce
{ value: ImportEntityColumnType.lwm2mServerSecurityMode },
{ value: ImportEntityColumnType.lwm2mServerClientPublicKeyOrId },
{ value: ImportEntityColumnType.lwm2mServerClientSecretKey },
{ value: ImportEntityColumnType.snmpHost },
{ value: ImportEntityColumnType.snmpPort },
{ value: ImportEntityColumnType.snmpVersion },
{ value: ImportEntityColumnType.snmpCommunityString },
);
break;
case EntityType.ASSET:

330
ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.html

@ -23,174 +23,164 @@
<fieldset *ngFor="let source of sources" [ngClass]="{'fields-group': settings.showGroupTitle}">
<legend class="group-title" *ngIf="settings.showGroupTitle">{{ getGroupTitle(source.datasource) }}
</legend>
<div class="tb-multiple-input-layout layout-wrap"
[ngClass]="{'vertical-alignment': isVerticalAlignment || changeAlignment}">
<div *ngFor="let key of visibleKeys(source)"
[ngStyle]="{width: (isVerticalAlignment || changeAlignment) ? '100%' : inputWidthSettings}">
<div class="input-field" *ngIf="key.settings.dataKeyValueType === 'string'">
<mat-form-field class="mat-block" appearance="outline">
<mat-label>{{key.label}}</mat-label>
<input matInput
formControlName="{{key.formId}}"
[required]="key.settings.required"
[readonly]="key.settings.isEditable === 'readonly'"
type="text"
(focus)="key.isFocused = true; focusInputElement($event)"
(blur)="key.isFocused = false; inputChanged(source, key)">
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matPrefix>
<tb-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</tb-icon>
<ng-template #customToggleIcon>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon">
</ng-template>
</ng-container>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
{{ getErrorMessageText(key.settings, 'required') }}
</mat-error>
</mat-form-field>
</div>
<div class="input-field" *ngIf="key.settings.dataKeyValueType === 'double' ||
key.settings.dataKeyValueType === 'integer'">
<mat-form-field class="mat-block" appearance="outline">
<mat-label>{{key.label}}</mat-label>
<input matInput
formControlName="{{key.formId}}"
[required]="key.settings.required"
[readonly]="key.settings.isEditable === 'readonly'"
type="number"
step="{{key.settings.step}}"
min="{{key.settings.minValue}}"
max="{{key.settings.maxValue}}"
(focus)="key.isFocused = true; focusInputElement($event)"
(blur)="key.isFocused = false; inputChanged(source, key)">
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matPrefix>
<tb-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</tb-icon>
<ng-template #customToggleIcon>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon">
</ng-template>
</ng-container>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
{{ getErrorMessageText(key.settings,'required') }}
</mat-error>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('min')">
{{ getErrorMessageText(key.settings,'min') }}
</mat-error>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('max')">
{{ getErrorMessageText(key.settings,'max') }}
</mat-error>
</mat-form-field>
</div>
<div class="input-field mat-block" *ngIf="key.settings.dataKeyValueType === 'booleanCheckbox'">
<mat-checkbox formControlName="{{key.formId}}"
(change)="inputChanged(source, key)">
<span class="label-wrapper">{{key.label}}</span>
</mat-checkbox>
</div>
<div class="input-field" *ngIf="key.settings.dataKeyValueType === 'JSON'">
<mat-form-field class="mat-block" appearance="outline">
<mat-label>{{key.label}}</mat-label>
<input
matInput
type="text"
formControlName="{{key.formId}}"
tb-json-to-string
[readonly]="key.settings.isEditable === 'readonly'"
[required]="key.settings.required"
(focus)="key.isFocused = true; focusInputElement($event)"
(blur)="key.isFocused = false; inputChanged(source, key)"
/>
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matPrefix>
<tb-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</tb-icon>
<ng-template #customToggleIcon>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon">
</ng-template>
</ng-container>
<button [disabled]="key.settings.isEditable === 'disabled' || key.settings.isEditable === 'readonly'"
type="button"
matSuffix mat-icon-button
(click)="openEditJSONDialog($event, key, source)">
<mat-icon>open_in_new</mat-icon>
</button>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
{{ getErrorMessageText(key.settings,'required') }}
</mat-error>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('invalidJSON')">
{{ getErrorMessageText(key.settings,'invalidJSON') | translate }}
</mat-error>
</mat-form-field>
</div>
<div class="input-field mat-block" *ngIf="key.settings.dataKeyValueType === 'booleanSwitch'">
<mat-slide-toggle formControlName="{{key.formId}}"
[labelPosition]="key.settings.slideToggleLabelPosition"
(change)="inputChanged(source, key)">
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon">
<tb-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</tb-icon>
<ng-template #customToggleIcon>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon">
</ng-template>
</ng-container>
<span class="label-wrapper">{{key.label}}</span>
</mat-slide-toggle>
</div>
<div class="input-field mat-block date-time-input" *ngIf="(key.settings.dataKeyValueType === 'dateTime') ||
(key.settings.dataKeyValueType === 'date') ||
(key.settings.dataKeyValueType === 'time')" fxLayout="column">
<mat-form-field appearance="outline">
<mat-label>{{key.label}}</mat-label>
<mat-datetimepicker-toggle [for]="datePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #datePicker type="{{datePickerType(key.settings.dataKeyValueType)}}"
openOnFocus="true"></mat-datetimepicker>
<input matInput formControlName="{{key.formId}}"
[required]="key.settings.required"
[readonly]="key.settings.isEditable === 'readonly'"
[matDatetimepicker]="datePicker"
(focus)="key.isFocused = true;"
(blur)="key.isFocused = false;"
(dateChange)="inputChanged(source, key)">
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
{{ getErrorMessageText(key.settings, 'required') }}
</mat-error>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('matDatepickerParse')">
{{ getErrorMessageText(key.settings, 'invalidDate') }}
</mat-error>
</mat-form-field>
</div>
<div class="input-field" *ngIf="key.settings.dataKeyValueType === 'select'">
<mat-form-field class="mat-block" appearance="outline">
<mat-label>{{key.label}}</mat-label>
<mat-select formControlName="{{key.formId}}"
[required]="key.settings.required"
(focus)="key.isFocused = true;"
(selectionChange)="key.isFocused = false; inputChanged(source, key)">
<mat-option *ngFor="let option of key.settings.selectOptions"
[value]="option.value"
[disabled]="key.settings.isEditable === 'readonly'">
{{ getCustomTranslationText(option.label ? option.label : option.value) }}
</mat-option>
</mat-select>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
{{ getErrorMessageText(key.settings, 'required') }}
</mat-error>
</mat-form-field>
</div>
<div class="tb-form-row space-between color-picker-input" *ngIf="key.settings.dataKeyValueType === 'color'">
<div class="label-container">
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon">
<mat-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</mat-icon>
<ng-template #customToggleIcon>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon">
</ng-template>
</ng-container>
{{key.label}}
</div>
<tb-color-input asBoxInput
[required]="key.settings.required"
[requiredText]="getErrorMessageText(key.settings, 'required')"
openOnInput
formControlName="{{key.formId}}">
<div class="tb-multiple-input-layout"
[style]="{'grid-template-columns': 'repeat(' + columns + ', 1fr)', 'column-gap': settings.columnGap + 'px', 'row-gap': settings.rowGap + 'px'}">
<ng-container *ngFor="let key of visibleKeys(source)">
<mat-form-field *ngIf="key.settings.dataKeyValueType === 'string'"
[appearance]="key.settings.appearance" [subscriptSizing]="key.settings.subscriptSizing">
<mat-label>{{key.label}}</mat-label>
<input matInput
formControlName="{{key.formId}}"
[required]="key.settings.required"
[readonly]="key.settings.isEditable === 'readonly'"
type="text"
(focus)="key.isFocused = true; focusInputElement($event)"
(blur)="key.isFocused = false; inputChanged(source, key)">
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matIconPrefix>
<ng-container *ngTemplateOutlet="iconPrefix; context: {key: key}"></ng-container>
</ng-container>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
{{ getErrorMessageText(key.settings, 'required') }}
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="['double', 'integer'].includes(key.settings.dataKeyValueType)"
[appearance]="key.settings.appearance" [subscriptSizing]="key.settings.subscriptSizing">
<mat-label>{{key.label}}</mat-label>
<input matInput
formControlName="{{key.formId}}"
[required]="key.settings.required"
[readonly]="key.settings.isEditable === 'readonly'"
type="number"
step="{{key.settings.step}}"
min="{{key.settings.minValue}}"
max="{{key.settings.maxValue}}"
(focus)="key.isFocused = true; focusInputElement($event)"
(blur)="key.isFocused = false; inputChanged(source, key)">
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matIconPrefix>
<ng-container *ngTemplateOutlet="iconPrefix; context: {key: key}"></ng-container>
</ng-container>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
{{ getErrorMessageText(key.settings,'required') }}
</mat-error>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('min')">
{{ getErrorMessageText(key.settings,'min') }}
</mat-error>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('max')">
{{ getErrorMessageText(key.settings,'max') }}
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="key.settings.dataKeyValueType === 'JSON'"
[appearance]="key.settings.appearance" [subscriptSizing]="key.settings.subscriptSizing">
<mat-label>{{key.label}}</mat-label>
<input
matInput
type="text"
formControlName="{{key.formId}}"
tb-json-to-string
[readonly]="key.settings.isEditable === 'readonly'"
[required]="key.settings.required"
(focus)="key.isFocused = true; focusInputElement($event)"
(blur)="key.isFocused = false; inputChanged(source, key)"
/>
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matIconPrefix>
<ng-container *ngTemplateOutlet="iconPrefix; context: {key: key}"></ng-container>
</ng-container>
<button [disabled]="key.settings.isEditable === 'disabled' || key.settings.isEditable === 'readonly'"
type="button"
matSuffix mat-icon-button
(click)="openEditJSONDialog($event, key, source)">
<mat-icon>open_in_new</mat-icon>
</button>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
{{ getErrorMessageText(key.settings,'required') }}
</mat-error>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('invalidJSON')">
{{ getErrorMessageText(key.settings,'invalidJSON') | translate }}
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="['dateTime','date', 'time'].includes(key.settings.dataKeyValueType)"
[appearance]="key.settings.appearance" [subscriptSizing]="key.settings.subscriptSizing">
<mat-label>{{key.label}}</mat-label>
<mat-datetimepicker-toggle [for]="datePicker" matPrefix></mat-datetimepicker-toggle>
<mat-datetimepicker #datePicker type="{{datePickerType(key.settings.dataKeyValueType)}}"
openOnFocus="true"></mat-datetimepicker>
<input matInput formControlName="{{key.formId}}"
[required]="key.settings.required"
[readonly]="key.settings.isEditable === 'readonly'"
[matDatetimepicker]="datePicker"
(focus)="key.isFocused = true;"
(blur)="key.isFocused = false;"
(dateChange)="inputChanged(source, key)">
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
{{ getErrorMessageText(key.settings, 'required') }}
</mat-error>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('matDatepickerParse')">
{{ getErrorMessageText(key.settings, 'invalidDate') }}
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="key.settings.dataKeyValueType === 'select'"
[appearance]="key.settings.appearance" [subscriptSizing]="key.settings.subscriptSizing">
<mat-label>{{key.label}}</mat-label>
<mat-select formControlName="{{key.formId}}"
[required]="key.settings.required"
(focus)="key.isFocused = true;"
(selectionChange)="key.isFocused = false; inputChanged(source, key)">
<mat-option *ngFor="let option of key.settings.selectOptions"
[value]="option.value"
[disabled]="key.settings.isEditable === 'readonly'">
{{ getCustomTranslationText(option.label ? option.label : option.value) }}
</mat-option>
</mat-select>
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matIconPrefix>
<ng-container *ngTemplateOutlet="iconPrefix; context: {key: key}"></ng-container>
</ng-container>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
{{ getErrorMessageText(key.settings, 'required') }}
</mat-error>
</mat-form-field>
<mat-form-field *ngIf="key.settings.dataKeyValueType === 'color'"
class="color-input" [appearance]="key.settings.appearance" [subscriptSizing]="key.settings.subscriptSizing"
(click)="colorInput.openColorPickerPopup($event)">
<mat-label>{{key.label}}</mat-label>
<input matInput
formControlName="{{key.formId}}"
[required]="key.settings.required"
[readonly]="key.settings.isEditable === 'readonly'"
type="text"
(keydown)="$event.preventDefault();">
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon" matIconPrefix>
<ng-container *ngTemplateOutlet="iconPrefix; context: {key: key}"></ng-container>
</ng-container>
<tb-color-input #colorInput asBoxInput matSuffix
colorClearButton
[disabled]="multipleInputFormGroup.get(key.formId).disabled"
[readonly]="key.settings.isEditable === 'readonly'"
[ngModel]="multipleInputFormGroup.get(key.formId).value"
(ngModelChange)="colorChanged(source, key, $event)"
[ngModelOptions]="{ standalone: true }">
</tb-color-input>
</div>
</div>
<mat-error *ngIf="multipleInputFormGroup.get(key.formId).hasError('required')">
{{ getErrorMessageText(key.settings, 'required') }}
</mat-error>
</mat-form-field>
<mat-checkbox *ngIf="key.settings.dataKeyValueType === 'booleanCheckbox'"
formControlName="{{key.formId}}"
(change)="inputChanged(source, key)">
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon">
<ng-container *ngTemplateOutlet="iconPrefix; context: {key: key}"></ng-container>
</ng-container>
<span class="label-wrapper">{{key.label}}</span>
</mat-checkbox>
<mat-slide-toggle *ngIf="key.settings.dataKeyValueType === 'booleanSwitch'"
formControlName="{{key.formId}}"
[labelPosition]="key.settings.slideToggleLabelPosition"
(change)="inputChanged(source, key)">
<ng-container *ngIf="key.settings.icon || key.settings.safeCustomIcon">
<ng-container *ngTemplateOutlet="iconPrefix; context: {key: key}"></ng-container>
</ng-container>
<span class="label-wrapper">{{key.label}}</span>
</mat-slide-toggle>
</ng-container>
</div>
</fieldset>
</div>
@ -216,3 +206,9 @@
</div>
</div>
</form>
<ng-template #iconPrefix let-key="key">
<tb-icon *ngIf="!key.settings.safeCustomIcon; else customToggleIcon">{{key.settings.icon}}</tb-icon>
<ng-template #customToggleIcon>
<img class="mat-icon" [src]="key.settings.safeCustomIcon" alt="icon">
</ng-template>
</ng-template>

72
ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss

@ -38,33 +38,7 @@
}
.tb-multiple-input-layout {
display: flex;
flex-direction: row;
align-items: start;
}
.color-picker-input {
padding: 7px 16px 7px 12px;
margin: 0 10px 22px 0;
border-color: rgba(0, 0, 0, 0.4);
.label-container {
display: flex;
flex-direction: row;
align-items: center;
}
.mat-icon, img {
margin-right: 5px;
}
}
.input-field {
padding-right: 10px;
mat-form-field {
margin-bottom: 5px;
}
display: grid;
}
.mat-mdc-slide-toggle {
@ -73,17 +47,6 @@
margin-bottom: 8px;
}
.date-time-input {
.mat-mdc-form-field {
width: 100%;
margin: 2px 0;
}
}
.vertical-alignment {
flex-direction: column;
}
&--buttons-container {
display: flex;
flex-direction: row;
@ -112,18 +75,33 @@
:host ::ng-deep {
.tb-multiple-input {
.mat-mdc-slide-toggle .mdc-form-field {
width: 100%;
& > label {
display: flex;
align-items: center;
.mat-mdc-slide-toggle, .mat-mdc-checkbox {
.mdc-form-field {
width: 100%;
margin: 0;
.mat-icon {
margin-left: 8px;
margin-right: 8px;
& > label {
display: flex;
align-items: center;
width: 100%;
margin: 0;
.mat-icon {
margin-right: 8px;
}
}
}
}
.mat-mdc-slide-toggle {
.mdc-form-field {
& > label {
.mat-icon {
margin-left: 8px;
}
}
}
}
.color-input {
.mat-mdc-form-field-icon-suffix {
padding: 0 8px 0 4px;
}
}
}
}

66
ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.ts

@ -48,6 +48,7 @@ import {
} from '@shared/components/dialog/json-object-edit-dialog.component';
import { MatDialog } from '@angular/material/dialog';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { MatFormFieldAppearance, SubscriptSizing } from '@angular/material/form-field';
type FieldAlignment = 'row' | 'column';
@ -55,7 +56,7 @@ type MultipleInputWidgetDataKeyType = 'server' | 'shared' | 'timeseries';
export type MultipleInputWidgetDataKeyValueType = 'string' | 'double' | 'integer' |
'JSON' | 'booleanCheckbox' | 'booleanSwitch' |
'dateTime' | 'date' | 'time' | 'select' | 'color';
type MultipleInputWidgetDataKeyEditableType = 'editable' | 'disabled' | 'readonly';
export type MultipleInputWidgetDataKeyEditableType = 'editable' | 'disabled' | 'readonly';
type ConvertGetValueFunction = (value: any, ctx: WidgetContext) => any;
type ConvertSetValueFunction = (value: any, originValue: any, ctx: WidgetContext) => any;
@ -71,6 +72,8 @@ interface MultipleInputWidgetSettings {
groupTitle: string;
fieldsAlignment: FieldAlignment;
fieldsInRow: number;
columnGap: number;
rowGap: number;
attributesShared?: boolean;
}
@ -88,6 +91,8 @@ interface MultipleInputWidgetDataKeySettings {
isEditable: MultipleInputWidgetDataKeyEditableType;
disabledOnDataKey: string;
dataKeyHidden: boolean;
appearance: MatFormFieldAppearance;
subscriptSizing: SubscriptSizing;
step?: number;
minValue?: number;
maxValue?: number;
@ -148,8 +153,7 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
private isSavingInProgress = false;
isVerticalAlignment: boolean;
inputWidthSettings: string;
changeAlignment: boolean;
columns: number;
saveButtonLabel: string;
resetButtonLabel: string;
@ -228,15 +232,15 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
if (isUndefined(this.settings.fieldsInRow)) {
this.settings.fieldsInRow = 2;
}
// For backward compatibility
this.isVerticalAlignment = !(this.settings.fieldsAlignment === 'row');
if (!this.isVerticalAlignment && this.settings.fieldsInRow) {
this.inputWidthSettings = 100 / this.settings.fieldsInRow + '%';
if (isUndefined(this.settings.columnGap)) {
this.settings.columnGap = 10;
}
if (isUndefined(this.settings.rowGap)) {
this.settings.rowGap = 5;
}
this.updateWidgetDisplaying();
this.updateColumns();
}
private updateDatasources() {
@ -284,6 +288,15 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
dataKey.settings.isEditable = 'editable';
}
}
if (isUndefined(dataKey.settings.appearance)) {
dataKey.settings.appearance = 'outline';
}
if (isUndefined(dataKey.settings.subscriptSizing)) {
dataKey.settings.subscriptSizing = 'fixed';
}
// For backward compatibility
if (dataKey.settings.dataKeyValueType === 'select') {
@ -387,12 +400,6 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
}
});
}
} else if (key.settings.dataKeyValueType === 'color') {
formControl.valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(() => {
this.inputChanged(source, key);
});
}
this.multipleInputFormGroup.addControl(key.formId, formControl);
}
@ -452,9 +459,8 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
}
if (key.settings.isEditable === 'editable' && key.settings.disabledOnDataKey) {
const conditions = data.filter((item) => {
return source.datasource === item.datasource && item.dataKey.name === key.settings.disabledOnDataKey;
});
const conditions = data.filter((item) =>
source.datasource === item.datasource && item.dataKey.name === key.settings.disabledOnDataKey);
if (conditions && conditions.length) {
if (conditions[0].data.length) {
if (conditions[0].data[0][1] === 'false') {
@ -494,8 +500,17 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
return data;
}
private updateWidgetDisplaying() {
this.changeAlignment = (this.ctx.$container && this.ctx.$container[0].offsetWidth < 620);
private updateColumns() {
const changeAlignment = (this.ctx.$container && this.ctx.$container[0].offsetWidth < 620);
if (changeAlignment) {
this.columns = 1;
} else {
if (!this.isVerticalAlignment && this.settings.fieldsInRow) {
this.columns = this.settings.fieldsInRow;
} else {
this.columns = 1;
}
}
}
public onDataUpdated() {
@ -504,7 +519,7 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
}
private resize() {
this.updateWidgetDisplaying();
this.updateColumns();
this.ctx.detectChanges();
}
@ -599,6 +614,13 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
}
}
public colorChanged(source: MultipleInputWidgetSource, key: MultipleInputWidgetDataKey, color: string) {
this.multipleInputFormGroup.get(key.formId).setValue(color);
this.multipleInputFormGroup.get(key.formId).markAsDirty();
this.multipleInputFormGroup.get(key.formId).markAsTouched();
this.inputChanged(source, key);
}
public saveForm() {
if (this.settings.showActionButtons) {
this.save();
@ -737,7 +759,7 @@ export class MultipleInputWidgetComponent extends PageComponent implements OnIni
this.multipleInputFormGroup.reset(undefined, {emitEvent: false});
this.sources.forEach((source) => {
for (const key of this.visibleKeys(source)) {
this.multipleInputFormGroup.get(key.formId).patchValue(key.value, {emitEvent: false});
this.multipleInputFormGroup.get(key.formId).patchValue(key.value);
}
});
this.multipleInputFormGroup.markAsPristine();

31
ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.html

@ -93,11 +93,36 @@
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field [fxShow]="updateMultipleAttributesKeySettingsForm.get('isEditable').value !== 'disabled'" fxFlex="50" class="mat-block">
<mat-form-field [fxShow]="updateMultipleAttributesKeySettingsForm.get('isEditable').value === 'editable'" fxFlex="50" class="mat-block">
<mat-label translate>widgets.input-widgets.disable-on-datakey-name</mat-label>
<input matInput formControlName="disabledOnDataKey">
</mat-form-field>
</section>
<section *ngIf="!['booleanSwitch', 'booleanCheckbox'].includes(updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value)"
fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex="50" class="mat-block">
<mat-label translate>widgets.input-widgets.field-appearance</mat-label>
<mat-select formControlName="appearance">
<mat-option [value]="'fill'">
{{ 'widgets.input-widgets.appearance-fill' | translate }}
</mat-option>
<mat-option [value]="'outline'">
{{ 'widgets.input-widgets.appearance-outline' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="50" class="mat-block">
<mat-label translate>widgets.input-widgets.subscript-sizing</mat-label>
<mat-select formControlName="subscriptSizing">
<mat-option [value]="'fixed'">
{{ 'widgets.input-widgets.subscript-sizing-fixed' | translate }}
</mat-option>
<mat-option [value]="'dynamic'">
{{ 'widgets.input-widgets.subscript-sizing-dynamic' | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</section>
</fieldset>
<fieldset [fxShow]="!updateMultipleAttributesKeySettingsForm.get('dataKeyHidden').value &&
updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value === 'booleanSwitch'" class="fields-group">
@ -208,7 +233,9 @@
</mat-form-field>
</section>
</fieldset>
<fieldset [fxShow]="!updateMultipleAttributesKeySettingsForm.get('dataKeyHidden').value" class="fields-group">
<fieldset [fxShow]="!updateMultipleAttributesKeySettingsForm.get('dataKeyHidden').value &&
!['date', 'dateTime', 'time'].includes(updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value)"
class="fields-group">
<legend class="group-title" translate>widgets.input-widgets.icon-settings</legend>
<mat-slide-toggle formControlName="useCustomIcon" class="slide-block">
{{ 'widgets.input-widgets.use-custom-icon' | translate }}

18
ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-key-settings.component.ts

@ -24,7 +24,10 @@ import {
dataKeySelectOptionValidator
} from '@home/components/widget/lib/settings/input/datakey-select-option.component';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { MultipleInputWidgetDataKeyValueType } from '@home/components/widget/lib/multiple-input-widget.component';
import {
MultipleInputWidgetDataKeyEditableType,
MultipleInputWidgetDataKeyValueType
} from '@home/components/widget/lib/multiple-input-widget.component';
@Component({
selector: 'tb-update-multiple-attributes-key-settings',
@ -52,6 +55,8 @@ export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettings
required: false,
isEditable: 'editable',
disabledOnDataKey: '',
appearance: 'outline',
subscriptSizing: 'fixed',
slideToggleLabelPosition: 'after',
selectOptions: [],
@ -92,6 +97,8 @@ export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettings
required: [settings.required, []],
isEditable: [settings.isEditable, []],
disabledOnDataKey: [settings.disabledOnDataKey, []],
appearance: [settings.appearance, []],
subscriptSizing: [settings.subscriptSizing, []],
// Slide toggle settings
@ -146,7 +153,7 @@ export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettings
const dataKeyValueType: MultipleInputWidgetDataKeyValueType =
this.updateMultipleAttributesKeySettingsForm.get('dataKeyValueType').value;
const required: boolean = this.updateMultipleAttributesKeySettingsForm.get('required').value;
const isEditable: string = this.updateMultipleAttributesKeySettingsForm.get('isEditable').value;
const isEditable: MultipleInputWidgetDataKeyEditableType = this.updateMultipleAttributesKeySettingsForm.get('isEditable').value;
const useCustomIcon: boolean = this.updateMultipleAttributesKeySettingsForm.get('useCustomIcon').value;
const useGetValueFunction: boolean = this.updateMultipleAttributesKeySettingsForm.get('useGetValueFunction').value;
const useSetValueFunction: boolean = this.updateMultipleAttributesKeySettingsForm.get('useSetValueFunction').value;
@ -163,10 +170,15 @@ export class UpdateMultipleAttributesKeySettingsComponent extends WidgetSettings
this.updateMultipleAttributesKeySettingsForm.get('useGetValueFunction').enable({emitEvent: false});
this.updateMultipleAttributesKeySettingsForm.get('useSetValueFunction').enable({emitEvent: false});
if (isEditable !== 'disabled') {
if (isEditable === 'editable') {
this.updateMultipleAttributesKeySettingsForm.get('disabledOnDataKey').enable({emitEvent: false});
}
if (!['booleanSwitch', 'booleanCheckbox'].includes(dataKeyValueType)) {
this.updateMultipleAttributesKeySettingsForm.get('appearance').enable({emitEvent: false});
this.updateMultipleAttributesKeySettingsForm.get('subscriptSizing').enable({emitEvent: false});
}
if (dataKeyValueType === 'booleanSwitch') {
this.updateMultipleAttributesKeySettingsForm.get('slideToggleLabelPosition').enable({emitEvent: false});
} else if (dataKeyValueType === 'select') {

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.html

@ -72,7 +72,7 @@
</section>
</fieldset>
<fieldset class="fields-group">
<legend class="group-title" translate>widgets.input-widgets.fields-alignment</legend>
<legend class="group-title" translate>widgets.input-widgets.layout</legend>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex="50" class="mat-block">
<mat-label translate>widgets.input-widgets.fields-alignment</mat-label>
@ -90,5 +90,15 @@
<input matInput type="number" min="1" step="1" formControlName="fieldsInRow">
</mat-form-field>
</section>
<section fxLayout="column" fxLayout.gt-xs="row" fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<mat-form-field fxFlex="50" class="mat-block">
<mat-label translate>widgets.input-widgets.row-gap</mat-label>
<input matInput type="number" min="0" step="1" formControlName="rowGap">
</mat-form-field>
<mat-form-field fxFlex="50" [fxShow]="updateMultipleAttributesWidgetSettingsForm.get('fieldsAlignment').value === 'row'" class="mat-block">
<mat-label translate>widgets.input-widgets.column-gap</mat-label>
<input matInput type="number" min="0" step="1" formControlName="columnGap">
</mat-form-field>
</section>
</fieldset>
</section>

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/input/update-multiple-attributes-widget-settings.component.ts

@ -49,7 +49,9 @@ export class UpdateMultipleAttributesWidgetSettingsComponent extends WidgetSetti
showGroupTitle: false,
groupTitle: '',
fieldsAlignment: 'row',
fieldsInRow: 2
fieldsInRow: 2,
rowGap: 5,
columnGap: 10
};
}
@ -77,6 +79,11 @@ export class UpdateMultipleAttributesWidgetSettingsComponent extends WidgetSetti
fieldsAlignment: [settings.fieldsAlignment, []],
fieldsInRow: [settings.fieldsInRow, [Validators.min(1)]],
// Layout gap
rowGap: [settings.rowGap, [Validators.min(0)]],
columnGap: [settings.columnGap, [Validators.min(0)]]
});
}
@ -105,13 +112,16 @@ export class UpdateMultipleAttributesWidgetSettingsComponent extends WidgetSetti
}
if (fieldsAlignment === 'row') {
this.updateMultipleAttributesWidgetSettingsForm.get('fieldsInRow').enable();
this.updateMultipleAttributesWidgetSettingsForm.get('columnGap').enable();
} else {
this.updateMultipleAttributesWidgetSettingsForm.get('fieldsInRow').disable();
this.updateMultipleAttributesWidgetSettingsForm.get('columnGap').disable();
}
this.updateMultipleAttributesWidgetSettingsForm.get('updateAllValues').updateValueAndValidity({emitEvent});
this.updateMultipleAttributesWidgetSettingsForm.get('saveButtonLabel').updateValueAndValidity({emitEvent});
this.updateMultipleAttributesWidgetSettingsForm.get('resetButtonLabel').updateValueAndValidity({emitEvent});
this.updateMultipleAttributesWidgetSettingsForm.get('groupTitle').updateValueAndValidity({emitEvent});
this.updateMultipleAttributesWidgetSettingsForm.get('fieldsInRow').updateValueAndValidity({emitEvent});
this.updateMultipleAttributesWidgetSettingsForm.get('columnGap').updateValueAndValidity({emitEvent});
}
}

2
ui-ngx/src/app/shared/components/color-input.component.html

@ -40,7 +40,7 @@
class="tb-box-button"
[disabled]="disabled"
#matButton
(click)="openColorPickerPopup($event, matButton)">
(click)="openColorPickerPopup($event, matButton._elementRef)">
<div class="tb-color-preview no-margin box" [ngClass]="{'disabled': disabled}">
<div class="tb-color-result" [style]="!disabled ? {background: colorFormGroup.get('color').value} : {}"></div>
</div>

83
ui-ngx/src/app/shared/components/color-input.component.ts

@ -14,7 +14,16 @@
/// limitations under the License.
///
import { ChangeDetectorRef, Component, forwardRef, Input, OnInit, Renderer2, ViewContainerRef } from '@angular/core';
import {
ChangeDetectorRef,
Component,
ElementRef,
forwardRef,
Input,
OnInit,
Renderer2,
ViewContainerRef
} from '@angular/core';
import { PageComponent } from '@shared/components/page.component';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -84,6 +93,10 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro
@Input()
disabled: boolean;
@Input()
@coerceBoolean()
readonly = false;
private modelValue: string;
private propagateChange = null;
@ -151,43 +164,47 @@ export class ColorInputComponent extends PageComponent implements OnInit, Contro
showColorPicker($event: MouseEvent) {
$event.stopPropagation();
this.dialogs.colorPicker(this.colorFormGroup.get('color').value,
this.colorClearButton).subscribe(
(result) => {
if (!result?.canceled) {
this.colorFormGroup.patchValue(
{color: result?.color}, {emitEvent: true}
);
this.cd.markForCheck();
}
}
);
if (!this.disabled && !this.readonly) {
this.dialogs.colorPicker(this.colorFormGroup.get('color').value,
this.colorClearButton).subscribe(
(result) => {
if (!result?.canceled) {
this.colorFormGroup.patchValue(
{color: result?.color}, {emitEvent: true}
);
this.cd.markForCheck();
}
}
);
}
}
openColorPickerPopup($event: Event, matButton: MatButton) {
openColorPickerPopup($event: Event, element?: ElementRef) {
if ($event) {
$event.stopPropagation();
}
const trigger = matButton._elementRef.nativeElement;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const colorPickerPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, ColorPickerPanelComponent, 'left', true, null,
{
color: this.colorFormGroup.get('color').value,
colorClearButton: this.colorClearButton
},
{},
{}, {}, true);
colorPickerPopover.tbComponentRef.instance.popover = colorPickerPopover;
colorPickerPopover.tbComponentRef.instance.colorSelected.subscribe((color) => {
colorPickerPopover.hide();
this.colorFormGroup.patchValue(
{color}, {emitEvent: true}
);
this.cd.markForCheck();
});
if (!this.disabled && !this.readonly) {
const trigger = element ? element.nativeElement : $event.target;
if (this.popoverService.hasPopover(trigger)) {
this.popoverService.hidePopover(trigger);
} else {
const colorPickerPopover = this.popoverService.displayPopover(trigger, this.renderer,
this.viewContainerRef, ColorPickerPanelComponent, 'left', true, null,
{
color: this.colorFormGroup.get('color').value,
colorClearButton: this.colorClearButton
},
{},
{}, {}, true);
colorPickerPopover.tbComponentRef.instance.popover = colorPickerPopover;
colorPickerPopover.tbComponentRef.instance.colorSelected.subscribe((color) => {
colorPickerPopover.hide();
this.colorFormGroup.patchValue(
{color}, {emitEvent: true}
);
this.cd.markForCheck();
});
}
}
}

12
ui-ngx/src/app/shared/models/device.models.ts

@ -298,14 +298,16 @@ export enum SnmpSpecType {
TELEMETRY_QUERYING = 'TELEMETRY_QUERYING',
CLIENT_ATTRIBUTES_QUERYING = 'CLIENT_ATTRIBUTES_QUERYING',
SHARED_ATTRIBUTES_SETTING = 'SHARED_ATTRIBUTES_SETTING',
TO_DEVICE_RPC_REQUEST = 'TO_DEVICE_RPC_REQUEST'
TO_DEVICE_RPC_REQUEST = 'TO_DEVICE_RPC_REQUEST',
TO_SERVER_RPC_REQUEST = 'TO_SERVER_RPC_REQUEST'
}
export const SnmpSpecTypeTranslationMap = new Map<SnmpSpecType, string>([
[SnmpSpecType.TELEMETRY_QUERYING, ' Telemetry'],
[SnmpSpecType.CLIENT_ATTRIBUTES_QUERYING, 'Client attributes'],
[SnmpSpecType.SHARED_ATTRIBUTES_SETTING, 'Shared attributes'],
[SnmpSpecType.TO_DEVICE_RPC_REQUEST, 'RPC request']
[SnmpSpecType.TELEMETRY_QUERYING, ' Telemetry (SNMP GET)'],
[SnmpSpecType.CLIENT_ATTRIBUTES_QUERYING, 'Client attributes (SNMP GET)'],
[SnmpSpecType.SHARED_ATTRIBUTES_SETTING, 'Shared attributes (SNMP SET)'],
[SnmpSpecType.TO_DEVICE_RPC_REQUEST, 'To-device RPC request (SNMP GET/SET)'],
[SnmpSpecType.TO_SERVER_RPC_REQUEST, 'From-device RPC request (SNMP TRAP)']
]);
export interface SnmpCommunicationConfig {

848
ui-ngx/src/assets/dashboard/api_usage.json

File diff suppressed because it is too large

45
ui-ngx/src/assets/dashboard/customer_user_home_page.json

@ -7,13 +7,7 @@
"description": "",
"widgets": {
"867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4": {
"isSystemType": true,
"bundleAlias": "home_page_widgets",
"typeAlias": "documentation_links",
"type": "static",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5,
"sizeY": 3,
"config": {
@ -41,16 +35,11 @@
},
"row": 0,
"col": 0,
"id": "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4"
"id": "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4",
"typeFullFqn": "system.home_page_widgets.documentation_links"
},
"9e3ef045-d8bc-1640-a3f4-2dd10b19d50e": {
"isSystemType": true,
"bundleAlias": "home_page_widgets",
"typeAlias": "quick_links",
"type": "static",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5,
"sizeY": 3,
"config": {
@ -73,16 +62,11 @@
},
"row": 0,
"col": 0,
"id": "9e3ef045-d8bc-1640-a3f4-2dd10b19d50e"
"id": "9e3ef045-d8bc-1640-a3f4-2dd10b19d50e",
"typeFullFqn": "system.home_page_widgets.quick_links"
},
"6d6e2b1d-6ce7-4678-3745-c6b0897b2674": {
"isSystemType": true,
"bundleAlias": "home_page_widgets",
"typeAlias": "dashboards",
"type": "static",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5,
"sizeY": 6.5,
"config": {
@ -103,16 +87,11 @@
},
"row": 0,
"col": 0,
"id": "6d6e2b1d-6ce7-4678-3745-c6b0897b2674"
"id": "6d6e2b1d-6ce7-4678-3745-c6b0897b2674",
"typeFullFqn": "system.home_page_widgets.dashboards"
},
"35d3ac3b-f1af-dc3d-a1d1-11f351d16fc6": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
@ -238,16 +217,11 @@
},
"row": 0,
"col": 0,
"id": "35d3ac3b-f1af-dc3d-a1d1-11f351d16fc6"
"id": "35d3ac3b-f1af-dc3d-a1d1-11f351d16fc6",
"typeFullFqn": "system.cards.markdown_card"
},
"7ac20b6a-dc40-b18e-9f5f-bca20bc693bb": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
@ -392,7 +366,8 @@
},
"row": 0,
"col": 0,
"id": "7ac20b6a-dc40-b18e-9f5f-bca20bc693bb"
"id": "7ac20b6a-dc40-b18e-9f5f-bca20bc693bb",
"typeFullFqn": "system.cards.markdown_card"
}
},
"states": {

250
ui-ngx/src/assets/dashboard/sys_admin_home_page.json

@ -7,13 +7,7 @@
"description": "",
"widgets": {
"d70cc256-4c7b-ee06-9905-b8c5e546605f": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
@ -72,16 +66,11 @@
},
"row": 0,
"col": 0,
"id": "d70cc256-4c7b-ee06-9905-b8c5e546605f"
"id": "d70cc256-4c7b-ee06-9905-b8c5e546605f",
"typeFullFqn": "system.cards.markdown_card"
},
"8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e": {
"isSystemType": true,
"bundleAlias": "charts",
"typeAlias": "timeseries_bars_flot",
"type": "timeseries",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 8,
"sizeY": 5,
"config": {
@ -179,7 +168,18 @@
"showLabels": true
},
"customLegendEnabled": false,
"dataKeysListForLabels": []
"dataKeysListForLabels": [],
"showLegend": false,
"legendConfig": {
"direction": "column",
"position": "bottom",
"sortDataKeys": false,
"showMin": false,
"showMax": false,
"showAvg": true,
"showTotal": false,
"showLatest": false
}
},
"title": "Transport messages",
"dropShadow": false,
@ -192,37 +192,21 @@
"padding": "0"
},
"useDashboardTimewindow": false,
"showLegend": false,
"actions": {},
"displayTimewindow": true,
"showTitleIcon": false,
"titleTooltip": "",
"widgetCss": ".tb-widget-container > .tb-widget {\n border: none !important;\n border-radius: 0 !important;\n box-shadow: none !important;\n}\n\n.tb-widget-container > .tb-widget .flot-base {\n opacity: 0.48;\n}\n",
"pageSize": 1024,
"noDataDisplayMessage": "",
"legendConfig": {
"direction": "column",
"position": "bottom",
"sortDataKeys": false,
"showMin": false,
"showMax": false,
"showAvg": true,
"showTotal": false,
"showLatest": false
}
"noDataDisplayMessage": ""
},
"row": 0,
"col": 0,
"id": "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e"
"id": "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e",
"typeFullFqn": "system.charts.timeseries_bars_flot"
},
"4b5e47ed-c197-a937-d727-041ba8decec2": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
@ -307,16 +291,11 @@
},
"row": 0,
"col": 0,
"id": "4b5e47ed-c197-a937-d727-041ba8decec2"
"id": "4b5e47ed-c197-a937-d727-041ba8decec2",
"typeFullFqn": "system.cards.markdown_card"
},
"eace9148-b02a-48fe-1a95-acd9928aa8c5": {
"isSystemType": true,
"bundleAlias": "charts",
"typeAlias": "basic_timeseries",
"type": "timeseries",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 8,
"sizeY": 5,
"config": {
@ -501,7 +480,18 @@
"showLabels": true
},
"customLegendEnabled": false,
"dataKeysListForLabels": []
"dataKeysListForLabels": [],
"showLegend": true,
"legendConfig": {
"direction": "row",
"position": "top",
"sortDataKeys": false,
"showMin": false,
"showMax": false,
"showAvg": false,
"showTotal": false,
"showLatest": false
}
},
"title": "System Info Chart",
"dropShadow": false,
@ -518,31 +508,15 @@
"pageSize": 1024,
"units": "%",
"noDataDisplayMessage": "",
"showLegend": true,
"legendConfig": {
"direction": "row",
"position": "top",
"sortDataKeys": false,
"showMin": false,
"showMax": false,
"showAvg": false,
"showTotal": false,
"showLatest": false
},
"displayTimewindow": true
},
"row": 0,
"col": 0,
"id": "eace9148-b02a-48fe-1a95-acd9928aa8c5"
"id": "eace9148-b02a-48fe-1a95-acd9928aa8c5",
"typeFullFqn": "system.charts.basic_timeseries"
},
"8acbf5df-f9fc-114d-216f-86f081aa4779": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
@ -601,16 +575,11 @@
},
"row": 0,
"col": 0,
"id": "8acbf5df-f9fc-114d-216f-86f081aa4779"
"id": "8acbf5df-f9fc-114d-216f-86f081aa4779",
"typeFullFqn": "system.cards.markdown_card"
},
"d2784f6c-0518-fd95-1d28-b21f70bdcb10": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
@ -668,16 +637,11 @@
},
"row": 0,
"col": 0,
"id": "d2784f6c-0518-fd95-1d28-b21f70bdcb10"
"id": "d2784f6c-0518-fd95-1d28-b21f70bdcb10",
"typeFullFqn": "system.cards.markdown_card"
},
"66dcf1a2-9d83-6873-c693-d6a5d989b3f8": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
@ -735,16 +699,11 @@
},
"row": 0,
"col": 0,
"id": "66dcf1a2-9d83-6873-c693-d6a5d989b3f8"
"id": "66dcf1a2-9d83-6873-c693-d6a5d989b3f8",
"typeFullFqn": "system.cards.markdown_card"
},
"867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4": {
"isSystemType": true,
"bundleAlias": "home_page_widgets",
"typeAlias": "documentation_links",
"type": "static",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5,
"sizeY": 3,
"config": {
@ -772,16 +731,11 @@
},
"row": 0,
"col": 0,
"id": "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4"
"id": "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4",
"typeFullFqn": "system.home_page_widgets.documentation_links"
},
"a23185ad-dc46-806c-0e50-5b21fb080ace": {
"isSystemType": true,
"bundleAlias": "home_page_widgets",
"typeAlias": "getting_started",
"type": "static",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5,
"sizeY": 6.5,
"config": {
@ -809,16 +763,11 @@
},
"row": 0,
"col": 0,
"id": "a23185ad-dc46-806c-0e50-5b21fb080ace"
"id": "a23185ad-dc46-806c-0e50-5b21fb080ace",
"typeFullFqn": "system.home_page_widgets.getting_started"
},
"b35fa1b0-7f95-0b23-fade-d066653a40e1": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
@ -930,16 +879,11 @@
},
"row": 0,
"col": 0,
"id": "b35fa1b0-7f95-0b23-fade-d066653a40e1"
"id": "b35fa1b0-7f95-0b23-fade-d066653a40e1",
"typeFullFqn": "system.cards.markdown_card"
},
"5eabde4c-095f-defc-dd09-6c0afb9b9f7b": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
@ -1063,16 +1007,11 @@
},
"row": 0,
"col": 0,
"id": "5eabde4c-095f-defc-dd09-6c0afb9b9f7b"
"id": "5eabde4c-095f-defc-dd09-6c0afb9b9f7b",
"typeFullFqn": "system.cards.markdown_card"
},
"451e2588-80b8-cd08-2199-1263ed4dc12c": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
@ -1196,16 +1135,11 @@
},
"row": 0,
"col": 0,
"id": "451e2588-80b8-cd08-2199-1263ed4dc12c"
"id": "451e2588-80b8-cd08-2199-1263ed4dc12c",
"typeFullFqn": "system.cards.markdown_card"
},
"01b94c17-84eb-4e49-6a92-c3f108ecff8e": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
@ -1417,16 +1351,11 @@
},
"row": 0,
"col": 0,
"id": "01b94c17-84eb-4e49-6a92-c3f108ecff8e"
"id": "01b94c17-84eb-4e49-6a92-c3f108ecff8e",
"typeFullFqn": "system.cards.markdown_card"
},
"f0479b00-ed47-0a30-0c36-bde7847aae00": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
@ -1488,16 +1417,11 @@
},
"row": 0,
"col": 0,
"id": "f0479b00-ed47-0a30-0c36-bde7847aae00"
"id": "f0479b00-ed47-0a30-0c36-bde7847aae00",
"typeFullFqn": "system.cards.markdown_card"
},
"57ed0ad9-8fcf-4454-1fa7-d39e22f84570": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
@ -1579,16 +1503,11 @@
},
"row": 0,
"col": 0,
"id": "57ed0ad9-8fcf-4454-1fa7-d39e22f84570"
"id": "57ed0ad9-8fcf-4454-1fa7-d39e22f84570",
"typeFullFqn": "system.cards.markdown_card"
},
"0464b6bc-7863-ba75-4b84-64b8acae2af9": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
@ -1670,16 +1589,11 @@
},
"row": 0,
"col": 0,
"id": "0464b6bc-7863-ba75-4b84-64b8acae2af9"
"id": "0464b6bc-7863-ba75-4b84-64b8acae2af9",
"typeFullFqn": "system.cards.markdown_card"
},
"4c043bba-6ac1-dd23-0796-fe6ae4c3c0a3": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
@ -1761,16 +1675,11 @@
},
"row": 0,
"col": 0,
"id": "4c043bba-6ac1-dd23-0796-fe6ae4c3c0a3"
"id": "4c043bba-6ac1-dd23-0796-fe6ae4c3c0a3",
"typeFullFqn": "system.cards.markdown_card"
},
"8b8d5d4f-2c5f-c768-d4fb-68d6e0b716f4": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
@ -1852,16 +1761,11 @@
},
"row": 0,
"col": 0,
"id": "8b8d5d4f-2c5f-c768-d4fb-68d6e0b716f4"
"id": "8b8d5d4f-2c5f-c768-d4fb-68d6e0b716f4",
"typeFullFqn": "system.cards.markdown_card"
},
"d58bfdd4-18b0-a70c-5d5f-73d12c2ea468": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
@ -1943,16 +1847,11 @@
},
"row": 0,
"col": 0,
"id": "d58bfdd4-18b0-a70c-5d5f-73d12c2ea468"
"id": "d58bfdd4-18b0-a70c-5d5f-73d12c2ea468",
"typeFullFqn": "system.cards.markdown_card"
},
"a3d83738-a4f2-ea92-17d7-1a3c52aa1770": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
@ -2034,16 +1933,11 @@
},
"row": 0,
"col": 0,
"id": "a3d83738-a4f2-ea92-17d7-1a3c52aa1770"
"id": "a3d83738-a4f2-ea92-17d7-1a3c52aa1770",
"typeFullFqn": "system.cards.markdown_card"
},
"ff4f04a2-f739-2f3e-17dc-9ede9f2df75d": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
@ -2235,16 +2129,11 @@
},
"row": 0,
"col": 0,
"id": "ff4f04a2-f739-2f3e-17dc-9ede9f2df75d"
"id": "ff4f04a2-f739-2f3e-17dc-9ede9f2df75d",
"typeFullFqn": "system.cards.markdown_card"
},
"163025d8-3ca4-dd1e-c17b-e40d8e03e8a8": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
@ -2306,7 +2195,8 @@
},
"row": 0,
"col": 0,
"id": "163025d8-3ca4-dd1e-c17b-e40d8e03e8a8"
"id": "163025d8-3ca4-dd1e-c17b-e40d8e03e8a8",
"typeFullFqn": "system.cards.markdown_card"
}
},
"states": {
@ -2721,4 +2611,4 @@
},
"externalId": null,
"name": "System Administrator Home Page"
}
}

144
ui-ngx/src/assets/dashboard/tenant_admin_home_page.json

@ -7,13 +7,7 @@
"description": "",
"widgets": {
"d70cc256-4c7b-ee06-9905-b8c5e546605f": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
@ -72,16 +66,11 @@
},
"row": 0,
"col": 0,
"id": "d70cc256-4c7b-ee06-9905-b8c5e546605f"
"id": "d70cc256-4c7b-ee06-9905-b8c5e546605f",
"typeFullFqn": "system.cards.markdown_card"
},
"8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e": {
"isSystemType": true,
"bundleAlias": "charts",
"typeAlias": "timeseries_bars_flot",
"type": "timeseries",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 8,
"sizeY": 5,
"config": {
@ -179,7 +168,18 @@
"showLabels": true
},
"customLegendEnabled": false,
"dataKeysListForLabels": []
"dataKeysListForLabels": [],
"showLegend": false,
"legendConfig": {
"direction": "column",
"position": "bottom",
"sortDataKeys": false,
"showMin": false,
"showMax": false,
"showAvg": true,
"showTotal": false,
"showLatest": false
}
},
"title": "Transport messages",
"dropShadow": false,
@ -192,37 +192,21 @@
"padding": "0"
},
"useDashboardTimewindow": false,
"showLegend": false,
"actions": {},
"displayTimewindow": true,
"showTitleIcon": false,
"titleTooltip": "",
"widgetCss": ".tb-widget-container > .tb-widget {\n border: none !important;\n border-radius: 0 !important;\n box-shadow: none !important;\n}\n\n.tb-widget-container > .tb-widget .flot-base {\n opacity: 0.48;\n}\n",
"pageSize": 1024,
"noDataDisplayMessage": "",
"legendConfig": {
"direction": "column",
"position": "bottom",
"sortDataKeys": false,
"showMin": false,
"showMax": false,
"showAvg": true,
"showTotal": false,
"showLatest": false
}
"noDataDisplayMessage": ""
},
"row": 0,
"col": 0,
"id": "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e"
"id": "8ee72d43-678c-4e25-e9a8-7d4cfd7a5f8e",
"typeFullFqn": "system.charts.timeseries_bars_flot"
},
"867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4": {
"isSystemType": true,
"bundleAlias": "home_page_widgets",
"typeAlias": "documentation_links",
"type": "static",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5,
"sizeY": 3,
"config": {
@ -250,16 +234,11 @@
},
"row": 0,
"col": 0,
"id": "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4"
"id": "867f82cf-ecf2-2d5c-35cb-08c6f2edc3a4",
"typeFullFqn": "system.home_page_widgets.documentation_links"
},
"a23185ad-dc46-806c-0e50-5b21fb080ace": {
"isSystemType": true,
"bundleAlias": "home_page_widgets",
"typeAlias": "getting_started",
"type": "static",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5,
"sizeY": 6.5,
"config": {
@ -287,16 +266,11 @@
},
"row": 0,
"col": 0,
"id": "a23185ad-dc46-806c-0e50-5b21fb080ace"
"id": "a23185ad-dc46-806c-0e50-5b21fb080ace",
"typeFullFqn": "system.home_page_widgets.getting_started"
},
"d26e5cd7-75ef-d475-00c7-1a2d1114efe8": {
"isSystemType": true,
"bundleAlias": "charts",
"typeAlias": "basic_timeseries",
"type": "timeseries",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 8,
"sizeY": 5,
"config": {
@ -412,7 +386,18 @@
"showLabels": true
},
"customLegendEnabled": false,
"dataKeysListForLabels": []
"dataKeysListForLabels": [],
"showLegend": false,
"legendConfig": {
"direction": "column",
"position": "bottom",
"sortDataKeys": false,
"showMin": false,
"showMax": false,
"showAvg": true,
"showTotal": false,
"showLatest": false
}
},
"title": "Devices activity",
"dropShadow": false,
@ -430,31 +415,15 @@
},
"widgetCss": ".tb-widget-container > .tb-widget {\n border: none !important;\n border-radius: 0 !important;\n box-shadow: none !important;\n}\n",
"pageSize": 1024,
"noDataDisplayMessage": "",
"showLegend": false,
"legendConfig": {
"direction": "column",
"position": "bottom",
"sortDataKeys": false,
"showMin": false,
"showMax": false,
"showAvg": true,
"showTotal": false,
"showLatest": false
}
"noDataDisplayMessage": ""
},
"row": 0,
"col": 0,
"id": "d26e5cd7-75ef-d475-00c7-1a2d1114efe8"
"id": "d26e5cd7-75ef-d475-00c7-1a2d1114efe8",
"typeFullFqn": "system.charts.basic_timeseries"
},
"ebbd0a6e-8a47-e770-5086-7f4974250f2d": {
"isSystemType": true,
"bundleAlias": "home_page_widgets",
"typeAlias": "usage_info",
"type": "static",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5,
"sizeY": 6.5,
"config": {
@ -475,16 +444,11 @@
},
"row": 0,
"col": 0,
"id": "ebbd0a6e-8a47-e770-5086-7f4974250f2d"
"id": "ebbd0a6e-8a47-e770-5086-7f4974250f2d",
"typeFullFqn": "system.home_page_widgets.usage_info"
},
"9e3ef045-d8bc-1640-a3f4-2dd10b19d50e": {
"isSystemType": true,
"bundleAlias": "home_page_widgets",
"typeAlias": "quick_links",
"type": "static",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5,
"sizeY": 3,
"config": {
@ -507,16 +471,11 @@
},
"row": 0,
"col": 0,
"id": "9e3ef045-d8bc-1640-a3f4-2dd10b19d50e"
"id": "9e3ef045-d8bc-1640-a3f4-2dd10b19d50e",
"typeFullFqn": "system.home_page_widgets.quick_links"
},
"6d6e2b1d-6ce7-4678-3745-c6b0897b2674": {
"isSystemType": true,
"bundleAlias": "home_page_widgets",
"typeAlias": "dashboards",
"type": "static",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 7.5,
"sizeY": 6.5,
"config": {
@ -537,16 +496,11 @@
},
"row": 0,
"col": 0,
"id": "6d6e2b1d-6ce7-4678-3745-c6b0897b2674"
"id": "6d6e2b1d-6ce7-4678-3745-c6b0897b2674",
"typeFullFqn": "system.home_page_widgets.dashboards"
},
"35d3ac3b-f1af-dc3d-a1d1-11f351d16fc6": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
@ -672,16 +626,11 @@
},
"row": 0,
"col": 0,
"id": "35d3ac3b-f1af-dc3d-a1d1-11f351d16fc6"
"id": "35d3ac3b-f1af-dc3d-a1d1-11f351d16fc6",
"typeFullFqn": "system.cards.markdown_card"
},
"7ac20b6a-dc40-b18e-9f5f-bca20bc693bb": {
"isSystemType": true,
"bundleAlias": "cards",
"typeAlias": "markdown_card",
"type": "latest",
"title": "New widget",
"image": null,
"description": null,
"sizeX": 5,
"sizeY": 3.5,
"config": {
@ -826,7 +775,8 @@
},
"row": 0,
"col": 0,
"id": "7ac20b6a-dc40-b18e-9f5f-bca20bc693bb"
"id": "7ac20b6a-dc40-b18e-9f5f-bca20bc693bb",
"typeFullFqn": "system.cards.markdown_card"
}
},
"states": {
@ -1101,4 +1051,4 @@
},
"externalId": null,
"name": "Tenant Administrator Home Page"
}
}

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

@ -2784,6 +2784,12 @@
"lwm2m-server-secret-key": "LwM2M server secret key",
"lwm2m-server-public-key-id": "LwM2M server public key or id"
},
"snmp": {
"host": "SNMP host",
"port": "SNMP port",
"version": "SNMP version (v1, v2c or v3)",
"community-string": "SNMP community string"
},
"isgateway": "Is Gateway",
"activity-time-from-gateway-device": "Activity time from gateway device",
"description": "Description",
@ -5329,6 +5335,9 @@
"input-fields-alignment": "Input fields alignment",
"input-fields-alignment-column": "Column (default)",
"input-fields-alignment-row": "Row",
"layout": "Layout",
"row-gap": "Gap between rows in pixels",
"column-gap": "Gap between columns in pixels",
"latitude-field-required": "Latitude field required",
"longitude-field-required": "Longitude field required",
"attribute-settings": "Attribute settings",
@ -5384,6 +5393,12 @@
"ability-to-edit-attribute-disabled": "Disabled",
"ability-to-edit-attribute-readonly": "Read-only",
"disable-on-datakey-name": "Disable on false value of another datakey (specify datakey name)",
"field-appearance": "Field appearance",
"appearance-fill": "Fill",
"appearance-outline": "Outline",
"subscript-sizing": "Subscript sizing",
"subscript-sizing-fixed": "Fixed",
"subscript-sizing-dynamic": "Dynamic",
"slide-toggle-settings": "Slide toggle settings",
"slide-toggle-label-position": "Slide toggle label position",
"slide-toggle-label-position-after": "After",

Loading…
Cancel
Save