Browse Source

Merge branch 'master' of github.com:ShvaykaD/thingsboard into feature/json-payload-for-v1-topics-in-proto-profile

pull/5441/head
ShvaykaD 5 years ago
parent
commit
ac12eaecd8
  1. 4
      application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java
  2. 6
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  3. 8
      application/src/main/java/org/thingsboard/server/controller/AuditLogController.java
  4. 8
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  5. 164
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  6. 2
      application/src/main/java/org/thingsboard/server/controller/DashboardController.java
  7. 36
      application/src/main/java/org/thingsboard/server/controller/DeviceController.java
  8. 130
      application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
  9. 46
      application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java
  10. 18
      application/src/main/java/org/thingsboard/server/controller/RpcV2Controller.java
  11. 328
      application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
  12. 14
      application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
  13. 46
      application/src/main/java/org/thingsboard/server/controller/UiSettingsController.java
  14. 20
      application/src/main/java/org/thingsboard/server/controller/UserController.java
  15. 4
      application/src/main/java/org/thingsboard/server/controller/WidgetTypeController.java
  16. 12
      application/src/main/java/org/thingsboard/server/controller/WidgetsBundleController.java
  17. 26
      application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServerSecurityInfoRepository.java
  18. 159
      application/src/main/resources/thingsboard.yml
  19. 25
      application/src/test/resources/application-test.properties
  20. 37
      common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java
  21. 8
      common/data/src/main/java/org/thingsboard/server/common/data/Device.java
  22. 32
      common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java
  23. 3
      common/data/src/main/java/org/thingsboard/server/common/data/EntityViewInfo.java
  24. 24
      common/data/src/main/java/org/thingsboard/server/common/data/ResourceUtils.java
  25. 32
      common/data/src/main/java/org/thingsboard/server/common/data/SaveDeviceWithCredentialsRequest.java
  26. 6
      common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java
  27. 21
      common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/ServerSecurityConfig.java
  28. 6
      common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java
  29. 5
      common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java
  30. 252
      common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
  31. 52
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java
  32. 6
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MSecureServerConfig.java
  33. 24
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportBootstrapConfig.java
  34. 69
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java
  35. 8
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java
  36. 27
      common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java
  37. 42
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java
  38. 3
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
  39. 8
      common/transport/transport-api/pom.xml
  40. 191
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/AbstractSslCredentials.java
  41. 52
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/KeystoreSslCredentials.java
  42. 132
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/PemSslCredentials.java
  43. 45
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentials.java
  44. 66
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsConfig.java
  45. 21
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsType.java
  46. 7
      dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java
  47. 6
      docker/haproxy/config/haproxy.cfg
  48. 4
      msa/web-ui/server.js
  49. 9
      rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java
  50. 36
      transport/coap/src/main/resources/tb-coap-transport.yml
  51. 83
      transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml
  52. 30
      transport/mqtt/src/main/resources/tb-mqtt-transport.yml
  53. 4
      ui-ngx/proxy.conf.js
  54. 43
      ui-ngx/src/app/core/http/ui-settings.service.ts
  55. 58
      ui-ngx/src/app/core/services/help.service.ts
  56. 7
      ui-ngx/src/app/shared/components/help-popup.component.ts
  57. 70
      ui-ngx/src/app/shared/components/marked-options.service.ts
  58. 4
      ui-ngx/src/assets/help/en_US/rulenode/clear_alarm_node_script_fn.md
  59. 0
      ui-ngx/src/assets/help/en_US/rulenode/common_node_script_args.md
  60. 4
      ui-ngx/src/assets/help/en_US/rulenode/create_alarm_node_script_fn.md
  61. 4
      ui-ngx/src/assets/help/en_US/rulenode/filter_node_script_fn.md
  62. 0
      ui-ngx/src/assets/help/en_US/rulenode/generator_node_script_fn.md
  63. 2
      ui-ngx/src/assets/help/en_US/rulenode/log_node_script_fn.md
  64. 2
      ui-ngx/src/assets/help/en_US/rulenode/switch_node_script_fn.md
  65. 4
      ui-ngx/src/assets/help/en_US/rulenode/transformation_node_script_fn.md
  66. 2
      ui-ngx/src/assets/help/en_US/widget/action/custom_action_fn.md
  67. 4
      ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_html.md
  68. 4
      ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_js.md
  69. 4
      ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_html.md
  70. 4
      ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_js.md
  71. 2
      ui-ngx/src/assets/help/en_US/widget/action/mobile_process_qr_code_fn.md
  72. 2
      ui-ngx/src/assets/help/en_US/widget/editor/examples/alarm_widget.md
  73. 2
      ui-ngx/src/assets/help/en_US/widget/editor/examples/ext_latest_values_example.md
  74. 2
      ui-ngx/src/assets/help/en_US/widget/editor/examples/ext_timeseries_example.md
  75. 2
      ui-ngx/src/assets/help/en_US/widget/editor/examples/latest_values_widget.md
  76. 18
      ui-ngx/src/assets/help/en_US/widget/editor/examples/rpc_widget.md
  77. 2
      ui-ngx/src/assets/help/en_US/widget/editor/examples/static_widget.md
  78. 2
      ui-ngx/src/assets/help/en_US/widget/editor/examples/timeseries_widget.md
  79. 4
      ui-ngx/src/assets/help/en_US/widget/editor/widget_js_fn.md
  80. 8
      ui-ngx/src/assets/help/en_US/widget/editor/widget_js_subscription_object.md
  81. 2
      ui-ngx/src/assets/help/en_US/widget/lib/markdown/markdown_text_fn.md
  82. 2
      ui-ngx/src/assets/help/en_US/widget/lib/qrcode/qrcode_text_fn.md
  83. BIN
      ui-ngx/src/assets/help/images/widget/editor/examples/add-rpc-device-alias.png
  84. BIN
      ui-ngx/src/assets/help/images/widget/editor/examples/alarm-widget-sample.png
  85. BIN
      ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample-response-one-way.png
  86. BIN
      ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample-response-timeout.png
  87. BIN
      ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample-response-two-way.png
  88. BIN
      ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample-settings.png
  89. BIN
      ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample.png
  90. BIN
      ui-ngx/src/assets/help/images/widget/editor/examples/dashboard-create-new-widget-button.png
  91. BIN
      ui-ngx/src/assets/help/images/widget/editor/examples/dashboard-toolbar-entity-aliases.png
  92. BIN
      ui-ngx/src/assets/help/images/widget/editor/examples/external-js-timeseries-widget-sample.png
  93. BIN
      ui-ngx/src/assets/help/images/widget/editor/examples/external-js-widget-sample.png
  94. BIN
      ui-ngx/src/assets/help/images/widget/editor/examples/latest-values-widget-sample.png
  95. BIN
      ui-ngx/src/assets/help/images/widget/editor/examples/static-widget-sample.png
  96. BIN
      ui-ngx/src/assets/help/images/widget/editor/examples/timeseries-widget-sample.png
  97. 3
      ui-ngx/src/styles.scss

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

@ -25,7 +25,6 @@ import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.rpc.RpcError;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
@ -34,10 +33,11 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.rpc.RpcError;
import org.thingsboard.server.common.data.rpc.ToDeviceRpcRequestBody;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.service.rpc.LocalRequestMetaData;
import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService;
import org.thingsboard.server.service.security.AccessValidator;

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

@ -232,9 +232,9 @@ public class AlarmController extends BaseController {
@RequestMapping(value = "/alarm/{entityType}/{entityId}", method = RequestMethod.GET)
@ResponseBody
public PageData<AlarmInfo> getAlarms(
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION)
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE")
@PathVariable(ENTITY_TYPE) String strEntityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION)
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(ENTITY_ID) String strEntityId,
@ApiParam(value = ALARM_QUERY_SEARCH_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES)
@RequestParam(required = false) String searchStatus,
@ -333,7 +333,7 @@ public class AlarmController extends BaseController {
@RequestMapping(value = "/alarm/highestSeverity/{entityType}/{entityId}", method = RequestMethod.GET)
@ResponseBody
public AlarmSeverity getHighestAlarmSeverity(
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true)
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE")
@PathVariable(ENTITY_TYPE) String strEntityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(ENTITY_ID) String strEntityId,

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

@ -156,13 +156,13 @@ public class AuditLogController extends BaseController {
@RequestMapping(value = "/audit/logs/entity/{entityType}/{entityId}", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<AuditLog> getAuditLogsByEntityId(
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION)
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE")
@PathVariable("entityType") String strEntityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION)
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("entityId") String strEntityId,
@ApiParam(value = PAGE_SIZE_DESCRIPTION)
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION)
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = AUDIT_LOG_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,

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

@ -15,8 +15,6 @@
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
@ -50,13 +48,13 @@ import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.model.ActivateUserRequest;
import org.thingsboard.server.service.security.model.ChangePasswordRequest;
import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.service.security.model.ResetPasswordEmailRequest;
import org.thingsboard.server.service.security.model.ResetPasswordRequest;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;

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

@ -17,7 +17,6 @@ package org.thingsboard.server.controller;
public class ControllerConstants {
protected static final String NEW_LINE = "\n\n";
protected static final int DEFAULT_PAGE_SIZE = 1000;
protected static final String ENTITY_TYPE = "entityType";
@ -32,6 +31,7 @@ public class ControllerConstants {
protected static final String DASHBOARD_ID_PARAM_DESCRIPTION = "A string value representing the device id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String RPC_ID_PARAM_DESCRIPTION = "A string value representing the rpc id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String DEVICE_ID_PARAM_DESCRIPTION = "A string value representing the device id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String ENTITY_VIEW_ID_PARAM_DESCRIPTION = "A string value representing the entity view id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String DEVICE_PROFILE_ID_PARAM_DESCRIPTION = "A string value representing the device profile id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String TENANT_PROFILE_ID_PARAM_DESCRIPTION = "A string value representing the tenant profile id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String TENANT_ID_PARAM_DESCRIPTION = "A string value representing the tenant id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
@ -56,6 +56,7 @@ public class ControllerConstants {
protected static final String PAGE_SIZE_DESCRIPTION = "Maximum amount of entities in a one page";
protected static final String PAGE_NUMBER_DESCRIPTION = "Sequence number of page starting from 0";
protected static final String DEVICE_TYPE_DESCRIPTION = "Device type as the name of the device profile";
protected static final String ENTITY_VIEW_TYPE_DESCRIPTION = "Entity View type";
protected static final String ASSET_TYPE_DESCRIPTION = "Asset type";
protected static final String EDGE_TYPE_DESCRIPTION = "A string value representing the edge type. For example, 'default'";
protected static final String RULE_CHAIN_TYPE_DESCRIPTION = "Rule chain type (CORE or EDGE)";
@ -64,6 +65,7 @@ public class ControllerConstants {
protected static final String WIDGET_BUNDLE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the widget bundle title.";
protected static final String RPC_TEXT_SEARCH_DESCRIPTION = "Not implemented. Leave empty.";
protected static final String DEVICE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the device name.";
protected static final String ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the entity view name.";
protected static final String USER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the user email.";
protected static final String TENANT_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the tenant name.";
protected static final String TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'startsWith' filter based on the tenant profile name.";
@ -78,6 +80,8 @@ public class ControllerConstants {
protected static final String CUSTOMER_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title, email, country, city";
protected static final String RPC_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, expirationTime, request, response";
protected static final String DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, deviceProfileName, label, customerTitle";
protected static final String ENTITY_VIEW_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type";
protected static final String ENTITY_VIEW_INFO_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, customerTitle";
protected static final String USER_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, firstName, lastName, email";
protected static final String TENANT_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, title, email, country, state, city, address, address2, zip, phone, email";
protected static final String TENANT_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, description, isDefault";
@ -172,6 +176,46 @@ public class ControllerConstants {
protected static final String EVENT_DEBUG_RULE_CHAIN_FILTER_OBJ = MARKDOWN_CODE_BLOCK_START + "{\n" +
" \"eventType\":\"DEBUG_RULE_CHAIN\",\n" + DEBUG_FILTER_OBJ + MARKDOWN_CODE_BLOCK_END;
protected static final String IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION = "A Boolean value representing the Server SecurityInfo for future Bootstrap client mode settings. Values: 'true' for Bootstrap Server; 'false' for Lwm2m Server. ";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION =
"{\n" +
" \"device\": {\n" +
" \"name\": \"LwRpk00000000\",\n" +
" \"type\": \"lwm2mProfileRpk\"\n" +
" },\n" +
" \"credentials\": {\n" +
" \"id\": \"null\",\n" +
" \"createdTime\": 0,\n" +
" \"deviceId\": \"null\",\n" +
" \"credentialsType\": \"LWM2M_CREDENTIALS\",\n" +
" \"credentialsId\": \"LwRpk00000000\",\n" +
" \"credentialsValue\": {\n" +
" \"client\": {\n" +
" \"endpoint\": \"LwRpk00000000\",\n" +
" \"securityConfigClientMode\": \"RPK\",\n" +
" \"key\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\"\n" +
" },\n" +
" \"bootstrap\": {\n" +
" \"bootstrapServer\": {\n" +
" \"securityMode\": \"RPK\",\n" +
" \"clientPublicKeyOrId\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\",\n" +
" \"clientSecretKey\": \"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\"\n" +
" },\n" +
" \"lwm2mServer\": {\n" +
" \"securityMode\": \"RPK\",\n" +
" \"clientPublicKeyOrId\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUEBxNl/RcYJNm8mk91CyVXoIJiROYDlXcSSqK6e5bDHwOW4ZiN2lNnXalyF0Jxw8MbAytnDMERXyAja5VEMeVQ==\",\n" +
" \"clientSecretKey\": \"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgd9GAx7yZW37autew5KZykn4IgRpge/tZSjnudnZJnMahRANCAARQQHE2X9Fxgk2byaT3ULJVeggmJE5gOVdxJKorp7lsMfA5bhmI3aU2ddqXIXQnHDwxsDK2cMwRFfICNrlUQx5V\"\n" +
" }\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
protected static final String DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN =
MARKDOWN_CODE_BLOCK_START + DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION + MARKDOWN_CODE_BLOCK_END;
protected static final String FILTER_VALUE_TYPE = NEW_LINE + "## Value Type and Operations" + NEW_LINE +
"Provides a hint about the data type of the entity field that is defined in the filter key. " +
"The value type impacts the list of possible operations that you may use in the corresponding predicate. For example, you may use 'STARTS_WITH' or 'END_WITH', but you can't use 'GREATER_OR_EQUAL' for string values." +
@ -1359,4 +1403,122 @@ public class ControllerConstants {
protected static final String DEVICE_PROFILE_ID = "deviceProfileId";
protected static final String MODEL_DESCRIPTION = "See the 'Model' tab for more details.";
protected static final String ENTITY_VIEW_DESCRIPTION = "Entity Views limit the degree of exposure of the Device or Asset telemetry and attributes to the Customers. " +
"Every Entity View references exactly one entity (device or asset) and defines telemetry and attribute keys that will be visible to the assigned Customer. " +
"As a Tenant Administrator you are able to create multiple EVs per Device or Asset and assign them to different Customers. ";
protected static final String ENTITY_VIEW_INFO_DESCRIPTION = "Entity Views Info extends the Entity View with customer title and 'is public' flag. " + ENTITY_VIEW_DESCRIPTION;
protected static final String ATTRIBUTES_SCOPE_DESCRIPTION = "A string value representing the attributes scope. For example, 'SERVER_SCOPE'.";
protected static final String ATTRIBUTES_KEYS_DESCRIPTION = "A string value representing the comma-separated list of attributes keys. For example, 'active,inactivityAlarmTime'.";
protected static final String ATTRIBUTES_SCOPE_ALLOWED_VALUES = "SERVER_SCOPE, CLIENT_SCOPE, SHARED_SCOPE";
protected static final String ATTRIBUTES_JSON_REQUEST_DESCRIPTION = "A string value representing the json object. For example, '{\"key\":\"value\"}'. See API call description for more details.";
protected static final String TELEMETRY_KEYS_BASE_DESCRIPTION = "A string value representing the comma-separated list of telemetry keys.";
protected static final String TELEMETRY_KEYS_DESCRIPTION = TELEMETRY_KEYS_BASE_DESCRIPTION + " If keys are not selected, the result will return all latest timeseries. For example, 'temperature,humidity'.";
protected static final String TELEMETRY_SCOPE_DESCRIPTION = "Value is deprecated, reserved for backward compatibility and not used in the API call implementation. Specify any scope for compatibility";
protected static final String TELEMETRY_JSON_REQUEST_DESCRIPTION = "A JSON with the telemetry values. See API call description for more details.";
protected static final String STRICT_DATA_TYPES_DESCRIPTION = "Enables/disables conversion of telemetry values to strings. Conversion is enabled by default. Set parameter to 'true' in order to disable the conversion.";
protected static final String INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION = "Referencing a non-existing entity Id or invalid entity type will cause an error. ";
protected static final String SAVE_ATTIRIBUTES_STATUS_OK = "Attribute from the request was created or updated. ";
protected static final String INVALID_STRUCTURE_OF_THE_REQUEST = "Invalid structure of the request";
protected static final String SAVE_ATTIRIBUTES_STATUS_BAD_REQUEST = INVALID_STRUCTURE_OF_THE_REQUEST + " or invalid attributes scope provided.";
protected static final String SAVE_ENTITY_ATTRIBUTES_STATUS_OK = "Platform creates an audit log event about entity attributes updates with action type 'ATTRIBUTES_UPDATED', " +
"and also sends event msg to the rule engine with msg type 'ATTRIBUTES_UPDATED'.";
protected static final String SAVE_ENTITY_ATTRIBUTES_STATUS_UNAUTHORIZED = "User is not authorized to save entity attributes for selected entity. Most likely, User belongs to different Customer or Tenant.";
protected static final String SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about entity attributes updates with action type 'ATTRIBUTES_UPDATED' that includes an error stacktrace.";
protected static final String SAVE_ENTITY_TIMESERIES_STATUS_OK = "Timeseries from the request was created or updated. " +
"Platform creates an audit log event about entity timeseries updates with action type 'TIMESERIES_UPDATED'.";
protected static final String SAVE_ENTITY_TIMESERIES_STATUS_UNAUTHORIZED = "User is not authorized to save entity timeseries for selected entity. Most likely, User belongs to different Customer or Tenant.";
protected static final String SAVE_ENTITY_TIMESERIES_STATUS_INTERNAL_SERVER_ERROR = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about entity timeseries updates with action type 'TIMESERIES_UPDATED' that includes an error stacktrace.";
protected static final String ENTITY_ATTRIBUTE_SCOPES = " List of possible attribute scopes depends on the entity type: " +
"\n\n * SERVER_SCOPE - supported for all entity types;" +
"\n * CLIENT_SCOPE - supported for devices;" +
"\n * SHARED_SCOPE - supported for devices. "+ "\n\n";
protected static final String ATTRIBUTE_DATA_EXAMPLE = "[\n" +
" {\"key\": \"stringAttributeKey\", \"value\": \"value\", \"lastUpdateTs\": 1609459200000},\n" +
" {\"key\": \"booleanAttributeKey\", \"value\": false, \"lastUpdateTs\": 1609459200001},\n" +
" {\"key\": \"doubleAttributeKey\", \"value\": 42.2, \"lastUpdateTs\": 1609459200002},\n" +
" {\"key\": \"longKeyExample\", \"value\": 73, \"lastUpdateTs\": 1609459200003},\n" +
" {\"key\": \"jsonKeyExample\",\n" +
" \"value\": {\n" +
" \"someNumber\": 42,\n" +
" \"someArray\": [1,2,3],\n" +
" \"someNestedObject\": {\"key\": \"value\"}\n" +
" },\n" +
" \"lastUpdateTs\": 1609459200004\n" +
" }\n" +
"]";
protected static final String LATEST_TS_STRICT_DATA_EXAMPLE = "{\n" +
" \"stringTsKey\": [{ \"value\": \"value\", \"ts\": 1609459200000}],\n" +
" \"booleanTsKey\": [{ \"value\": false, \"ts\": 1609459200000}],\n" +
" \"doubleTsKey\": [{ \"value\": 42.2, \"ts\": 1609459200000}],\n" +
" \"longTsKey\": [{ \"value\": 73, \"ts\": 1609459200000}],\n" +
" \"jsonTsKey\": [{ \n" +
" \"value\": {\n" +
" \"someNumber\": 42,\n" +
" \"someArray\": [1,2,3],\n" +
" \"someNestedObject\": {\"key\": \"value\"}\n" +
" }, \n" +
" \"ts\": 1609459200000}]\n" +
"}\n";
protected static final String LATEST_TS_NON_STRICT_DATA_EXAMPLE = "{\n" +
" \"stringTsKey\": [{ \"value\": \"value\", \"ts\": 1609459200000}],\n" +
" \"booleanTsKey\": [{ \"value\": \"false\", \"ts\": 1609459200000}],\n" +
" \"doubleTsKey\": [{ \"value\": \"42.2\", \"ts\": 1609459200000}],\n" +
" \"longTsKey\": [{ \"value\": \"73\", \"ts\": 1609459200000}],\n" +
" \"jsonTsKey\": [{ \"value\": \"{\\\"someNumber\\\": 42,\\\"someArray\\\": [1,2,3],\\\"someNestedObject\\\": {\\\"key\\\": \\\"value\\\"}}\", \"ts\": 1609459200000}]\n" +
"}\n";
protected static final String TS_STRICT_DATA_EXAMPLE = "{\n" +
" \"temperature\": [\n" +
" {\n" +
" \"value\": 36.7,\n" +
" \"ts\": 1609459200000\n" +
" },\n" +
" {\n" +
" \"value\": 36.6,\n" +
" \"ts\": 1609459201000\n" +
" }\n" +
" ]\n" +
"}";
protected static final String SAVE_ATTRIBUTES_REQUEST_PAYLOAD = "The request payload is a JSON object with key-value format of attributes to create or update. " +
"For example:\n\n"
+ MARKDOWN_CODE_BLOCK_START
+ "{\n" +
" \"stringKey\":\"value1\", \n" +
" \"booleanKey\":true, \n" +
" \"doubleKey\":42.0, \n" +
" \"longKey\":73, \n" +
" \"jsonKey\": {\n" +
" \"someNumber\": 42,\n" +
" \"someArray\": [1,2,3],\n" +
" \"someNestedObject\": {\"key\": \"value\"}\n" +
" }\n" +
"}"
+ MARKDOWN_CODE_BLOCK_END + "\n";
protected static final String SAVE_TIMESERIES_REQUEST_PAYLOAD = "The request payload is a JSON document with three possible formats:\n\n" +
"Simple format without timestamp. In such a case, current server time will be used: \n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\"temperature\": 26}" +
MARKDOWN_CODE_BLOCK_END +
"\n\n Single JSON object with timestamp: \n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\"ts\":1634712287000,\"values\":{\"temperature\":26, \"humidity\":87}}" +
MARKDOWN_CODE_BLOCK_END +
"\n\n JSON array with timestamps: \n\n" +
MARKDOWN_CODE_BLOCK_START +
"[{\"ts\":1634712287000,\"values\":{\"temperature\":26, \"humidity\":87}}, {\"ts\":1634712588000,\"values\":{\"temperature\":25, \"humidity\":88}}]" +
MARKDOWN_CODE_BLOCK_END ;
}

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

@ -96,7 +96,7 @@ public class DashboardController extends BaseController {
public static final String DASHBOARD_DEFINITION = "The Dashboard object is a heavyweight object that contains information about the dashboard (e.g. title, image, assigned customers) and also configuration JSON (e.g. layouts, widgets, entity aliases).";
public static final String HIDDEN_FOR_MOBILE = "Exclude dashboards that are hidden for mobile";
@Value("${dashboard.max_datapoints_limit}")
@Value("${ui.dashboard.max_datapoints_limit}")
private long maxDatapointsLimit;
@ApiOperation(value = "Get server time (getServerTime)",

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

@ -46,6 +46,7 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.device.DeviceSearchQuery;
@ -87,6 +88,7 @@ import java.util.UUID;
import java.util.stream.Collectors;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_INFO_DESCRIPTION;
@ -95,6 +97,7 @@ import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFI
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_TYPE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ID_PARAM_DESCRIPTION;
@ -197,7 +200,38 @@ public class DeviceController extends BaseController {
null, created ? ActionType.ADDED : ActionType.UPDATED, e);
throw handleException(e);
}
}
@ApiOperation(value = "Create Device (saveDevice) with credentials ",
notes = "Create or update the Device. When creating device, platform generates Device Id as [time-based UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_1_(date-time_and_MAC_address). " +
"Requires to provide the Device Credentials object as well. Useful to create device and credentials in one request. " +
"You may find the example of LwM2M device and RPK credentials below: \n\n" +
DEVICE_WITH_DEVICE_CREDENTIALS_PARAM_DESCRIPTION_MARKDOWN +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/device-with-credentials", method = RequestMethod.POST)
@ResponseBody
public Device saveDeviceWithCredentials(@ApiParam(value = "The JSON object with device and credentials. See method description above for example.")
@RequestBody SaveDeviceWithCredentialsRequest deviceAndCredentials) throws ThingsboardException {
Device device = checkNotNull(deviceAndCredentials.getDevice());
DeviceCredentials credentials = checkNotNull(deviceAndCredentials.getCredentials());
boolean created = device.getId() == null;
try {
device.setTenantId(getCurrentUser().getTenantId());
checkEntity(device.getId(), device, Resource.DEVICE);
Device savedDevice = deviceService.saveDeviceWithCredentials(device, credentials);
checkNotNull(savedDevice);
tbClusterService.onDeviceUpdated(savedDevice, device);
logEntityAction(savedDevice.getId(), savedDevice,
savedDevice.getCustomerId(),
device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
return savedDevice;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.DEVICE), device,
null, created ? ActionType.ADDED : ActionType.UPDATED, e);
throw handleException(e);
}
}
private void onDeviceCreatedOrUpdated(Device savedDevice, Device oldDevice, boolean updated, SecurityUser user) {
@ -492,7 +526,7 @@ public class DeviceController extends BaseController {
@ResponseBody
public PageData<Device> getCustomerDevices(
@ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("customerId") String strCustomerId,
@PathVariable(CUSTOMER_ID) String strCustomerId,
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)

130
application/src/main/java/org/thingsboard/server/controller/EntityViewController.java

@ -21,6 +21,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
@ -75,10 +76,27 @@ import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_RECEIVE_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_UNASSIGN_RECEIVE_STEP_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_INFO_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_INFO_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_VIEW_TYPE;
import static org.thingsboard.server.controller.ControllerConstants.MODEL_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
/**
@ -95,10 +113,16 @@ public class EntityViewController extends BaseController {
@Autowired
private TimeseriesService tsService;
@ApiOperation(value = "Get entity view (getEntityViewById)",
notes = "Fetch the EntityView object based on the provided entity view id. "
+ ENTITY_VIEW_DESCRIPTION + MODEL_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entityView/{entityViewId}", method = RequestMethod.GET)
@ResponseBody
public EntityView getEntityViewById(@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
public EntityView getEntityViewById(
@ApiParam(value = ENTITY_VIEW_ID_PARAM_DESCRIPTION)
@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
checkParameter(ENTITY_VIEW_ID, strEntityViewId);
try {
return checkEntityViewId(new EntityViewId(toUUID(strEntityViewId)), Operation.READ);
@ -107,10 +131,16 @@ public class EntityViewController extends BaseController {
}
}
@ApiOperation(value = "Get Entity View info (getEntityViewInfoById)",
notes = "Fetch the Entity View info object based on the provided Entity View Id. "
+ ENTITY_VIEW_INFO_DESCRIPTION + MODEL_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entityView/info/{entityViewId}", method = RequestMethod.GET)
@ResponseBody
public EntityViewInfo getEntityViewInfoById(@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
public EntityViewInfo getEntityViewInfoById(
@ApiParam(value = ENTITY_VIEW_ID_PARAM_DESCRIPTION)
@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
checkParameter(ENTITY_VIEW_ID, strEntityViewId);
try {
EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId));
@ -120,10 +150,15 @@ public class EntityViewController extends BaseController {
}
}
@ApiOperation(value = "Save or update entity view (saveEntityView)",
notes = ENTITY_VIEW_DESCRIPTION + MODEL_DESCRIPTION,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entityView", method = RequestMethod.POST)
@ResponseBody
public EntityView saveEntityView(@RequestBody EntityView entityView) throws ThingsboardException {
public EntityView saveEntityView(
@ApiParam(value = "A JSON object representing the entity view.")
@RequestBody EntityView entityView) throws ThingsboardException {
try {
entityView.setTenantId(getCurrentUser().getTenantId());
@ -357,10 +392,15 @@ public class EntityViewController extends BaseController {
keys);
}
@ApiOperation(value = "Delete entity view (deleteEntityView)",
notes = "Delete the EntityView object based on the provided entity view id. "
+ TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/entityView/{entityViewId}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK)
public void deleteEntityView(@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
public void deleteEntityView(
@ApiParam(value = ENTITY_VIEW_ID_PARAM_DESCRIPTION)
@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
checkParameter(ENTITY_VIEW_ID, strEntityViewId);
try {
EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId));
@ -382,10 +422,14 @@ public class EntityViewController extends BaseController {
}
}
@ApiOperation(value = "Get Entity View by name (getTenantEntityView)",
notes = "Fetch the Entity View object based on the tenant id and entity view name. " + TENANT_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/entityViews", params = {"entityViewName"}, method = RequestMethod.GET)
@ResponseBody
public EntityView getTenantEntityView(
@ApiParam(value = "Entity View name")
@RequestParam String entityViewName) throws ThingsboardException {
try {
TenantId tenantId = getCurrentUser().getTenantId();
@ -395,11 +439,16 @@ public class EntityViewController extends BaseController {
}
}
@ApiOperation(value = "Assign Entity View to customer (assignEntityViewToCustomer)",
notes = "Creates assignment of the Entity View to customer. Customer will be able to query Entity View afterwards." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/{customerId}/entityView/{entityViewId}", method = RequestMethod.POST)
@ResponseBody
public EntityView assignEntityViewToCustomer(@PathVariable(CUSTOMER_ID) String strCustomerId,
@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
public EntityView assignEntityViewToCustomer(
@ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION)
@PathVariable(CUSTOMER_ID) String strCustomerId,
@ApiParam(value = ENTITY_VIEW_ID_PARAM_DESCRIPTION)
@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
checkParameter(CUSTOMER_ID, strCustomerId);
checkParameter(ENTITY_VIEW_ID, strEntityViewId);
try {
@ -426,10 +475,14 @@ public class EntityViewController extends BaseController {
}
}
@ApiOperation(value = "Unassign Entity View from customer (unassignEntityViewFromCustomer)",
notes = "Clears assignment of the Entity View to customer. Customer will not be able to query Entity View afterwards." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/entityView/{entityViewId}", method = RequestMethod.DELETE)
@ResponseBody
public EntityView unassignEntityViewFromCustomer(@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
public EntityView unassignEntityViewFromCustomer(
@ApiParam(value = ENTITY_VIEW_ID_PARAM_DESCRIPTION)
@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
checkParameter(ENTITY_VIEW_ID, strEntityViewId);
try {
EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId));
@ -455,18 +508,28 @@ public class EntityViewController extends BaseController {
}
}
@ApiOperation(value = "Get Customer Entity Views (getCustomerEntityViews)",
notes = "Returns a page of Entity View objects assigned to customer. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/entityViews", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<EntityView> getCustomerEntityViews(
@PathVariable("customerId") String strCustomerId,
@ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(CUSTOMER_ID) String strCustomerId,
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = ENTITY_VIEW_TYPE)
@RequestParam(required = false) String type,
@ApiParam(value = ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ENTITY_VIEW_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
checkParameter("customerId", strCustomerId);
checkParameter(CUSTOMER_ID, strCustomerId);
try {
TenantId tenantId = getCurrentUser().getTenantId();
CustomerId customerId = new CustomerId(toUUID(strCustomerId));
@ -482,16 +545,26 @@ public class EntityViewController extends BaseController {
}
}
@ApiOperation(value = "Get Customer Entity View info (getCustomerEntityViewInfos)",
notes = "Returns a page of Entity View info objects assigned to customer. " + ENTITY_VIEW_DESCRIPTION +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/customer/{customerId}/entityViewInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<EntityViewInfo> getCustomerEntityViewInfos(
@PathVariable("customerId") String strCustomerId,
@ApiParam(value = CUSTOMER_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(CUSTOMER_ID) String strCustomerId,
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = ENTITY_VIEW_TYPE)
@RequestParam(required = false) String type,
@ApiParam(value = ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ENTITY_VIEW_INFO_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
checkParameter("customerId", strCustomerId);
try {
@ -509,15 +582,24 @@ public class EntityViewController extends BaseController {
}
}
@ApiOperation(value = "Get Tenant Entity Views (getTenantEntityViews)",
notes = "Returns a page of entity views owned by tenant. " + ENTITY_VIEW_DESCRIPTION +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/entityViews", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<EntityView> getTenantEntityViews(
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = ENTITY_VIEW_TYPE)
@RequestParam(required = false) String type,
@ApiParam(value = ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ENTITY_VIEW_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
try {
TenantId tenantId = getCurrentUser().getTenantId();
@ -533,15 +615,24 @@ public class EntityViewController extends BaseController {
}
}
@ApiOperation(value = "Get Tenant Entity Views (getTenantEntityViews)",
notes = "Returns a page of entity views info owned by tenant. " + ENTITY_VIEW_DESCRIPTION +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/tenant/entityViewInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<EntityViewInfo> getTenantEntityViewInfos(
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = ENTITY_VIEW_TYPE)
@RequestParam(required = false) String type,
@ApiParam(value = ENTITY_VIEW_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ENTITY_VIEW_INFO_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
try {
TenantId tenantId = getCurrentUser().getTenantId();
@ -556,10 +647,16 @@ public class EntityViewController extends BaseController {
}
}
@ApiOperation(value = "Find related entity views (findByQuery)",
notes = "Returns all entity views that are related to the specific entity. " +
"The entity id, relation type, entity view types, depth of the search, and other query parameters defined using complex 'EntityViewSearchQuery' object. " +
"See 'Model' tab of the Parameters for more info." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entityViews", method = RequestMethod.POST)
@ResponseBody
public List<EntityView> findByQuery(@RequestBody EntityViewSearchQuery query) throws ThingsboardException {
public List<EntityView> findByQuery(
@ApiParam(value = "The entity view search query JSON")
@RequestBody EntityViewSearchQuery query) throws ThingsboardException {
checkNotNull(query);
checkNotNull(query.getParameters());
checkNotNull(query.getEntityViewTypes());
@ -580,6 +677,9 @@ public class EntityViewController extends BaseController {
}
}
@ApiOperation(value = "Get Entity View Types (getEntityViewTypes)",
notes = "Returns a set of unique entity view types based on entity views that are either owned by the tenant or assigned to the customer which user is performing the request."
+ TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/entityView/types", method = RequestMethod.GET)
@ResponseBody
@ -594,10 +694,16 @@ public class EntityViewController extends BaseController {
}
}
@ApiOperation(value = "Make entity view publicly available (assignEntityViewToPublicCustomer)",
notes = "Entity View will be available for non-authorized (not logged-in) users. " +
"This is useful to create dashboards that you plan to share/embed on a publicly available website. " +
"However, users that are logged-in and belong to different tenant will not be able to access the entity view." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/customer/public/entityView/{entityViewId}", method = RequestMethod.POST)
@ResponseBody
public EntityView assignEntityViewToPublicCustomer(@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
public EntityView assignEntityViewToPublicCustomer(
@ApiParam(value = ENTITY_VIEW_ID_PARAM_DESCRIPTION)
@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
checkParameter(ENTITY_VIEW_ID, strEntityViewId);
try {
EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId));

46
application/src/main/java/org/thingsboard/server/controller/Lwm2mController.java

@ -16,7 +16,10 @@
package org.thingsboard.server.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
@ -25,26 +28,39 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.lwm2m.ServerSecurityConfig;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.Map;
import static org.thingsboard.server.controller.ControllerConstants.IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
@Slf4j
@RestController
@TbCoreComponent
@RequestMapping("/api")
public class Lwm2mController extends BaseController {
@Autowired
private DeviceController deviceController;
public static final String IS_BOOTSTRAP_SERVER = "isBootstrapServer";
@ApiOperation(value = "Get Lwm2m Bootstrap SecurityInfo (getLwm2mBootstrapSecurityInfo)",
notes = "Get the Lwm2m Bootstrap SecurityInfo object (of the current server) based on the provided isBootstrapServer parameter. If isBootstrapServer == true, get the parameters of the current Bootstrap Server. If isBootstrapServer == false, get the parameters of the current Lwm2m Server. Used for client settings when starting the client in Bootstrap mode. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = "application/json")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/lwm2m/deviceProfile/bootstrap/{isBootstrapServer}", method = RequestMethod.GET)
@ResponseBody
public ServerSecurityConfig getLwm2mBootstrapSecurityInfo(@PathVariable("isBootstrapServer") boolean bootstrapServer) throws ThingsboardException {
public ServerSecurityConfig getLwm2mBootstrapSecurityInfo(
@ApiParam(value = IS_BOOTSTRAP_SERVER_PARAM_DESCRIPTION)
@PathVariable(IS_BOOTSTRAP_SERVER) boolean bootstrapServer) throws ThingsboardException {
try {
return lwM2MServerSecurityInfoRepository.getServerSecurityInfo(bootstrapServer);
} catch (Exception e) {
@ -52,28 +68,14 @@ public class Lwm2mController extends BaseController {
}
}
@ApiOperation(hidden = true, value = "Save device with credentials (Deprecated)")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/lwm2m/device-credentials", method = RequestMethod.POST)
@ResponseBody
public Device saveDeviceWithCredentials(@RequestBody (required=false) Map<Class<?>, Object> deviceWithDeviceCredentials) throws ThingsboardException {
public Device saveDeviceWithCredentials(@RequestBody Map<Class<?>, Object> deviceWithDeviceCredentials) throws ThingsboardException {
ObjectMapper mapper = new ObjectMapper();
Device device = checkNotNull(mapper.convertValue(deviceWithDeviceCredentials.get(Device.class), Device.class));
DeviceCredentials credentials = checkNotNull(mapper.convertValue( deviceWithDeviceCredentials.get(DeviceCredentials.class), DeviceCredentials.class));
try {
device.setTenantId(getCurrentUser().getTenantId());
checkEntity(device.getId(), device, Resource.DEVICE);
Device savedDevice = deviceService.saveDeviceWithCredentials(device, credentials);
checkNotNull(savedDevice);
tbClusterService.onDeviceUpdated(savedDevice, device);
logEntityAction(savedDevice.getId(), savedDevice,
savedDevice.getCustomerId(),
device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
return savedDevice;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.DEVICE), device,
null, device.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
throw handleException(e);
}
DeviceCredentials credentials = checkNotNull(mapper.convertValue(deviceWithDeviceCredentials.get(DeviceCredentials.class), DeviceCredentials.class));
return deviceController.saveDeviceWithCredentials(new SaveDeviceWithCredentialsRequest(device, credentials));
}
}

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

@ -52,8 +52,22 @@ import javax.annotation.Nullable;
import java.util.UUID;
import static org.thingsboard.server.common.data.DataConstants.RPC_DELETED;
import static org.thingsboard.server.controller.ControllerConstants.*;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.RPC_ID;
import static org.thingsboard.server.controller.ControllerConstants.RPC_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.RPC_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.RPC_STATUS_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.RPC_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
@RestController
@TbCoreComponent

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

@ -15,9 +15,7 @@
*/
package org.thingsboard.server.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Function;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
@ -46,6 +44,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg;
import org.thingsboard.server.common.data.DataConstants;
@ -90,7 +89,6 @@ import org.thingsboard.server.service.telemetry.exception.UncheckedApiException;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
@ -103,12 +101,41 @@ import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.thingsboard.server.controller.ControllerConstants.ATTRIBUTES_JSON_REQUEST_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ATTRIBUTES_KEYS_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ATTRIBUTES_SCOPE_ALLOWED_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.ATTRIBUTES_SCOPE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ATTRIBUTE_DATA_EXAMPLE;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ATTRIBUTE_SCOPES;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.INVALID_STRUCTURE_OF_THE_REQUEST;
import static org.thingsboard.server.controller.ControllerConstants.LATEST_TS_NON_STRICT_DATA_EXAMPLE;
import static org.thingsboard.server.controller.ControllerConstants.LATEST_TS_STRICT_DATA_EXAMPLE;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START;
import static org.thingsboard.server.controller.ControllerConstants.SAVE_ATTIRIBUTES_STATUS_BAD_REQUEST;
import static org.thingsboard.server.controller.ControllerConstants.SAVE_ATTIRIBUTES_STATUS_OK;
import static org.thingsboard.server.controller.ControllerConstants.SAVE_ATTRIBUTES_REQUEST_PAYLOAD;
import static org.thingsboard.server.controller.ControllerConstants.SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR;
import static org.thingsboard.server.controller.ControllerConstants.SAVE_ENTITY_ATTRIBUTES_STATUS_OK;
import static org.thingsboard.server.controller.ControllerConstants.SAVE_ENTITY_ATTRIBUTES_STATUS_UNAUTHORIZED;
import static org.thingsboard.server.controller.ControllerConstants.SAVE_ENTITY_TIMESERIES_STATUS_INTERNAL_SERVER_ERROR;
import static org.thingsboard.server.controller.ControllerConstants.SAVE_ENTITY_TIMESERIES_STATUS_OK;
import static org.thingsboard.server.controller.ControllerConstants.SAVE_ENTITY_TIMESERIES_STATUS_UNAUTHORIZED;
import static org.thingsboard.server.controller.ControllerConstants.SAVE_TIMESERIES_REQUEST_PAYLOAD;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.STRICT_DATA_TYPES_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TELEMETRY_JSON_REQUEST_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TELEMETRY_KEYS_BASE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TELEMETRY_KEYS_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TELEMETRY_SCOPE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TS_STRICT_DATA_EXAMPLE;
/**
@ -120,48 +147,6 @@ import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CU
@Slf4j
public class TelemetryController extends BaseController {
private static final String ATTRIBUTES_SCOPE_DESCRIPTION = "A string value representing the attributes scope. For example, 'SERVER_SCOPE'.";
private static final String ATTRIBUTES_KEYS_DESCRIPTION = "A string value representing the comma-separated list of attributes keys. For example, 'active,inactivityAlarmTime'.";
private static final String ATTRIBUTES_SCOPE_ALLOWED_VALUES = "SERVER_SCOPE, CLIENT_SCOPE, SHARED_SCOPE";
private static final String ATTRIBUTES_JSON_REQUEST_DESCRIPTION = "A string value representing the json object. For example, '{\"key\":\"value\"}'";
private static final String ATTRIBUTE_DATA_CLASS_DESCRIPTION = "AttributeData class represents information regarding a particular attribute and includes the next parameters: 'lastUpdatesTs' - a long value representing the timestamp of the last attribute modification in milliseconds. 'key' - attribute key name, and 'value' - attribute value.";
private static final String GET_ALL_ATTRIBUTES_BASE_DESCRIPTION = "Returns a JSON structure that represents a list of AttributeData class objects for the selected entity based on the specified comma-separated list of attribute key names. " + ATTRIBUTE_DATA_CLASS_DESCRIPTION;
private static final String GET_ALL_ATTRIBUTES_BY_SCOPE_BASE_DESCRIPTION = "Returns a JSON structure that represents a list of AttributeData class objects for the selected entity based on the attributes scope selected and a comma-separated list of attribute key names. " + ATTRIBUTE_DATA_CLASS_DESCRIPTION;
private static final String TS_DATA_CLASS_DESCRIPTION = "TsData class is a timeseries data point for specific telemetry key that includes 'value' - object value, and 'ts' - a long value representing timestamp in milliseconds for this value. ";
private static final String TELEMETRY_KEYS_BASE_DESCRIPTION = "A string value representing the comma-separated list of telemetry keys.";
private static final String TELEMETRY_KEYS_DESCRIPTION = TELEMETRY_KEYS_BASE_DESCRIPTION + " If keys are not selected, the result will return all latest timeseries. For example, 'temp,humidity'.";
private static final String TELEMETRY_SCOPE_DESCRIPTION = "Value is not used in the API call implementation. However, you need to specify whatever value cause scope is a path variable.";
private static final String TELEMETRY_JSON_REQUEST_DESCRIPTION = "A string value representing the json object. For example, '{\"key\":\"value\"}' or '{\"ts\":1527863043000,\"values\":{\"key1\":\"value1\",\"key2\":\"value2\"}}' or [{\"ts\":1527863043000,\"values\":{\"key1\":\"value1\",\"key2\":\"value2\"}}, {\"ts\":1527863053000,\"values\":{\"key1\":\"value3\",\"key2\":\"value4\"}}]";
private static final String STRICT_DATA_TYPES_DESCRIPTION = "A boolean value to specify if values of selected telemetry keys will represent string values(by default) or use strict data type.";
private static final String INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION = "Referencing a non-existing entity Id or invalid entity type will cause an error. ";
private static final String SAVE_ENTITY_ATTRIBUTES_DESCRIPTION = "Creates or updates the entity attributes based on entity id, entity type, specified attributes scope " +
"and request payload that represents a JSON object with key-value format of attributes to create or update. " +
"For example, '{\"temperature\": 26}'. Key is a unique parameter and cannot be overwritten. Only value can be overwritten for the key. ";
private static final String SAVE_ATTIRIBUTES_STATUS_OK = "Attribute from the request was created or updated. ";
private static final String INVALID_STRUCTURE_OF_THE_REQUEST = "Invalid structure of the request";
private static final String SAVE_ATTIRIBUTES_STATUS_BAD_REQUEST = INVALID_STRUCTURE_OF_THE_REQUEST + " or invalid attributes scope provided.";
private static final String SAVE_ENTITY_ATTRIBUTES_STATUS_OK = "Platform creates an audit log event about entity attributes updates with action type 'ATTRIBUTES_UPDATED', " +
"and also sends event msg to the rule engine with msg type 'ATTRIBUTES_UPDATED'.";
private static final String SAVE_ENTITY_ATTRIBUTES_STATUS_UNAUTHORIZED = "User is not authorized to save entity attributes for selected entity. Most likely, User belongs to different Customer or Tenant.";
private static final String SAVE_ENTITY_ATTRIBUTES_STATUS_INTERNAL_SERVER_ERROR = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about entity attributes updates with action type 'ATTRIBUTES_UPDATED' that includes an error stacktrace.";
private static final String SAVE_ENTITY_TIMESERIES_DESCRIPTION = "Creates or updates the entity timeseries based on entity id, entity type " +
"and request payload that represents a JSON object with key-value or ts-values format. " +
"For example, '{\"temperature\": 26}' or '{\"ts\":1634712287000,\"values\":{\"temperature\":26, \"humidity\":87}}', " +
"or JSON array with inner objects inside of ts-values format. " +
"For example, '[{\"ts\":1634712287000,\"values\":{\"temperature\":26, \"humidity\":87}}, {\"ts\":1634712588000,\"values\":{\"temperature\":25, \"humidity\":88}}]'. " +
"The scope parameter is not used in the API call implementation but should be specified whatever value because it is used as a path variable. ";
private static final String SAVE_ENTITY_TIMESERIES_STATUS_OK = "Timeseries from the request was created or updated. " +
"Platform creates an audit log event about entity timeseries updates with action type 'TIMESERIES_UPDATED'.";
private static final String SAVE_ENTITY_TIMESERIES_STATUS_UNAUTHORIZED = "User is not authorized to save entity timeseries for selected entity. Most likely, User belongs to different Customer or Tenant.";
private static final String SAVE_ENTITY_TIMESERIES_STATUS_INTERNAL_SERVER_ERROR = "The exception was thrown during processing the request. " +
"Platform creates an audit log event about entity timeseries updates with action type 'TIMESERIES_UPDATED' that includes an error stacktrace.";
@Autowired
private TimeseriesService tsService;
@ -173,8 +158,6 @@ public class TelemetryController extends BaseController {
private ExecutorService executor;
private static final ObjectMapper mapper = new ObjectMapper();
@PostConstruct
public void initExecutor() {
executor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("telemetry-controller"));
@ -188,96 +171,117 @@ public class TelemetryController extends BaseController {
}
@ApiOperation(value = "Get all attribute keys (getAttributeKeys)",
notes = "Returns a list of all attribute key names for the selected entity. " +
"In the case of device entity specified, a response will include merged attribute key names list from each scope: " +
"SERVER_SCOPE, CLIENT_SCOPE, SHARED_SCOPE. "
+ INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
notes = "Returns a set of unique attribute key names for the selected entity. " +
"The response will include merged key names set for all attribute scopes:" +
"\n\n * SERVER_SCOPE - supported for all entity types;" +
"\n * CLIENT_SCOPE - supported for devices;" +
"\n * SHARED_SCOPE - supported for devices. "
+ "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/keys/attributes", method = RequestMethod.GET)
@ResponseBody
public DeferredResult<ResponseEntity> getAttributeKeys(
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION) @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION) @PathVariable("entityId") String entityIdStr) throws ThingsboardException {
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr) throws ThingsboardException {
return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr, this::getAttributeKeysCallback);
}
@ApiOperation(value = "Get all attributes keys by scope (getAttributeKeysByScope)",
notes = "Returns a list of attribute key names from the specified attributes scope for the selected entity. " +
"If scope parameter is omitted, Get all attribute keys(getAttributeKeys) API will be called. "
+ INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
@ApiOperation(value = "Get all attribute keys by scope (getAttributeKeysByScope)",
notes = "Returns a set of unique attribute key names for the selected entity and attributes scope: " +
"\n\n * SERVER_SCOPE - supported for all entity types;" +
"\n * CLIENT_SCOPE - supported for devices;" +
"\n * SHARED_SCOPE - supported for devices. "
+ "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/keys/attributes/{scope}", method = RequestMethod.GET)
@ResponseBody
public DeferredResult<ResponseEntity> getAttributeKeysByScope(
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION) @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES) @PathVariable("scope") String scope) throws ThingsboardException {
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, required = true, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES) @PathVariable("scope") String scope) throws ThingsboardException {
return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr,
(result, tenantId, entityId) -> getAttributeKeysCallback(result, tenantId, entityId, scope));
}
@ApiOperation(value = "Get attributes (getAttributes)",
notes = GET_ALL_ATTRIBUTES_BASE_DESCRIPTION + " If 'keys' parameter is omitted, AttributeData class objects will be added to the response for all existing keys of the selected entity. " +
INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
notes = "Returns all attributes that belong to specified entity. Use optional 'keys' parameter to return specific attributes."
+ "\n Example of the result: \n\n"
+ MARKDOWN_CODE_BLOCK_START
+ ATTRIBUTE_DATA_EXAMPLE
+ MARKDOWN_CODE_BLOCK_END
+ "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/values/attributes", method = RequestMethod.GET)
@ResponseBody
public DeferredResult<ResponseEntity> getAttributes(
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION) @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException {
SecurityUser user = getCurrentUser();
return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr,
(result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, null, keysStr));
}
@ApiOperation(value = "Get attributes by scope (getAttributesByScope)",
notes = GET_ALL_ATTRIBUTES_BY_SCOPE_BASE_DESCRIPTION + " In case that 'keys' parameter is not selected, " +
"AttributeData class objects will be added to the response for all existing attribute keys from the " +
"specified attributes scope of the selected entity. If 'scope' parameter is omitted, " +
"Get attributes (getAttributes) API will be called. " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
notes = "Returns all attributes of a specified scope that belong to specified entity." +
ENTITY_ATTRIBUTE_SCOPES +
"Use optional 'keys' parameter to return specific attributes."
+ "\n Example of the result: \n\n"
+ MARKDOWN_CODE_BLOCK_START
+ ATTRIBUTE_DATA_EXAMPLE
+ MARKDOWN_CODE_BLOCK_END
+ "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/values/attributes/{scope}", method = RequestMethod.GET)
@ResponseBody
public DeferredResult<ResponseEntity> getAttributesByScope(
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION) @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES) @PathVariable("scope") String scope,
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES, required = true) @PathVariable("scope") String scope,
@ApiParam(value = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr) throws ThingsboardException {
SecurityUser user = getCurrentUser();
return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_ATTRIBUTES, entityType, entityIdStr,
(result, tenantId, entityId) -> getAttributeValuesCallback(result, user, entityId, scope, keysStr));
}
@ApiOperation(value = "Get timeseries keys (getTimeseriesKeys)",
notes = "Returns a list of all telemetry key names for the selected entity based on entity id and entity type specified. " +
INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
@ApiOperation(value = "Get time-series keys (getTimeseriesKeys)",
notes = "Returns a set of unique time-series key names for the selected entity. " +
"\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/keys/timeseries", method = RequestMethod.GET)
@ResponseBody
public DeferredResult<ResponseEntity> getTimeseriesKeys(
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION) @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION) @PathVariable("entityId") String entityIdStr) throws ThingsboardException {
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr) throws ThingsboardException {
return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr,
(result, tenantId, entityId) -> Futures.addCallback(tsService.findAllLatest(tenantId, entityId), getTsKeysToResponseCallback(result), MoreExecutors.directExecutor()));
}
@ApiOperation(value = "Get latest timeseries (getLatestTimeseries)",
notes = "Returns a JSON structure that represents a Map, where the map key is a telemetry key name " +
"and map value - is a singleton list of TsData class objects. "
+ TS_DATA_CLASS_DESCRIPTION + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
@ApiOperation(value = "Get latest time-series value (getLatestTimeseries)",
notes = "Returns all time-series that belong to specified entity. Use optional 'keys' parameter to return specific time-series." +
" The result is a JSON object. The format of the values depends on the 'useStrictDataTypes' parameter." +
" By default, all time-series values are converted to strings: \n\n"
+ MARKDOWN_CODE_BLOCK_START
+ LATEST_TS_NON_STRICT_DATA_EXAMPLE
+ MARKDOWN_CODE_BLOCK_END
+ "\n\n However, it is possible to request the values without conversion ('useStrictDataTypes'=true): \n\n"
+ MARKDOWN_CODE_BLOCK_START
+ LATEST_TS_STRICT_DATA_EXAMPLE
+ MARKDOWN_CODE_BLOCK_END
+ "\n\n " + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET)
@ResponseBody
public DeferredResult<ResponseEntity> getLatestTimeseries(
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION) @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = TELEMETRY_KEYS_DESCRIPTION) @RequestParam(name = "keys", required = false) String keysStr,
@ApiParam(value = STRICT_DATA_TYPES_DESCRIPTION)
@RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException {
@ -286,24 +290,26 @@ public class TelemetryController extends BaseController {
(result, tenantId, entityId) -> getLatestTimeseriesValuesCallback(result, user, entityId, keysStr, useStrictDataTypes));
}
@ApiOperation(value = "Get timeseries (getTimeseries)",
notes = "Returns a JSON structure that represents a Map, where the map key is a telemetry key name " +
"and map value - is a list of TsData class objects. " + TS_DATA_CLASS_DESCRIPTION +
"This method allows us to group original data into intervals and aggregate it using one of the aggregation methods or just limit the number of TsData objects to fetch for each key specified. " +
"See the desription of the request parameters for more details. " +
"The result can also be sorted in ascending or descending order. "
+ INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
@ApiOperation(value = "Get time-series data (getTimeseries)",
notes = "Returns a range of time-series values for specified entity. " +
"Returns not aggregated data by default. " +
"Use aggregation function ('agg') and aggregation interval ('interval') to enable aggregation of the results on the database / server side. " +
"The aggregation is generally more efficient then fetching all records. \n\n"
+ MARKDOWN_CODE_BLOCK_START
+ TS_STRICT_DATA_EXAMPLE
+ MARKDOWN_CODE_BLOCK_END
+ "\n\n" + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET, params = {"keys", "startTs", "endTs"})
@ResponseBody
public DeferredResult<ResponseEntity> getTimeseries(
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION) @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = TELEMETRY_KEYS_BASE_DESCRIPTION) @RequestParam(name = "keys") String keys,
@ApiParam(value = "A long value representing the start timestamp of search time range in milliseconds.")
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = TELEMETRY_KEYS_BASE_DESCRIPTION, required = true) @RequestParam(name = "keys") String keys,
@ApiParam(value = "A long value representing the start timestamp of the time range in milliseconds, UTC.")
@RequestParam(name = "startTs") Long startTs,
@ApiParam(value = "A long value representing the end timestamp of search time range in milliseconds.")
@ApiParam(value = "A long value representing the end timestamp of the time range in milliseconds, UTC.")
@RequestParam(name = "endTs") Long endTs,
@ApiParam(value = "A long value representing the aggregation interval range in milliseconds.")
@RequestParam(name = "interval", defaultValue = "0") Long interval,
@ -329,11 +335,10 @@ public class TelemetryController extends BaseController {
});
}
@ApiOperation(value = "Save or update device attributes (saveDeviceAttributes)",
notes = "Creates or updates the device attributes based on device id, specified attribute scope, " +
"and request payload that represents a JSON object with key-value format of attributes to create or update. " +
"For example, '{\"temperature\": 26}'. Key is a unique parameter and cannot be overwritten. Only value can " +
"be overwritten for the key. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
@ApiOperation(value = "Save device attributes (saveDeviceAttributes)",
notes = "Creates or updates the device attributes based on device id and specified attribute scope. " +
SAVE_ATTRIBUTES_REQUEST_PAYLOAD
+ TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@ApiResponses(value = {
@ApiResponse(code = 200, message = SAVE_ATTIRIBUTES_STATUS_OK +
@ -348,15 +353,18 @@ public class TelemetryController extends BaseController {
@RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> saveDeviceAttributes(
@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) @PathVariable("deviceId") String deviceIdStr,
@ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES) @PathVariable("scope") String scope,
@ApiParam(value = ATTRIBUTES_JSON_REQUEST_DESCRIPTION) @RequestBody JsonNode request) throws ThingsboardException {
@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable("deviceId") String deviceIdStr,
@ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES, required = true) @PathVariable("scope") String scope,
@ApiParam(value = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr);
return saveAttributes(getTenantId(), entityId, scope, request);
}
@ApiOperation(value = "Save or update attributes (saveEntityAttributesV1)",
notes = SAVE_ENTITY_ATTRIBUTES_DESCRIPTION + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
@ApiOperation(value = "Save entity attributes (saveEntityAttributesV1)",
notes = "Creates or updates the entity attributes based on Entity Id and the specified attribute scope. " +
ENTITY_ATTRIBUTE_SCOPES +
SAVE_ATTRIBUTES_REQUEST_PAYLOAD
+ INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@ApiResponses(value = {
@ApiResponse(code = 200, message = SAVE_ATTIRIBUTES_STATUS_OK + SAVE_ENTITY_ATTRIBUTES_STATUS_OK),
@ -368,16 +376,19 @@ public class TelemetryController extends BaseController {
@RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> saveEntityAttributesV1(
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION) @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES) @PathVariable("scope") String scope,
@ApiParam(value = ATTRIBUTES_JSON_REQUEST_DESCRIPTION) @RequestBody JsonNode request) throws ThingsboardException {
@ApiParam(value = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return saveAttributes(getTenantId(), entityId, scope, request);
}
@ApiOperation(value = "Save or update attributes (saveEntityAttributesV2)",
notes = SAVE_ENTITY_ATTRIBUTES_DESCRIPTION + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
@ApiOperation(value = "Save entity attributes (saveEntityAttributesV2)",
notes = "Creates or updates the entity attributes based on Entity Id and the specified attribute scope. " +
ENTITY_ATTRIBUTE_SCOPES +
SAVE_ATTRIBUTES_REQUEST_PAYLOAD
+ INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@ApiResponses(value = {
@ApiResponse(code = 200, message = SAVE_ATTIRIBUTES_STATUS_OK + SAVE_ENTITY_ATTRIBUTES_STATUS_OK),
@ -389,16 +400,20 @@ public class TelemetryController extends BaseController {
@RequestMapping(value = "/{entityType}/{entityId}/attributes/{scope}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> saveEntityAttributesV2(
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION) @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES) @PathVariable("scope") String scope,
@ApiParam(value = ATTRIBUTES_JSON_REQUEST_DESCRIPTION) @RequestBody JsonNode request) throws ThingsboardException {
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES, required = true) @PathVariable("scope") String scope,
@ApiParam(value = ATTRIBUTES_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody JsonNode request) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return saveAttributes(getTenantId(), entityId, scope, request);
}
@ApiOperation(value = "Save or update telemetry (saveEntityTelemetry)",
notes = SAVE_ENTITY_TIMESERIES_DESCRIPTION + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
@ApiOperation(value = "Save or update time-series data (saveEntityTelemetry)",
notes = "Creates or updates the entity time-series data based on the Entity Id and request payload." +
SAVE_TIMESERIES_REQUEST_PAYLOAD +
"\n\n The scope parameter is not used in the API call implementation but should be specified whatever value because it is used as a path variable. "
+ INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@ApiResponses(value = {
@ApiResponse(code = 200, message = SAVE_ENTITY_TIMESERIES_STATUS_OK),
@ -410,16 +425,19 @@ public class TelemetryController extends BaseController {
@RequestMapping(value = "/{entityType}/{entityId}/timeseries/{scope}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> saveEntityTelemetry(
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION) @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = TELEMETRY_SCOPE_DESCRIPTION) @PathVariable("scope") String scope,
@ApiParam(value = TELEMETRY_JSON_REQUEST_DESCRIPTION) @RequestBody String requestBody) throws ThingsboardException {
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = TELEMETRY_SCOPE_DESCRIPTION, required = true, allowableValues = "ANY") @PathVariable("scope") String scope,
@ApiParam(value = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody String requestBody) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return saveTelemetry(getTenantId(), entityId, requestBody, 0L);
}
@ApiOperation(value = "Save or update telemetry with TTL (saveEntityTelemetryWithTTL)",
notes = SAVE_ENTITY_TIMESERIES_DESCRIPTION + "The ttl parameter used only in case of Cassandra DB use for timeseries data storage. "
@ApiOperation(value = "Save or update time-series data with TTL (saveEntityTelemetryWithTTL)",
notes = "Creates or updates the entity time-series data based on the Entity Id and request payload." +
SAVE_TIMESERIES_REQUEST_PAYLOAD +
"\n\n The scope parameter is not used in the API call implementation but should be specified whatever value because it is used as a path variable. "
+ "\n\nThe ttl parameter takes affect only in case of Cassandra DB."
+ INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@ApiResponses(value = {
@ -432,19 +450,21 @@ public class TelemetryController extends BaseController {
@RequestMapping(value = "/{entityType}/{entityId}/timeseries/{scope}/{ttl}", method = RequestMethod.POST)
@ResponseBody
public DeferredResult<ResponseEntity> saveEntityTelemetryWithTTL(
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION) @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = TELEMETRY_SCOPE_DESCRIPTION) @PathVariable("scope") String scope,
@ApiParam(value = "A long value representing TTL (Time to Live) parameter.") @PathVariable("ttl") Long ttl,
@ApiParam(value = TELEMETRY_JSON_REQUEST_DESCRIPTION) @RequestBody String requestBody) throws ThingsboardException {
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = TELEMETRY_SCOPE_DESCRIPTION, required = true, allowableValues = "ANY") @PathVariable("scope") String scope,
@ApiParam(value = "A long value representing TTL (Time to Live) parameter.", required = true) @PathVariable("ttl") Long ttl,
@ApiParam(value = TELEMETRY_JSON_REQUEST_DESCRIPTION, required = true) @RequestBody String requestBody) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return saveTelemetry(getTenantId(), entityId, requestBody, ttl);
}
@ApiOperation(value = "Delete entity timeseries (deleteEntityTimeseries)",
notes = "Delete timeseries for selected entity based on entity id, entity type, keys " +
"and removal time range. To delete all data for keys parameter 'deleteAllDataForKeys' should be set to true, " +
"otherwise, will be deleted data that is in range of the selected time interval. " + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
@ApiOperation(value = "Delete entity time-series data (deleteEntityTimeseries)",
notes = "Delete time-series for selected entity based on entity id, entity type and keys." +
" Use 'deleteAllDataForKeys' to delete all time-series data." +
" Use 'startTs' and 'endTs' to specify time-range instead. " +
" Use 'rewriteLatestIfDeleted' to rewrite latest value (stored in separate table for performance) after deletion of the time range. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Timeseries for the selected keys in the request was removed. " +
@ -458,9 +478,9 @@ public class TelemetryController extends BaseController {
@RequestMapping(value = "/{entityType}/{entityId}/timeseries/delete", method = RequestMethod.DELETE)
@ResponseBody
public DeferredResult<ResponseEntity> deleteEntityTimeseries(
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION) @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = TELEMETRY_KEYS_DESCRIPTION) @RequestParam(name = "keys") String keysStr,
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = TELEMETRY_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr,
@ApiParam(value = "A boolean value to specify if should be deleted all data for selected keys or only data that are in the selected time range.")
@RequestParam(name = "deleteAllDataForKeys", defaultValue = "false") boolean deleteAllDataForKeys,
@ApiParam(value = "A long value representing the start timestamp of removal time range in milliseconds.")
@ -501,7 +521,7 @@ public class TelemetryController extends BaseController {
deleteTsKvQueries.add(new BaseDeleteTsKvQuery(key, deleteFromTs, deleteToTs, rewriteLatestIfDeleted));
}
ListenableFuture<List<Void>> future = tsService.remove(user.getTenantId(), entityId, deleteTsKvQueries);
Futures.addCallback(future, new FutureCallback<List<Void>>() {
Futures.addCallback(future, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable List<Void> tmp) {
logTimeseriesDeleted(user, entityId, keys, deleteFromTs, deleteToTs, null);
@ -517,9 +537,9 @@ public class TelemetryController extends BaseController {
});
}
@ApiOperation(value = "Delete device attributes (deleteEntityAttributes)",
notes = "Delete device attributes from the specified attributes scope based on device id and a list of keys to delete. " +
"Selected keys will be deleted only if there are exist in the specified attribute scope. Referencing a non-existing device Id will cause an error" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
@ApiOperation(value = "Delete device attributes (deleteDeviceAttributes)",
notes = "Delete device attributes using provided Device Id, scope and a list of keys. " +
"Referencing a non-existing Device Id will cause an error" + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Device attributes was removed for the selected keys in the request. " +
@ -532,17 +552,17 @@ public class TelemetryController extends BaseController {
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/{deviceId}/{scope}", method = RequestMethod.DELETE)
@ResponseBody
public DeferredResult<ResponseEntity> deleteEntityAttributes(
@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION) @PathVariable("deviceId") String deviceIdStr,
@ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES) @PathVariable("scope") String scope,
@ApiParam(value = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys") String keysStr) throws ThingsboardException {
public DeferredResult<ResponseEntity> deleteDeviceAttributes(
@ApiParam(value = DEVICE_ID_PARAM_DESCRIPTION, required = true) @PathVariable(DEVICE_ID) String deviceIdStr,
@ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES, required = true) @PathVariable("scope") String scope,
@ApiParam(value = ATTRIBUTES_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndUuid(EntityType.DEVICE, deviceIdStr);
return deleteAttributes(entityId, scope, keysStr);
}
@ApiOperation(value = "Delete entity attributes (deleteEntityAttributes)",
notes = "Delete entity attributes from the specified attributes scope based on entity id, entity type and a list of keys to delete. " +
"Selected keys will be deleted only if there are exist in the specified attribute scope." + INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
notes = "Delete entity attributes using provided Entity Id, scope and a list of keys. " +
INVALID_ENTITY_ID_OR_ENTITY_TYPE_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Entity attributes was removed for the selected keys in the request. " +
@ -556,10 +576,10 @@ public class TelemetryController extends BaseController {
@RequestMapping(value = "/{entityType}/{entityId}/{scope}", method = RequestMethod.DELETE)
@ResponseBody
public DeferredResult<ResponseEntity> deleteEntityAttributes(
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION) @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES) @PathVariable("scope") String scope,
@ApiParam(value = ATTRIBUTES_KEYS_DESCRIPTION) @RequestParam(name = "keys") String keysStr) throws ThingsboardException {
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE") @PathVariable("entityType") String entityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true) @PathVariable("entityId") String entityIdStr,
@ApiParam(value = ATTRIBUTES_SCOPE_DESCRIPTION, required = true, allowableValues = ATTRIBUTES_SCOPE_ALLOWED_VALUES) @PathVariable("scope") String scope,
@ApiParam(value = ATTRIBUTES_KEYS_DESCRIPTION, required = true) @RequestParam(name = "keys") String keysStr) throws ThingsboardException {
EntityId entityId = EntityIdFactory.getByTypeAndId(entityType, entityIdStr);
return deleteAttributes(entityId, scope, keysStr);
}
@ -732,7 +752,7 @@ public class TelemetryController extends BaseController {
}
private FutureCallback<List<TsKvEntry>> getTsKeysToResponseCallback(final DeferredResult<ResponseEntity> response) {
return new FutureCallback<List<TsKvEntry>>() {
return new FutureCallback<>() {
@Override
public void onSuccess(List<TsKvEntry> values) {
List<String> keys = values.stream().map(KvEntry::getKey).collect(Collectors.toList());
@ -767,7 +787,7 @@ public class TelemetryController extends BaseController {
private FutureCallback<List<AttributeKvEntry>> getAttributeValuesToResponseCallback(final DeferredResult<ResponseEntity> response,
final SecurityUser user, final String scope,
final EntityId entityId, final List<String> keyList) {
return new FutureCallback<List<AttributeKvEntry>>() {
return new FutureCallback<>() {
@Override
public void onSuccess(List<AttributeKvEntry> attributes) {
List<AttributeData> values = attributes.stream().map(attribute ->
@ -787,7 +807,7 @@ public class TelemetryController extends BaseController {
}
private FutureCallback<List<TsKvEntry>> getTsKvListCallback(final DeferredResult<ResponseEntity> response, Boolean useStrictDataTypes) {
return new FutureCallback<List<TsKvEntry>>() {
return new FutureCallback<>() {
@Override
public void onSuccess(List<TsKvEntry> data) {
Map<String, List<TsData>> result = new LinkedHashMap<>();
@ -907,16 +927,16 @@ public class TelemetryController extends BaseController {
private String toJsonStr(JsonNode value) {
try {
return mapper.writeValueAsString(value);
} catch (JsonProcessingException e) {
return JacksonUtil.toString(value);
} catch (IllegalArgumentException e) {
throw new JsonParseException("Can't parse jsonValue: " + value, e);
}
}
private JsonNode toJsonNode(String value) {
try {
return mapper.readTree(value);
} catch (IOException e) {
return JacksonUtil.toJsonNode(value);
} catch (IllegalArgumentException e) {
throw new JsonParseException("Can't parse jsonValue: " + value, e);
}
}

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

@ -40,7 +40,19 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import static org.thingsboard.server.controller.ControllerConstants.*;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_END;
import static org.thingsboard.server.controller.ControllerConstants.MARKDOWN_CODE_BLOCK_START;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_PROFILE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_PROFILE_INFO_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION;
@RestController
@TbCoreComponent

46
application/src/main/java/org/thingsboard/server/controller/UiSettingsController.java

@ -0,0 +1,46 @@
/**
* Copyright © 2016-2021 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 io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.queue.util.TbCoreComponent;
@RestController
@TbCoreComponent
@RequestMapping("/api")
public class UiSettingsController extends BaseController {
@Value("${ui.help.base-url}")
private String helpBaseUrl;
@ApiOperation(value = "Get UI help base url (getHelpBaseUrl)",
notes = "Get UI help base url used to fetch help assets. " +
"The actual value of the base url is configurable in the system configuration file.")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/uiSettings/helpBaseUrl", method = RequestMethod.GET)
@ResponseBody
public String getHelpBaseUrl() throws ThingsboardException {
return helpBaseUrl;
}
}

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

@ -62,7 +62,25 @@ import org.thingsboard.server.service.security.system.SystemSecurityService;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import static org.thingsboard.server.controller.ControllerConstants.*;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.DEFAULT_DASHBOARD;
import static org.thingsboard.server.controller.ControllerConstants.HOME_DASHBOARD;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.USER_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.USER_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.USER_TEXT_SEARCH_DESCRIPTION;
@RequiredArgsConstructor
@RestController

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

@ -43,7 +43,9 @@ import org.thingsboard.server.service.security.permission.Resource;
import java.util.List;
import static org.thingsboard.server.controller.ControllerConstants.*;
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.WIDGET_TYPE_ID_PARAM_DESCRIPTION;
@Slf4j
@RestController

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

@ -41,7 +41,17 @@ import org.thingsboard.server.service.security.permission.Resource;
import java.util.List;
import static org.thingsboard.server.controller.ControllerConstants.*;
import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.WIDGET_BUNDLE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.WIDGET_BUNDLE_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.WIDGET_BUNDLE_TEXT_SEARCH_DESCRIPTION;
@RestController
@TbCoreComponent

26
application/src/main/java/org/thingsboard/server/service/lwm2m/LwM2MServerSecurityInfoRepository.java

@ -22,28 +22,11 @@ import org.eclipse.leshan.core.util.Hex;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.lwm2m.ServerSecurityConfig;
import org.thingsboard.server.common.transport.config.ssl.SslCredentials;
import org.thingsboard.server.transport.lwm2m.config.LwM2MSecureServerConfig;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportBootstrapConfig;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
import java.math.BigInteger;
import java.security.AlgorithmParameters;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.KeySpec;
@Slf4j
@Service
@RequiredArgsConstructor
@ -72,10 +55,9 @@ public class LwM2MServerSecurityInfoRepository {
private String getPublicKey(LwM2MSecureServerConfig config) {
try {
KeyStore keyStore = serverConfig.getKeyStoreValue();
if (keyStore != null) {
X509Certificate serverCertificate = (X509Certificate) serverConfig.getKeyStoreValue().getCertificate(config.getCertificateAlias());
return Hex.encodeHexString(serverCertificate.getPublicKey().getEncoded());
SslCredentials sslCredentials = config.getSslCredentials();
if (sslCredentials != null) {
return Hex.encodeHexString(sslCredentials.getPublicKey().getEncoded());
}
} catch (Exception e) {
log.trace("Failed to fetch public key from key store!", e);

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

@ -129,10 +129,16 @@ usage:
check:
cycle: "${USAGE_STATS_CHECK_CYCLE:60000}"
# Dashboard parameters
dashboard:
# Maximum allowed datapoints fetched by widgets
max_datapoints_limit: "${DASHBOARD_MAX_DATAPOINTS_LIMIT:50000}"
# UI parameters
ui:
# Dashboard parameters
dashboard:
# Maximum allowed datapoints fetched by widgets
max_datapoints_limit: "${DASHBOARD_MAX_DATAPOINTS_LIMIT:50000}"
# Help parameters
help:
# Base url for UI help assets
base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.3.2}"
database:
ts_max_intervals: "${DATABASE_TS_MAX_INTERVALS:700}" # Max number of DB queries generated by single API call to fetch telemetry records
@ -613,14 +619,28 @@ transport:
bind_port: "${MQTT_SSL_BIND_PORT:8883}"
# SSL protocol: See http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#SSLContext
protocol: "${MQTT_SSL_PROTOCOL:TLSv1.2}"
# Path to the key store that holds the SSL certificate
key_store: "${MQTT_SSL_KEY_STORE:mqttserver.jks}"
# Password used to access the key store
key_store_password: "${MQTT_SSL_KEY_STORE_PASSWORD:server_ks_password}"
# Password used to access the key
key_password: "${MQTT_SSL_KEY_PASSWORD:server_key_password}"
# Type of the key store
key_store_type: "${MQTT_SSL_KEY_STORE_TYPE:JKS}"
# Server SSL credentials
credentials:
# Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore)
type: "${MQTT_SSL_CREDENTIALS_TYPE:PEM}"
# PEM server credentials
pem:
# Path to the server certificate file (holds server certificate or certificate chain, may include server private key)
cert_file: "${MQTT_SSL_PEM_CERT:mqttserver.pem}"
# Path to the server certificate private key file (optional)
key_file: "${MQTT_SSL_PEM_KEY:mqttserver_key.pem}"
# Server certificate private key password (optional)
key_password: "${MQTT_SSL_PEM_KEY_PASSWORD:server_key_password}"
# Keystore server credentials
keystore:
# Type of the key store
type: "${MQTT_SSL_KEY_STORE_TYPE:JKS}"
# Path to the key store that holds the SSL certificate
store_file: "${MQTT_SSL_KEY_STORE:mqttserver.jks}"
# Password used to access the key store
store_password: "${MQTT_SSL_KEY_STORE_PASSWORD:server_ks_password}"
# Password used to access the key
key_password: "${MQTT_SSL_KEY_PASSWORD:server_key_password}"
# Skip certificate validity check for client certificates.
skip_validity_check_for_client_cert: "${MQTT_SSL_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}"
# Local CoAP transport parameters
@ -639,14 +659,30 @@ transport:
bind_address: "${COAP_DTLS_BIND_ADDRESS:0.0.0.0}"
# CoAP DTLS bind port
bind_port: "${COAP_DTLS_BIND_PORT:5684}"
# Path to the key store that holds the certificate
key_store: "${COAP_DTLS_KEY_STORE:coapserver.jks}"
# Password used to access the key store
key_store_password: "${COAP_DTLS_KEY_STORE_PASSWORD:server_ks_password}"
# Password used to access the key
key_password: "${COAP_DTLS_KEY_PASSWORD:server_key_password}"
# Key alias
key_alias: "${COAP_DTLS_KEY_ALIAS:serveralias}"
# Server DTLS credentials
credentials:
# Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore)
type: "${COAP_DTLS_CREDENTIALS_TYPE:PEM}"
# PEM server credentials
pem:
# Path to the server certificate file (holds server certificate or certificate chain, may include server private key)
cert_file: "${COAP_DTLS_PEM_CERT:coapserver.pem}"
# Path to the server certificate private key file (optional)
key_file: "${COAP_DTLS_PEM_KEY:coapserver_key.pem}"
# Server certificate private key password (optional)
key_password: "${COAP_DTLS_PEM_KEY_PASSWORD:server_key_password}"
# Keystore server credentials
keystore:
# Type of the key store
type: "${COAP_DTLS_KEY_STORE_TYPE:JKS}"
# Path to the key store that holds the SSL certificate
store_file: "${COAP_DTLS_KEY_STORE:coapserver.jks}"
# Password used to access the key store
store_password: "${COAP_DTLS_KEY_STORE_PASSWORD:server_ks_password}"
# Password used to access the key
key_password: "${COAP_DTLS_KEY_PASSWORD:server_key_password}"
# Key alias
key_alias: "${COAP_DTLS_KEY_ALIAS:serveralias}"
x509:
# Skip certificate validity check for client certificates.
skip_validity_check_for_client_cert: "${TB_COAP_X509_DTLS_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}"
@ -663,9 +699,33 @@ transport:
security:
bind_address: "${LWM2M_SECURITY_BIND_ADDRESS:0.0.0.0}"
bind_port: "${LWM2M_SECURITY_BIND_PORT:5686}"
# Server X509 Certificates support
credentials:
# Whether to enable LWM2M server X509 Certificate/RPK support
enabled: "${LWM2M_SERVER_CREDENTIALS_ENABLED:false}"
# Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore)
type: "${LWM2M_SERVER_CREDENTIALS_TYPE:PEM}"
# PEM server credentials
pem:
# Path to the server certificate file (holds server certificate or certificate chain, may include server private key)
cert_file: "${LWM2M_SERVER_PEM_CERT:lwm2mserver.pem}"
# Path to the server certificate private key file (optional)
key_file: "${LWM2M_SERVER_PEM_KEY:lwm2mserver_key.pem}"
# Server certificate private key password (optional)
key_password: "${LWM2M_SERVER_PEM_KEY_PASSWORD:server_key_password}"
# Keystore server credentials
keystore:
# Type of the key store
type: "${LWM2M_SERVER_KEY_STORE_TYPE:JKS}"
# Path to the key store that holds the SSL certificate
store_file: "${LWM2M_SERVER_KEY_STORE:lwm2mserver.jks}"
# Password used to access the key store
store_password: "${LWM2M_SERVER_KEY_STORE_PASSWORD:server_ks_password}"
# Password used to access the key
key_password: "${LWM2M_SERVER_KEY_PASSWORD:server_key_password}"
# Key alias
key_alias: "${LWM2M_SERVER_KEY_ALIAS:server}"
# Only Certificate_x509:
key_alias: "${LWM2M_SERVER_KEY_ALIAS:server}"
key_password: "${LWM2M_SERVER_KEY_PASSWORD:server_ks_password}"
skip_validity_check_for_client_cert: "${TB_LWM2M_SERVER_SECURITY_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}"
bootstrap:
enable: "${LWM2M_ENABLED_BS:true}"
@ -675,18 +735,51 @@ transport:
security:
bind_address: "${LWM2M_BS_SECURITY_BIND_ADDRESS:0.0.0.0}"
bind_port: "${LWM2M_BS_SECURITY_BIND_PORT:5688}"
# Only Certificate_x509:
key_alias: "${LWM2M_BS_KEY_ALIAS:bootstrap}"
key_password: "${LWM2M_BS_KEY_PASSWORD:server_ks_password}"
# Bootstrap server X509 Certificates support
credentials:
# Whether to enable LWM2M bootstrap server X509 Certificate/RPK support
enabled: "${LWM2M_BS_CREDENTIALS_ENABLED:false}"
# Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore)
type: "${LWM2M_BS_CREDENTIALS_TYPE:PEM}"
# PEM server credentials
pem:
# Path to the server certificate file (holds server certificate or certificate chain, may include server private key)
cert_file: "${LWM2M_BS_PEM_CERT:lwm2mserver.pem}"
# Path to the server certificate private key file (optional)
key_file: "${LWM2M_BS_PEM_KEY:lwm2mserver_key.pem}"
# Server certificate private key password (optional)
key_password: "${LWM2M_BS_PEM_KEY_PASSWORD:server_key_password}"
# Keystore server credentials
keystore:
# Type of the key store
type: "${LWM2M_BS_KEY_STORE_TYPE:JKS}"
# Path to the key store that holds the SSL certificate
store_file: "${LWM2M_BS_KEY_STORE:lwm2mserver.jks}"
# Password used to access the key store
store_password: "${LWM2M_BS_KEY_STORE_PASSWORD:server_ks_password}"
# Password used to access the key
key_password: "${LWM2M_BS_KEY_PASSWORD:server_key_password}"
# Key alias
key_alias: "${LWM2M_BS_KEY_ALIAS:bootstrap}"
security:
# Certificate_x509:
# To get helps about files format and how to generate it, see: https://github.com/eclipse/leshan/wiki/Credential-files-format
# Create new X509 Certificates: common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh
key_store_type: "${LWM2M_KEYSTORE_TYPE:JKS}"
# key_store_path_file: "${KEY_STORE_PATH_FILE:/common/transport/lwm2m/src/main/resources/credentials/serverKeyStore.jks"
key_store: "${LWM2M_KEYSTORE:lwm2mserver.jks}"
key_store_password: "${LWM2M_KEYSTORE_PASSWORD:server_ks_password}"
root_alias: "${LWM2M_SERVER_ROOT_CA_ALIAS:rootca}"
# X509 trust certificates
trust-credentials:
# Whether to load X509 trust certificates
enabled: "${LWM2M_TRUST_CREDENTIALS_ENABLED:false}"
# Trust certificates store type (PEM - pem certificates file; KEYSTORE - java keystore)
type: "${LWM2M_TRUST_CREDENTIALS_TYPE:PEM}"
# PEM certificates
pem:
# Path to the certificates file (holds trust certificates)
cert_file: "${LWM2M_TRUST_PEM_CERT:lwm2mserver.pem}"
# Keystore with trust certificates
keystore:
# Type of the key store
type: "${LWM2M_TRUST_KEY_STORE_TYPE:JKS}"
# Path to the key store that holds the X509 certificates
store_file: "${LWM2M_TRUST_KEY_STORE:lwm2mserver.jks}"
# Password used to access the key store
store_password: "${LWM2M_TRUST_KEY_STORE_PASSWORD:server_ks_password}"
recommended_ciphers: "${LWM2M_RECOMMENDED_CIPHERS:false}"
recommended_supported_groups: "${LWM2M_RECOMMENDED_SUPPORTED_GROUPS:true}"
timeout: "${LWM2M_TIMEOUT:120000}"

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

@ -1,10 +1,21 @@
transport.lwm2m.security.key_store=lwm2m/credentials/serverKeyStore.jks
transport.lwm2m.security.key_store_password=server
transport.lwm2m.server.security.credentials.enabled=true
transport.lwm2m.server.security.credentials.type=KEYSTORE
transport.lwm2m.server.security.credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks
transport.lwm2m.server.security.credentials.keystore.store_password=server
transport.lwm2m.server.security.credentials.keystore.key_alias=server
transport.lwm2m.server.security.credentials.keystore.key_password=server
transport.lwm2m.bootstrap.security.credentials.enabled=true
transport.lwm2m.bootstrap.security.credentials.type=KEYSTORE
transport.lwm2m.bootstrap.security.credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks
transport.lwm2m.bootstrap.security.credentials.keystore.store_password=server
transport.lwm2m.bootstrap.security.credentials.keystore.key_alias=server
transport.lwm2m.bootstrap.security.credentials.keystore.key_password=server
transport.lwm2m.security.trust-credentials.enabled=true
transport.lwm2m.security.trust-credentials.type=KEYSTORE
transport.lwm2m.security.trust-credentials.keystore.store_file=lwm2m/credentials/serverKeyStore.jks
transport.lwm2m.security.trust-credentials.keystore.store_password=server
edges.enabled=true
edges.storage.no_read_records_sleep=500
edges.storage.sleep_between_batches=500
transport.lwm2m.server.security.key_alias=server
transport.lwm2m.server.security.key_password=server
transport.lwm2m.bootstrap.security.key_alias=server
transport.lwm2m.bootstrap.security.key_password=server
actors.rpc.sequential=true
actors.rpc.sequential=true

37
common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java

@ -20,11 +20,16 @@ import org.eclipse.californium.elements.util.SslContextUtil;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.eclipse.californium.scandium.dtls.CertificateType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.ResourceUtils;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.common.transport.config.ssl.SslCredentials;
import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import java.io.IOException;
@ -45,17 +50,15 @@ public class TbCoapDtlsSettings {
@Value("${transport.coap.dtls.bind_port}")
private Integer port;
@Value("${transport.coap.dtls.key_store}")
private String keyStoreFile;
@Value("${transport.coap.dtls.key_store_password}")
private String keyStorePassword;
@Value("${transport.coap.dtls.key_password}")
private String keyPassword;
@Bean
@ConfigurationProperties(prefix = "transport.coap.dtls.credentials")
public SslCredentialsConfig coapDtlsCredentials() {
return new SslCredentialsConfig("COAP DTLS Credentials", false);
}
@Value("${transport.coap.dtls.key_alias}")
private String keyAlias;
@Autowired
@Qualifier("coapDtlsCredentials")
private SslCredentialsConfig coapDtlsCredentialsConfig;
@Value("${transport.coap.dtls.x509.skip_validity_check_for_client_cert:false}")
private boolean skipValidityCheckForClientCert;
@ -75,8 +78,9 @@ public class TbCoapDtlsSettings {
public DtlsConnectorConfig dtlsConnectorConfig() throws UnknownHostException {
DtlsConnectorConfig.Builder configBuilder = new DtlsConnectorConfig.Builder();
configBuilder.setAddress(getInetSocketAddress());
String keyStoreFilePath = ResourceUtils.getUri(this, keyStoreFile);
SslContextUtil.Credentials serverCredentials = loadServerCredentials(keyStoreFilePath);
SslCredentials sslCredentials = this.coapDtlsCredentialsConfig.getCredentials();
SslContextUtil.Credentials serverCredentials =
new SslContextUtil.Credentials(sslCredentials.getPrivateKey(), null, sslCredentials.getCertificateChain());
configBuilder.setServerOnly(true);
configBuilder.setClientAuthenticationRequired(false);
configBuilder.setClientAuthenticationWanted(true);
@ -94,15 +98,6 @@ public class TbCoapDtlsSettings {
return configBuilder.build();
}
private SslContextUtil.Credentials loadServerCredentials(String keyStoreFilePath) {
try {
return SslContextUtil.loadCredentials(keyStoreFilePath, keyAlias, keyStorePassword.toCharArray(),
keyPassword.toCharArray());
} catch (GeneralSecurityException | IOException e) {
throw new RuntimeException("Failed to load serverCredentials due to: ", e);
}
}
private InetSocketAddress getInetSocketAddress() throws UnknownHostException {
InetAddress addr = InetAddress.getByName(host);
return new InetSocketAddress(addr, port);

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

@ -92,10 +92,10 @@ public class Device extends SearchTextBasedWithAdditionalInfo<DeviceId> implemen
return this;
}
@ApiModelProperty(position = 1, value = "JSON object with the device Id. " +
"Specify this field to update the device. " +
"Referencing non-existing device Id will cause error. " +
"Omit this field to create new device." )
@ApiModelProperty(position = 1, value = "JSON object with the Device Id. " +
"Specify this field to update the Device. " +
"Referencing non-existing Device Id will cause error. " +
"Omit this field to create new Device." )
@Override
public DeviceId getId() {
return super.getId();

32
common/data/src/main/java/org/thingsboard/server/common/data/EntityView.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.server.common.data;
import com.fasterxml.jackson.databind.JsonNode;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -37,15 +39,21 @@ public class EntityView extends SearchTextBasedWithAdditionalInfo<EntityViewId>
private static final long serialVersionUID = 5582010124562018986L;
@ApiModelProperty(position = 7, required = true, value = "JSON object with the referenced Entity Id (Device or Asset).")
private EntityId entityId;
private TenantId tenantId;
private CustomerId customerId;
@NoXss
@ApiModelProperty(position = 5, required = true, value = "Entity View name", example = "A4B72CCDFF33")
private String name;
@NoXss
@ApiModelProperty(position = 6, required = true, value = "Device Profile Name", example = "Temperature Sensor")
private String type;
@ApiModelProperty(position = 8, required = true, value = "Set of telemetry and attribute keys to expose via Entity View.")
private TelemetryEntityView keys;
@ApiModelProperty(position = 9, value = "Represents the start time of the interval that is used to limit access to target device telemetry. Customer will not be able to see entity telemetry that is outside the specified interval;")
private long startTimeMs;
@ApiModelProperty(position = 10, value = "Represents the end time of the interval that is used to limit access to target device telemetry. Customer will not be able to see entity telemetry that is outside the specified interval;")
private long endTimeMs;
public EntityView() {
@ -73,6 +81,7 @@ public class EntityView extends SearchTextBasedWithAdditionalInfo<EntityViewId>
return getName() /*What the ...*/;
}
@ApiModelProperty(position = 4, value = "JSON object with Customer Id. Use 'assignEntityViewToCustomer' to change the Customer Id.", readOnly = true)
@Override
public CustomerId getCustomerId() {
return customerId;
@ -83,8 +92,31 @@ public class EntityView extends SearchTextBasedWithAdditionalInfo<EntityViewId>
return name;
}
@ApiModelProperty(position = 3, value = "JSON object with Tenant Id.", readOnly = true)
@Override
public TenantId getTenantId() {
return tenantId;
}
@ApiModelProperty(position = 1, value = "JSON object with the Entity View Id. " +
"Specify this field to update the Entity View. " +
"Referencing non-existing Entity View Id will cause error. " +
"Omit this field to create new Entity View." )
@Override
public EntityViewId getId() {
return super.getId();
}
@ApiModelProperty(position = 2, value = "Timestamp of the Entity View creation, in milliseconds", example = "1609459200000", readOnly = true)
@Override
public long getCreatedTime() {
return super.getCreatedTime();
}
@ApiModelProperty(position = 11, value = "Additional parameters of the device", dataType = "com.fasterxml.jackson.databind.JsonNode")
@Override
public JsonNode getAdditionalInfo() {
return super.getAdditionalInfo();
}
}

3
common/data/src/main/java/org/thingsboard/server/common/data/EntityViewInfo.java

@ -15,13 +15,16 @@
*/
package org.thingsboard.server.common.data;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.thingsboard.server.common.data.id.EntityViewId;
@Data
public class EntityViewInfo extends EntityView {
@ApiModelProperty(position = 12, value = "Title of the Customer that owns the entity view.", readOnly = true)
private String customerTitle;
@ApiModelProperty(position = 13, value = "Indicates special 'Public' Customer that is auto-generated to use the entity view on public dashboards.", readOnly = true)
private boolean customerIsPublic;
public EntityViewInfo() {

24
common/data/src/main/java/org/thingsboard/server/common/data/ResourceUtils.java

@ -27,6 +27,30 @@ import java.net.URL;
@Slf4j
public class ResourceUtils {
public static boolean resourceExists(Object classLoaderSource, String filePath) {
return resourceExists(classLoaderSource.getClass().getClassLoader(), filePath);
}
public static boolean resourceExists(ClassLoader classLoader, String filePath) {
File resourceFile = new File(filePath);
if (resourceFile.exists()) {
return true;
} else {
InputStream classPathStream = classLoader.getResourceAsStream(filePath);
if (classPathStream != null) {
return true;
} else {
try {
URL url = Resources.getResource(filePath);
if (url != null) {
return true;
}
} catch (IllegalArgumentException e) {}
}
}
return false;
}
public static InputStream getInputStream(Object classLoaderSource, String filePath) {
return getInputStream(classLoaderSource.getClass().getClassLoader(), filePath);
}

32
common/data/src/main/java/org/thingsboard/server/common/data/SaveDeviceWithCredentialsRequest.java

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2021 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;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.thingsboard.server.common.data.security.DeviceCredentials;
@ApiModel
@Data
public class SaveDeviceWithCredentialsRequest {
@ApiModelProperty(position = 1, value = "The JSON with device entity.", required = true)
private final Device device;
@ApiModelProperty(position = 2, value = "The JSON with credentials entity.", required = true)
private final DeviceCredentials credentials;
}

6
common/data/src/main/java/org/thingsboard/server/common/data/entityview/EntityViewSearchQuery.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.server.common.data.entityview;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.relation.EntityRelation;
@ -25,11 +27,15 @@ import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import java.util.Collections;
import java.util.List;
@ApiModel
@Data
public class EntityViewSearchQuery {
@ApiModelProperty(position = 3, value = "Main search parameters.")
private RelationsSearchParameters parameters;
@ApiModelProperty(position = 1, value = "Type of the relation between root entity and device (e.g. 'Contains' or 'Manages').")
private String relationType;
@ApiModelProperty(position = 2, value = "Array of entity view types to filter the related entities (e.g. 'Temperature Sensor', 'Smoke Sensor').")
private List<String> entityViewTypes;
public EntityRelationsQuery toEntitySearchQuery() {

21
common/data/src/main/java/org/thingsboard/server/common/data/lwm2m/ServerSecurityConfig.java

@ -15,17 +15,30 @@
*/
package org.thingsboard.server.common.data.lwm2m;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@ApiModel
@Data
public class ServerSecurityConfig {
@ApiModelProperty(position = 1, value = "Is Bootstrap Server", example = "true", readOnly = true)
boolean bootstrapServerIs = true;
@ApiModelProperty(position = 2, value = "Host for 'No Security' mode", example = "0.0.0.0", readOnly = true)
String host;
String securityHost;
@ApiModelProperty(position = 3, value = "Port for 'No Security' mode", example = "5687", readOnly = true)
Integer port;
@ApiModelProperty(position = 4, value = "Host for 'Security' mode (DTLS)", example = "0.0.0.0", readOnly = true)
String securityHost;
@ApiModelProperty(position = 5, value = "Port for 'Security' mode (DTLS)", example = "5688", readOnly = true)
Integer securityPort;
String serverPublicKey;
boolean bootstrapServerIs = true;
Integer clientHoldOffTime = 1;
@ApiModelProperty(position = 5, value = "Server short Id", example = "111", readOnly = true)
Integer serverId = 111;
@ApiModelProperty(position = 7, value = "Client Hold Off Time", example = "1", readOnly = true)
Integer clientHoldOffTime = 1;
@ApiModelProperty(position = 8, value = "Server Public Key (base64 encoded)", example = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAZ0pSaGKHk/GrDaUDnQZpeEdGwX7m3Ws+U/kiVat\n" +
"+44sgk3c8g0LotfMpLlZJPhPwJ6ipXV+O1r7IZUjBs3LNA==", readOnly = true)
String serverPublicKey;
@ApiModelProperty(position = 9, value = "Bootstrap Server Account Timeout", example = "0", readOnly = true)
Integer bootstrapServerAccountTimeout = 0;
}

6
common/data/src/main/java/org/thingsboard/server/common/data/objects/AttributesEntityView.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.server.common.data.objects;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
@ -26,11 +28,15 @@ import java.util.List;
* Created by Victor Basanets on 9/05/2017.
*/
@Data
@ApiModel
@NoArgsConstructor
public class AttributesEntityView implements Serializable {
@ApiModelProperty(position = 1, required = true, value = "List of client-side attribute keys to expose", example = "currentConfiguration")
private List<String> cs = new ArrayList<>();
@ApiModelProperty(position = 3, required = true, value = "List of server-side attribute keys to expose", example = "model")
private List<String> ss = new ArrayList<>();
@ApiModelProperty(position = 2, required = true, value = "List of shared attribute keys to expose", example = "targetConfiguration")
private List<String> sh = new ArrayList<>();
public AttributesEntityView(List<String> cs,

5
common/data/src/main/java/org/thingsboard/server/common/data/objects/TelemetryEntityView.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.server.common.data.objects;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
@ -25,11 +27,14 @@ import java.util.List;
/**
* Created by Victor Basanets on 9/05/2017.
*/
@ApiModel
@Data
@NoArgsConstructor
public class TelemetryEntityView implements Serializable {
@ApiModelProperty(position = 1, required = true, value = "List of time-series data keys to expose", example = "temperature, humidity")
private List<String> timeseries;
@ApiModelProperty(position = 2, required = true, value = "JSON object with attributes to expose")
private AttributesEntityView attributes;
public TelemetryEntityView(List<String> timeseries, AttributesEntityView attributes) {

252
common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java

@ -17,6 +17,8 @@ package org.thingsboard.server.transport.http;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -68,6 +70,7 @@ import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;
/**
* @author Andrew Shvayka
*/
@ -77,14 +80,70 @@ import java.util.function.Consumer;
@Slf4j
public class DeviceApiController implements TbTransportService {
private static final String MARKDOWN_CODE_BLOCK_START = "\n\n```json\n";
private static final String MARKDOWN_CODE_BLOCK_END = "\n```\n\n";
private static final String REQUIRE_ACCESS_TOKEN = "The API call is designed to be used by device firmware and requires device access token ('deviceToken'). " +
"It is not recommended to use this API call by third-party scripts, rule-engine or platform widgets (use 'Telemetry Controller' instead).\n";
private static final String ATTRIBUTE_PAYLOAD_EXAMPLE = "{\n" +
" \"stringKey\":\"value1\", \n" +
" \"booleanKey\":true, \n" +
" \"doubleKey\":42.0, \n" +
" \"longKey\":73, \n" +
" \"jsonKey\": {\n" +
" \"someNumber\": 42,\n" +
" \"someArray\": [1,2,3],\n" +
" \"someNestedObject\": {\"key\": \"value\"}\n" +
" }\n" +
"}";
protected static final String TS_PAYLOAD = "The request payload is a JSON document with three possible formats:\n\n" +
"Simple format without timestamp. In such a case, current server time will be used: \n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"stringKey\":\"value1\", \n" +
" \"booleanKey\":true, \n" +
" \"doubleKey\":42.0, \n" +
" \"longKey\":73, \n" +
" \"jsonKey\": {\n" +
" \"someNumber\": 42,\n" +
" \"someArray\": [1,2,3],\n" +
" \"someNestedObject\": {\"key\": \"value\"}\n" +
" }\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"\n\n Single JSON object with timestamp: \n\n" +
MARKDOWN_CODE_BLOCK_START +
"{\"ts\":1634712287000,\"values\":{\"temperature\":26, \"humidity\":87}}" +
MARKDOWN_CODE_BLOCK_END +
"\n\n JSON array with timestamps: \n\n" +
MARKDOWN_CODE_BLOCK_START +
"[\n{\"ts\":1634712287000,\"values\":{\"temperature\":26, \"humidity\":87}}, \n{\"ts\":1634712588000,\"values\":{\"temperature\":25, \"humidity\":88}}\n]" +
MARKDOWN_CODE_BLOCK_END;
private static final String ACCESS_TOKEN_PARAM_DESCRIPTION = "Your device access token.";
@Autowired
private HttpTransportContext transportContext;
@ApiOperation(value = "Get attributes (getDeviceAttributes)",
notes = "Returns all attributes that belong to device. "
+ "Use optional 'clientKeys' and/or 'sharedKeys' parameter to return specific attributes. "
+ "\n Example of the result: "
+ MARKDOWN_CODE_BLOCK_START
+ ATTRIBUTE_PAYLOAD_EXAMPLE
+ MARKDOWN_CODE_BLOCK_END
+ REQUIRE_ACCESS_TOKEN,
produces = MediaType.APPLICATION_JSON_VALUE)
@RequestMapping(value = "/{deviceToken}/attributes", method = RequestMethod.GET, produces = "application/json")
public DeferredResult<ResponseEntity> getDeviceAttributes(@PathVariable("deviceToken") String deviceToken,
@RequestParam(value = "clientKeys", required = false, defaultValue = "") String clientKeys,
@RequestParam(value = "sharedKeys", required = false, defaultValue = "") String sharedKeys,
HttpServletRequest httpRequest) {
public DeferredResult<ResponseEntity> getDeviceAttributes(
@ApiParam(value = ACCESS_TOKEN_PARAM_DESCRIPTION, required = true, defaultValue = "YOUR_DEVICE_ACCESS_TOKEN")
@PathVariable("deviceToken") String deviceToken,
@ApiParam(value = "Comma separated key names for attribute with client scope", required = true, defaultValue = "state")
@RequestParam(value = "clientKeys", required = false, defaultValue = "") String clientKeys,
@ApiParam(value = "Comma separated key names for attribute with shared scope", required = true, defaultValue = "configuration")
@RequestParam(value = "sharedKeys", required = false, defaultValue = "") String sharedKeys) {
DeferredResult<ResponseEntity> responseWriter = new DeferredResult<>();
transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(),
new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> {
@ -106,9 +165,20 @@ public class DeviceApiController implements TbTransportService {
return responseWriter;
}
@ApiOperation(value = "Post attributes (postDeviceAttributes)",
notes = "Post client attribute updates on behalf of device. "
+ "\n Example of the request: "
+ MARKDOWN_CODE_BLOCK_START
+ ATTRIBUTE_PAYLOAD_EXAMPLE
+ MARKDOWN_CODE_BLOCK_END
+ REQUIRE_ACCESS_TOKEN,
produces = MediaType.APPLICATION_JSON_VALUE)
@RequestMapping(value = "/{deviceToken}/attributes", method = RequestMethod.POST)
public DeferredResult<ResponseEntity> postDeviceAttributes(@PathVariable("deviceToken") String deviceToken,
@RequestBody String json, HttpServletRequest request) {
public DeferredResult<ResponseEntity> postDeviceAttributes(
@ApiParam(value = ACCESS_TOKEN_PARAM_DESCRIPTION, required = true, defaultValue = "YOUR_DEVICE_ACCESS_TOKEN")
@PathVariable("deviceToken") String deviceToken,
@ApiParam(value = "JSON with attribute key-value pairs. See API call description for example.")
@RequestBody String json) {
DeferredResult<ResponseEntity> responseWriter = new DeferredResult<>();
transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(),
new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> {
@ -119,9 +189,17 @@ public class DeviceApiController implements TbTransportService {
return responseWriter;
}
@ApiOperation(value = "Post time-series data (postTelemetry)",
notes = "Post time-series data on behalf of device. "
+ "\n Example of the request: "
+ TS_PAYLOAD
+ REQUIRE_ACCESS_TOKEN,
produces = MediaType.APPLICATION_JSON_VALUE)
@RequestMapping(value = "/{deviceToken}/telemetry", method = RequestMethod.POST)
public DeferredResult<ResponseEntity> postTelemetry(@PathVariable("deviceToken") String deviceToken,
@RequestBody String json, HttpServletRequest request) {
public DeferredResult<ResponseEntity> postTelemetry(
@ApiParam(value = ACCESS_TOKEN_PARAM_DESCRIPTION, required = true, defaultValue = "YOUR_DEVICE_ACCESS_TOKEN")
@PathVariable("deviceToken") String deviceToken,
@RequestBody String json, HttpServletRequest request) {
DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>();
transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(),
new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> {
@ -132,9 +210,22 @@ public class DeviceApiController implements TbTransportService {
return responseWriter;
}
@ApiOperation(value = "Save claiming information (claimDevice)",
notes = "Saves the information required for user to claim the device. " +
"See more info about claiming in the corresponding 'Claiming devices' platform documentation."
+ "\n Example of the request payload: "
+ MARKDOWN_CODE_BLOCK_START
+ "{\"secretKey\":\"value\", \"durationMs\":60000}"
+ MARKDOWN_CODE_BLOCK_END
+ "Note: both 'secretKey' and 'durationMs' is optional parameters. " +
"In case the secretKey is not specified, the empty string as a default value is used. In case the durationMs is not specified, the system parameter device.claim.duration is used.\n\n"
+ REQUIRE_ACCESS_TOKEN,
produces = MediaType.APPLICATION_JSON_VALUE)
@RequestMapping(value = "/{deviceToken}/claim", method = RequestMethod.POST)
public DeferredResult<ResponseEntity> claimDevice(@PathVariable("deviceToken") String deviceToken,
@RequestBody(required = false) String json, HttpServletRequest request) {
public DeferredResult<ResponseEntity> claimDevice(
@ApiParam(value = ACCESS_TOKEN_PARAM_DESCRIPTION, required = true, defaultValue = "YOUR_DEVICE_ACCESS_TOKEN")
@PathVariable("deviceToken") String deviceToken,
@RequestBody(required = false) String json) {
DeferredResult<ResponseEntity> responseWriter = new DeferredResult<>();
transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(),
new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> {
@ -146,10 +237,18 @@ public class DeviceApiController implements TbTransportService {
return responseWriter;
}
@ApiOperation(value = "Subscribe to RPC commands (subscribeToCommands) (Deprecated)",
notes = "Subscribes to RPC commands using http long polling. " +
"Deprecated, since long polling is resource and network consuming. " +
"Consider using MQTT or CoAP protocol for light-weight real-time updates. \n\n" +
REQUIRE_ACCESS_TOKEN,
produces = MediaType.APPLICATION_JSON_VALUE)
@RequestMapping(value = "/{deviceToken}/rpc", method = RequestMethod.GET, produces = "application/json")
public DeferredResult<ResponseEntity> subscribeToCommands(@PathVariable("deviceToken") String deviceToken,
@RequestParam(value = "timeout", required = false, defaultValue = "0") long timeout,
HttpServletRequest httpRequest) {
public DeferredResult<ResponseEntity> subscribeToCommands(
@ApiParam(value = ACCESS_TOKEN_PARAM_DESCRIPTION, required = true, defaultValue = "YOUR_DEVICE_ACCESS_TOKEN")
@PathVariable("deviceToken") String deviceToken,
@ApiParam(value = "Optional timeout of the long poll. Typically less then 60 seconds, since limited on the server side.")
@RequestParam(value = "timeout", required = false, defaultValue = "0") long timeout) {
DeferredResult<ResponseEntity> responseWriter = new DeferredResult<>();
transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(),
new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> {
@ -164,10 +263,18 @@ public class DeviceApiController implements TbTransportService {
return responseWriter;
}
@ApiOperation(value = "Reply to RPC commands (replyToCommand)",
notes = "Replies to server originated RPC command identified by 'requestId' parameter. The response is arbitrary JSON.\n\n" +
REQUIRE_ACCESS_TOKEN,
produces = MediaType.APPLICATION_JSON_VALUE)
@RequestMapping(value = "/{deviceToken}/rpc/{requestId}", method = RequestMethod.POST)
public DeferredResult<ResponseEntity> replyToCommand(@PathVariable("deviceToken") String deviceToken,
@PathVariable("requestId") Integer requestId,
@RequestBody String json, HttpServletRequest request) {
public DeferredResult<ResponseEntity> replyToCommand(
@ApiParam(value = ACCESS_TOKEN_PARAM_DESCRIPTION, required = true, defaultValue = "YOUR_DEVICE_ACCESS_TOKEN")
@PathVariable("deviceToken") String deviceToken,
@ApiParam(value = "RPC request id from the incoming RPC request", required = true, defaultValue = "123")
@PathVariable("requestId") Integer requestId,
@ApiParam(value = "Reply to the RPC request, JSON. For example: {\"status\":\"success\"}", required = true)
@RequestBody String json) {
DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>();
transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(),
new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> {
@ -177,9 +284,23 @@ public class DeviceApiController implements TbTransportService {
return responseWriter;
}
@ApiOperation(value = "Send the RPC command (postRpcRequest)",
notes = "Send the RPC request to server. The request payload is a JSON document that contains 'method' and 'params'. For example:" +
MARKDOWN_CODE_BLOCK_START +
"{\"method\": \"sumOnServer\", \"params\":{\"a\":2, \"b\":2}}" +
MARKDOWN_CODE_BLOCK_END +
"The response contains arbitrary JSON with the RPC reply. For example: " +
MARKDOWN_CODE_BLOCK_START +
"{\"result\": 4}" +
MARKDOWN_CODE_BLOCK_END +
REQUIRE_ACCESS_TOKEN,
produces = MediaType.APPLICATION_JSON_VALUE)
@RequestMapping(value = "/{deviceToken}/rpc", method = RequestMethod.POST)
public DeferredResult<ResponseEntity> postRpcRequest(@PathVariable("deviceToken") String deviceToken,
@RequestBody String json, HttpServletRequest httpRequest) {
public DeferredResult<ResponseEntity> postRpcRequest(
@ApiParam(value = ACCESS_TOKEN_PARAM_DESCRIPTION, required = true, defaultValue = "YOUR_DEVICE_ACCESS_TOKEN")
@PathVariable("deviceToken") String deviceToken,
@ApiParam(value = "The RPC request JSON", required = true)
@RequestBody String json) {
DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>();
transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(),
new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> {
@ -196,10 +317,18 @@ public class DeviceApiController implements TbTransportService {
return responseWriter;
}
@ApiOperation(value = "Subscribe to attribute updates (subscribeToAttributes) (Deprecated)",
notes = "Subscribes to client and shared scope attribute updates using http long polling. " +
"Deprecated, since long polling is resource and network consuming. " +
"Consider using MQTT or CoAP protocol for light-weight real-time updates. \n\n" +
REQUIRE_ACCESS_TOKEN,
produces = MediaType.APPLICATION_JSON_VALUE)
@RequestMapping(value = "/{deviceToken}/attributes/updates", method = RequestMethod.GET, produces = "application/json")
public DeferredResult<ResponseEntity> subscribeToAttributes(@PathVariable("deviceToken") String deviceToken,
@RequestParam(value = "timeout", required = false, defaultValue = "0") long timeout,
HttpServletRequest httpRequest) {
public DeferredResult<ResponseEntity> subscribeToAttributes(
@ApiParam(value = ACCESS_TOKEN_PARAM_DESCRIPTION, required = true, defaultValue = "YOUR_DEVICE_ACCESS_TOKEN")
@PathVariable("deviceToken") String deviceToken,
@ApiParam(value = "Optional timeout of the long poll. Typically less then 60 seconds, since limited on the server side.")
@RequestParam(value = "timeout", required = false, defaultValue = "0") long timeout) {
DeferredResult<ResponseEntity> responseWriter = new DeferredResult<>();
transportContext.getTransportService().process(DeviceTransportType.DEFAULT, ValidateDeviceTokenRequestMsg.newBuilder().setToken(deviceToken).build(),
new DeviceAuthCallback(transportContext, responseWriter, sessionInfo -> {
@ -214,26 +343,85 @@ public class DeviceApiController implements TbTransportService {
return responseWriter;
}
@ApiOperation(value = "Get Device Firmware (getFirmware)",
notes = "Downloads the current firmware package." +
"When the platform initiates firmware update, " +
"it informs the device by updating the 'fw_title', 'fw_version', 'fw_checksum' and 'fw_checksum_algorithm' shared attributes." +
"The 'fw_title' and 'fw_version' parameters must be supplied in this request to double-check " +
"that the firmware that device is downloading matches the firmware it expects to download. " +
"This is important, since the administrator may change the firmware assignment while device is downloading the firmware. \n\n" +
"Optional 'chunk' and 'size' parameters may be used to download the firmware in chunks. " +
"For example, device may request first 16 KB of firmware using 'chunk'=0 and 'size'=16384. " +
"Next 16KB using 'chunk'=1 and 'size'=16384. The last chunk should have less bytes then requested using 'size' parameter. \n\n" +
REQUIRE_ACCESS_TOKEN,
produces = MediaType.APPLICATION_JSON_VALUE)
@RequestMapping(value = "/{deviceToken}/firmware", method = RequestMethod.GET)
public DeferredResult<ResponseEntity> getFirmware(@PathVariable("deviceToken") String deviceToken,
@RequestParam(value = "title") String title,
@RequestParam(value = "version") String version,
@RequestParam(value = "size", required = false, defaultValue = "0") int size,
@RequestParam(value = "chunk", required = false, defaultValue = "0") int chunk) {
public DeferredResult<ResponseEntity> getFirmware(
@ApiParam(value = ACCESS_TOKEN_PARAM_DESCRIPTION, required = true, defaultValue = "YOUR_DEVICE_ACCESS_TOKEN")
@PathVariable("deviceToken") String deviceToken,
@ApiParam(value = "Title of the firmware, corresponds to the value of 'fw_title' attribute.", required = true)
@RequestParam(value = "title") String title,
@ApiParam(value = "Version of the firmware, corresponds to the value of 'fw_version' attribute.", required = true)
@RequestParam(value = "version") String version,
@ApiParam(value = "Size of the chunk. Optional. Omit to download the entire file without chunks.")
@RequestParam(value = "size", required = false, defaultValue = "0") int size,
@ApiParam(value = "Index of the chunk. Optional. Omit to download the entire file without chunks.")
@RequestParam(value = "chunk", required = false, defaultValue = "0") int chunk) {
return getOtaPackageCallback(deviceToken, title, version, size, chunk, OtaPackageType.FIRMWARE);
}
@ApiOperation(value = "Get Device Software (getSoftware)",
notes = "Downloads the current software package." +
"When the platform initiates software update, " +
"it informs the device by updating the 'sw_title', 'sw_version', 'sw_checksum' and 'sw_checksum_algorithm' shared attributes." +
"The 'sw_title' and 'sw_version' parameters must be supplied in this request to double-check " +
"that the software that device is downloading matches the software it expects to download. " +
"This is important, since the administrator may change the software assignment while device is downloading the software. \n\n" +
"Optional 'chunk' and 'size' parameters may be used to download the software in chunks. " +
"For example, device may request first 16 KB of software using 'chunk'=0 and 'size'=16384. " +
"Next 16KB using 'chunk'=1 and 'size'=16384. The last chunk should have less bytes then requested using 'size' parameter. \n\n" +
REQUIRE_ACCESS_TOKEN,
produces = MediaType.APPLICATION_JSON_VALUE)
@RequestMapping(value = "/{deviceToken}/software", method = RequestMethod.GET)
public DeferredResult<ResponseEntity> getSoftware(@PathVariable("deviceToken") String deviceToken,
@RequestParam(value = "title") String title,
@RequestParam(value = "version") String version,
@RequestParam(value = "size", required = false, defaultValue = "0") int size,
@RequestParam(value = "chunk", required = false, defaultValue = "0") int chunk) {
public DeferredResult<ResponseEntity> getSoftware(
@ApiParam(value = ACCESS_TOKEN_PARAM_DESCRIPTION, required = true, defaultValue = "YOUR_DEVICE_ACCESS_TOKEN")
@PathVariable("deviceToken") String deviceToken,
@ApiParam(value = "Title of the software, corresponds to the value of 'sw_title' attribute.", required = true)
@RequestParam(value = "title") String title,
@ApiParam(value = "Version of the software, corresponds to the value of 'sw_version' attribute.", required = true)
@RequestParam(value = "version") String version,
@ApiParam(value = "Size of the chunk. Optional. Omit to download the entire file without using chunks.")
@RequestParam(value = "size", required = false, defaultValue = "0") int size,
@ApiParam(value = "Index of the chunk. Optional. Omit to download the entire file without using chunks.")
@RequestParam(value = "chunk", required = false, defaultValue = "0") int chunk) {
return getOtaPackageCallback(deviceToken, title, version, size, chunk, OtaPackageType.SOFTWARE);
}
@ApiOperation(value = "Provision new device (provisionDevice)",
notes = "Exchange the provision request to the device credentials. " +
"See more info about provisioning in the corresponding 'Device provisioning' platform documentation." +
"Requires valid JSON request with the following format: " +
MARKDOWN_CODE_BLOCK_START +
"{\n" +
" \"deviceName\": \"NEW_DEVICE_NAME\",\n" +
" \"provisionDeviceKey\": \"u7piawkboq8v32dmcmpp\",\n" +
" \"provisionDeviceSecret\": \"jpmwdn8ptlswmf4m29bw\"\n" +
"}" +
MARKDOWN_CODE_BLOCK_END +
"Where 'deviceName' is the name of enw or existing device which depends on the provisioning strategy. " +
"The 'provisionDeviceKey' and 'provisionDeviceSecret' matches info configured in one of the existing device profiles. " +
"The result of the successful call is the JSON object that contains new credentials:" +
MARKDOWN_CODE_BLOCK_START + "{\n" +
" \"credentialsType\":\"ACCESS_TOKEN\",\n" +
" \"credentialsValue\":\"DEVICE_ACCESS_TOKEN\",\n" +
" \"status\":\"SUCCESS\"\n" +
"}" + MARKDOWN_CODE_BLOCK_END
,
produces = MediaType.APPLICATION_JSON_VALUE)
@RequestMapping(value = "/provision", method = RequestMethod.POST)
public DeferredResult<ResponseEntity> provisionDevice(@RequestBody String json, HttpServletRequest httpRequest) {
public DeferredResult<ResponseEntity> provisionDevice(
@ApiParam(value = "JSON with provision request. See API call description for example.")
@RequestBody String json) {
DeferredResult<ResponseEntity> responseWriter = new DeferredResult<>();
transportContext.getTransportService().process(JsonConverter.convertToProvisionRequestMsg(json),
new DeviceProvisionCallback(responseWriter));

52
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/LwM2MTransportBootstrapService.java

@ -27,6 +27,7 @@ import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServer;
import org.eclipse.leshan.server.californium.bootstrap.LeshanBootstrapServerBuilder;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.transport.config.ssl.SslCredentials;
import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MBootstrapSecurityStore;
import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MInMemoryBootstrapConfigStore;
import org.thingsboard.server.transport.lwm2m.bootstrap.secure.LwM2MInMemoryBootstrapConfigurationAdapter;
@ -114,49 +115,22 @@ public class LwM2MTransportBootstrapService {
}
private void setServerWithCredentials(LeshanBootstrapServerBuilder builder) {
try {
if (serverConfig.getKeyStoreValue() != null) {
KeyStore keyStoreServer = serverConfig.getKeyStoreValue();
if (this.setBuilderX509(builder)) {
X509Certificate rootCAX509Cert = (X509Certificate) keyStoreServer.getCertificate(serverConfig.getRootCertificateAlias());
if (rootCAX509Cert != null) {
X509Certificate[] trustedCertificates = new X509Certificate[1];
trustedCertificates[0] = rootCAX509Cert;
builder.setTrustedCertificates(trustedCertificates);
} else {
/* by default trust all */
builder.setTrustedCertificates(new X509Certificate[0]);
}
}
if (this.bootstrapConfig.getSslCredentials() != null) {
SslCredentials sslCredentials = this.bootstrapConfig.getSslCredentials();
builder.setPublicKey(sslCredentials.getPublicKey());
builder.setPrivateKey(sslCredentials.getPrivateKey());
builder.setCertificateChain(sslCredentials.getCertificateChain());
if (this.serverConfig.getTrustSslCredentials() != null) {
builder.setTrustedCertificates(this.serverConfig.getTrustSslCredentials().getTrustedCertificates());
} else {
/* by default trust all */
builder.setTrustedCertificates(new X509Certificate[0]);
log.info("Unable to load X509 files for BootStrapServer");
this.pskMode = true;
}
} catch (KeyStoreException ex) {
log.error("[{}] Unable to load X509 files server", ex.getMessage());
} else {
/* by default trust all */
builder.setTrustedCertificates(new X509Certificate[0]);
log.info("Unable to load X509 files for BootStrapServer");
this.pskMode = true;
}
}
private boolean setBuilderX509(LeshanBootstrapServerBuilder builder) {
try {
X509Certificate[] certificateChain = SslContextUtil.asX509Certificates(serverConfig.getKeyStoreValue().getCertificateChain(this.bootstrapConfig.getCertificateAlias()));
X509Certificate serverCertificate = certificateChain[0];
PrivateKey privateKey = (PrivateKey) serverConfig.getKeyStoreValue().getKey(this.bootstrapConfig.getCertificateAlias(), serverConfig.getCertificatePassword() == null ? null : serverConfig.getCertificatePassword().toCharArray());
PublicKey publicKey = serverCertificate.getPublicKey();
if (privateKey != null && privateKey.getEncoded().length > 0 && publicKey != null && publicKey.getEncoded().length > 0) {
builder.setPublicKey(serverCertificate.getPublicKey());
builder.setPrivateKey(privateKey);
builder.setCertificateChain(certificateChain);
return true;
} else {
return false;
}
} catch (Exception ex) {
log.error("[{}] Unable to load KeyStore files server", ex.getMessage());
return false;
}
}
}

6
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MSecureServerConfig.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.server.transport.lwm2m.config;
import org.thingsboard.server.common.transport.config.ssl.SslCredentials;
public interface LwM2MSecureServerConfig {
Integer getId();
@ -27,8 +29,6 @@ public interface LwM2MSecureServerConfig {
Integer getSecurePort();
String getCertificateAlias();
String getCertificatePassword();
SslCredentials getSslCredentials();
}

24
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportBootstrapConfig.java

@ -17,9 +17,15 @@ package org.thingsboard.server.transport.lwm2m.config;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.transport.config.ssl.SslCredentials;
import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig;
@Slf4j
@Component
@ -46,12 +52,18 @@ public class LwM2MTransportBootstrapConfig implements LwM2MSecureServerConfig {
@Value("${transport.lwm2m.bootstrap.security.bind_port:}")
private Integer securePort;
@Getter
@Value("${transport.lwm2m.bootstrap.security.key_alias:}")
private String certificateAlias;
@Bean
@ConfigurationProperties(prefix = "transport.lwm2m.bootstrap.security.credentials")
public SslCredentialsConfig lwm2mBootstrapCredentials() {
return new SslCredentialsConfig("LWM2M Bootstrap DTLS Credentials", false);
}
@Getter
@Value("${transport.lwm2m.bootstrap.security.key_password:}")
private String certificatePassword;
@Autowired
@Qualifier("lwm2mBootstrapCredentials")
private SslCredentialsConfig credentialsConfig;
@Override
public SslCredentials getSslCredentials() {
return this.credentialsConfig.getCredentials();
}
}

69
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/config/LwM2MTransportServerConfig.java

@ -18,10 +18,16 @@ package org.thingsboard.server.transport.lwm2m.config;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.ResourceUtils;
import org.thingsboard.server.common.transport.config.ssl.SslCredentials;
import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig;
import javax.annotation.PostConstruct;
import java.io.InputStream;
@ -64,26 +70,6 @@ public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig {
@Value("${transport.lwm2m.clean_period_in_sec:}")
private int cleanPeriodInSec;
@Getter
@Value("${transport.lwm2m.security.key_store_type:}")
private String keyStoreType;
@Getter
@Value("${transport.lwm2m.security.key_store:}")
private String keyStoreFilePath;
@Getter
@Setter
private KeyStore keyStoreValue;
@Getter
@Value("${transport.lwm2m.security.key_store_password:}")
private String keyStorePassword;
@Getter
@Value("${transport.lwm2m.security.root_alias:}")
private String rootCertificateAlias;
@Getter
@Value("${transport.lwm2m.server.id:}")
private Integer id;
@ -104,14 +90,6 @@ public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig {
@Value("${transport.lwm2m.server.security.bind_port:}")
private Integer securePort;
@Getter
@Value("${transport.lwm2m.server.security.key_alias:}")
private String certificateAlias;
@Getter
@Value("${transport.lwm2m.server.security.key_password:}")
private String certificatePassword;
@Getter
@Value("${transport.lwm2m.log_max_length:}")
private int logMaxLength;
@ -124,15 +102,32 @@ public class LwM2MTransportServerConfig implements LwM2MSecureServerConfig {
@Value("${transport.lwm2m.paging_transmission_window:10000}")
private long pagingTransmissionWindow;
@PostConstruct
public void init() {
try {
InputStream keyStoreInputStream = ResourceUtils.getInputStream(this, keyStoreFilePath);
keyStoreValue = KeyStore.getInstance(keyStoreType);
keyStoreValue.load(keyStoreInputStream, keyStorePassword == null ? null : keyStorePassword.toCharArray());
} catch (Exception e) {
log.info("Unable to lookup LwM2M keystore. Reason: {}, {}", keyStoreFilePath, e.getMessage());
}
@Bean
@ConfigurationProperties(prefix = "transport.lwm2m.server.security.credentials")
public SslCredentialsConfig lwm2mServerCredentials() {
return new SslCredentialsConfig("LWM2M Server DTLS Credentials", false);
}
@Autowired
@Qualifier("lwm2mServerCredentials")
private SslCredentialsConfig credentialsConfig;
@Bean
@ConfigurationProperties(prefix = "transport.lwm2m.security.trust-credentials")
public SslCredentialsConfig lwm2mTrustCredentials() {
return new SslCredentialsConfig("LWM2M Trust Credentials", true);
}
@Autowired
@Qualifier("lwm2mTrustCredentials")
private SslCredentialsConfig trustCredentialsConfig;
@Override
public SslCredentials getSslCredentials() {
return this.credentialsConfig.getCredentials();
}
public SslCredentials getTrustSslCredentials() {
return this.trustCredentialsConfig.getCredentials();
}
}

8
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/secure/TbLwM2MDtlsCertificateVerifier.java

@ -87,12 +87,8 @@ public class TbLwM2MDtlsCertificateVerifier implements NewAdvancedCertificateVer
try {
/* by default trust all */
X509Certificate[] trustedCertificates = new X509Certificate[0];
if (config.getKeyStoreValue() != null) {
X509Certificate rootCAX509Cert = (X509Certificate) config.getKeyStoreValue().getCertificate(config.getRootCertificateAlias());
if (rootCAX509Cert != null) {
trustedCertificates = new X509Certificate[1];
trustedCertificates[0] = rootCAX509Cert;
}
if (config.getTrustSslCredentials() != null) {
trustedCertificates = config.getTrustSslCredentials().getTrustedCertificates();
}
staticCertificateVerifier = new StaticCertificateVerifier(trustedCertificates);
} catch (Exception e) {

27
common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java

@ -29,6 +29,7 @@ import org.eclipse.leshan.server.model.LwM2mModelProvider;
import org.springframework.stereotype.Component;
import org.thingsboard.server.cache.ota.OtaPackageDataCache;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.transport.config.ssl.SslCredentials;
import org.thingsboard.server.queue.util.TbLwM2mTransportComponent;
import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig;
import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MAuthorizer;
@ -141,7 +142,11 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService {
}
private void setServerWithCredentials(LeshanServerBuilder builder, DtlsConnectorConfig.Builder dtlsConfig) {
if (config.getKeyStoreValue() != null && this.setBuilderX509(builder)) {
if (this.config.getSslCredentials() != null) {
SslCredentials sslCredentials = this.config.getSslCredentials();
builder.setPublicKey(sslCredentials.getPublicKey());
builder.setPrivateKey(sslCredentials.getPrivateKey());
builder.setCertificateChain(sslCredentials.getCertificateChain());
dtlsConfig.setAdvancedCertificateVerifier(certificateVerifier);
builder.setAuthorizer(authorizer);
dtlsConfig.setSupportedCipherSuites(RPK_OR_X509_CIPHER_SUITES);
@ -153,26 +158,6 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService {
}
}
private boolean setBuilderX509(LeshanServerBuilder builder) {
try {
X509Certificate[] certificateChain = SslContextUtil.asX509Certificates(config.getKeyStoreValue().getCertificateChain(config.getCertificateAlias()));
X509Certificate serverCertificate = certificateChain[0];
PrivateKey privateKey = (PrivateKey) config.getKeyStoreValue().getKey(config.getCertificateAlias(), config.getCertificatePassword() == null ? null : config.getCertificatePassword().toCharArray());
PublicKey publicKey = serverCertificate.getPublicKey();
if (privateKey != null && privateKey.getEncoded().length > 0 && publicKey != null && publicKey.getEncoded().length > 0) {
builder.setPublicKey(serverCertificate.getPublicKey());
builder.setPrivateKey(privateKey);
builder.setCertificateChain(certificateChain);
return true;
} else {
return false;
}
} catch (Exception ex) {
log.error("[{}] Unable to load KeyStore files server", ex.getMessage());
return false;
}
}
@Override
public String getName() {
return DataConstants.LWM2M_TRANSPORT_NAME;

42
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java

@ -18,16 +18,20 @@ package org.thingsboard.server.transport.mqtt;
import io.netty.handler.ssl.SslHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.ResourceUtils;
import org.thingsboard.server.common.msg.EncryptionUtil;
import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.common.transport.TransportServiceCallback;
import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
import org.thingsboard.server.common.transport.config.ssl.SslCredentials;
import org.thingsboard.server.common.transport.config.ssl.SslCredentialsConfig;
import org.thingsboard.server.common.transport.util.SslUtil;
import org.thingsboard.server.gen.transport.TransportProtos;
@ -38,8 +42,6 @@ import javax.net.ssl.SSLEngine;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
@ -56,18 +58,20 @@ public class MqttSslHandlerProvider {
@Value("${transport.mqtt.ssl.protocol}")
private String sslProtocol;
@Value("${transport.mqtt.ssl.key_store}")
private String keyStoreFile;
@Value("${transport.mqtt.ssl.key_store_password}")
private String keyStorePassword;
@Value("${transport.mqtt.ssl.key_password}")
private String keyPassword;
@Value("${transport.mqtt.ssl.key_store_type}")
private String keyStoreType;
@Autowired
private TransportService transportService;
@Bean
@ConfigurationProperties(prefix = "transport.mqtt.ssl.credentials")
public SslCredentialsConfig mqttSslCredentials() {
return new SslCredentialsConfig("MQTT SSL Credentials", false);
}
@Autowired
@Qualifier("mqttSslCredentials")
private SslCredentialsConfig mqttSslCredentialsConfig;
private SSLContext sslContext;
public SslHandler getSslHandler() {
@ -86,19 +90,9 @@ public class MqttSslHandlerProvider {
private SSLContext createSslContext() {
try {
TrustManagerFactory tmFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
KeyStore trustStore = KeyStore.getInstance(keyStoreType);
try (InputStream tsFileInputStream = ResourceUtils.getInputStream(this, keyStoreFile)) {
trustStore.load(tsFileInputStream, keyStorePassword.toCharArray());
}
tmFactory.init(trustStore);
KeyStore ks = KeyStore.getInstance(keyStoreType);
try (InputStream ksFileInputStream = ResourceUtils.getInputStream(this, keyStoreFile)) {
ks.load(ksFileInputStream, keyStorePassword.toCharArray());
}
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, keyPassword.toCharArray());
SslCredentials sslCredentials = this.mqttSslCredentialsConfig.getCredentials();
TrustManagerFactory tmFactory = sslCredentials.createTrustManagerFactory();
KeyManagerFactory kmf = sslCredentials.createKeyManagerFactory();
KeyManager[] km = kmf.getKeyManagers();
TrustManager x509wrapped = getX509TrustManager(tmFactory);

3
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java

@ -991,7 +991,6 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
@Override
public void onAttributeUpdate(UUID sessionId, TransportProtos.AttributeUpdateNotificationMsg notification) {
log.trace("[{}] Received attributes update notification to device", sessionId);
log.info("[{}] : attrSubTopicType: {}", notification.toString(), attrSubTopicType);
String topic;
MqttTransportAdaptor adaptor = deviceSessionCtx.getAdaptor(attrSubTopicType);
switch (attrSubTopicType) {
@ -1023,7 +1022,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
@Override
public void onToDeviceRpcRequest(UUID sessionId, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) {
log.info("[{}] Received RPC command to device", sessionId);
log.trace("[{}] Received RPC command to device", sessionId);
String baseTopic;
MqttTransportAdaptor adaptor = deviceSessionCtx.getAdaptor(rpcSubTopicType);
switch (rpcSubTopicType) {

8
common/transport/transport-api/pom.xml

@ -129,6 +129,14 @@
<groupId>org.eclipse.leshan</groupId>
<artifactId>leshan-server-cf</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
</dependency>
</dependencies>
<build>

191
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/AbstractSslCredentials.java

@ -0,0 +1,191 @@
/**
* Copyright © 2016-2021 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.transport.config.ssl;
import org.thingsboard.server.common.data.StringUtils;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStore.PrivateKeyEntry;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.UnrecoverableEntryException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
public abstract class AbstractSslCredentials implements SslCredentials {
private char[] keyPasswordArray;
private KeyStore keyStore;
private PrivateKey privateKey;
private PublicKey publicKey;
private X509Certificate[] chain;
private X509Certificate[] trusts;
@Override
public void init(boolean trustsOnly) throws IOException, GeneralSecurityException {
String keyPassword = getKeyPassword();
if (StringUtils.isEmpty(keyPassword)) {
this.keyPasswordArray = new char[0];
} else {
this.keyPasswordArray = keyPassword.toCharArray();
}
this.keyStore = this.loadKeyStore(trustsOnly, this.keyPasswordArray);
Set<X509Certificate> trustedCerts = getTrustedCerts(this.keyStore);
this.trusts = trustedCerts.toArray(new X509Certificate[0]);
if (!trustsOnly) {
PrivateKeyEntry privateKeyEntry = null;
String keyAlias = this.getKeyAlias();
if (!StringUtils.isEmpty(keyAlias)) {
privateKeyEntry = tryGetPrivateKeyEntry(this.keyStore, keyAlias, this.keyPasswordArray);
} else {
for (Enumeration<String> e = this.keyStore.aliases(); e.hasMoreElements(); ) {
String alias = e.nextElement();
privateKeyEntry = tryGetPrivateKeyEntry(this.keyStore, alias, this.keyPasswordArray);
if (privateKeyEntry != null) {
break;
}
}
}
if (privateKeyEntry == null) {
throw new IllegalArgumentException("Failed to get private key from the keystore or pem files. " +
"Please check if the private key exists in the keystore or pem files and if the provided private key password is valid.");
}
this.chain = asX509Certificates(privateKeyEntry.getCertificateChain());
this.privateKey = privateKeyEntry.getPrivateKey();
if (this.chain.length > 0) {
this.publicKey = this.chain[0].getPublicKey();
}
}
}
@Override
public PrivateKey getPrivateKey() {
return this.privateKey;
}
@Override
public PublicKey getPublicKey() {
return this.publicKey;
}
@Override
public X509Certificate[] getCertificateChain() {
return this.chain;
}
@Override
public X509Certificate[] getTrustedCertificates() {
return this.trusts;
}
@Override
public TrustManagerFactory createTrustManagerFactory() throws NoSuchAlgorithmException, KeyStoreException {
TrustManagerFactory tmFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmFactory.init(this.keyStore);
return tmFactory;
}
@Override
public KeyManagerFactory createKeyManagerFactory() throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException {
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(this.keyStore, this.keyPasswordArray);
return kmf;
}
protected abstract boolean canUse();
protected abstract String getKeyPassword();
protected abstract String getKeyAlias();
protected abstract KeyStore loadKeyStore(boolean isPrivateKeyRequired, char[] keyPasswordArray) throws IOException, GeneralSecurityException;
private static X509Certificate[] asX509Certificates(Certificate[] certificates) {
if (null == certificates || 0 == certificates.length) {
throw new IllegalArgumentException("certificates missing!");
}
X509Certificate[] x509Certificates = new X509Certificate[certificates.length];
for (int index = 0; certificates.length > index; ++index) {
if (null == certificates[index]) {
throw new IllegalArgumentException("[" + index + "] is null!");
}
try {
x509Certificates[index] = (X509Certificate) certificates[index];
} catch (ClassCastException e) {
throw new IllegalArgumentException("[" + index + "] is not a x509 certificate! Instead it's a "
+ certificates[index].getClass().getName());
}
}
return x509Certificates;
}
private static PrivateKeyEntry tryGetPrivateKeyEntry(KeyStore keyStore, String alias, char[] pwd) {
PrivateKeyEntry entry = null;
try {
if (keyStore.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class)) {
try {
entry = (KeyStore.PrivateKeyEntry) keyStore
.getEntry(alias, new KeyStore.PasswordProtection(pwd));
} catch (UnsupportedOperationException e) {
PrivateKey key = (PrivateKey) keyStore.getKey(alias, pwd);
Certificate[] certs = keyStore.getCertificateChain(alias);
entry = new KeyStore.PrivateKeyEntry(key, certs);
}
}
} catch (KeyStoreException | UnrecoverableEntryException | NoSuchAlgorithmException ignored) {}
return entry;
}
private static Set<X509Certificate> getTrustedCerts(KeyStore ks) {
Set<X509Certificate> set = new HashSet<>();
try {
for (Enumeration<String> e = ks.aliases(); e.hasMoreElements(); ) {
String alias = e.nextElement();
if (ks.isCertificateEntry(alias)) {
Certificate cert = ks.getCertificate(alias);
if (cert instanceof X509Certificate) {
set.add((X509Certificate)cert);
}
} else if (ks.isKeyEntry(alias)) {
Certificate[] certs = ks.getCertificateChain(alias);
if ((certs != null) && (certs.length > 0) &&
(certs[0] instanceof X509Certificate)) {
set.add((X509Certificate)certs[0]);
}
}
}
} catch (KeyStoreException ignored) {}
return Collections.unmodifiableSet(set);
}
}

52
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/KeystoreSslCredentials.java

@ -0,0 +1,52 @@
/**
* Copyright © 2016-2021 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.transport.config.ssl;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.ResourceUtils;
import org.thingsboard.server.common.data.StringUtils;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
@Data
@EqualsAndHashCode(callSuper = false)
public class KeystoreSslCredentials extends AbstractSslCredentials {
private String type;
private String storeFile;
private String storePassword;
private String keyPassword;
private String keyAlias;
@Override
protected boolean canUse() {
return ResourceUtils.resourceExists(this, this.storeFile);
}
@Override
protected KeyStore loadKeyStore(boolean trustsOnly, char[] keyPasswordArray) throws IOException, GeneralSecurityException {
String keyStoreType = StringUtils.isEmpty(this.type) ? KeyStore.getDefaultType() : this.type;
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
try (InputStream tsFileInputStream = ResourceUtils.getInputStream(this, this.storeFile)) {
keyStore.load(tsFileInputStream, StringUtils.isEmpty(this.storePassword) ? new char[0] : this.storePassword.toCharArray());
}
return keyStore;
}
}

132
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/PemSslCredentials.java

@ -0,0 +1,132 @@
/**
* Copyright © 2016-2021 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.transport.config.ssl;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.thingsboard.server.common.data.ResourceUtils;
import org.thingsboard.server.common.data.StringUtils;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.CertPath;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Data
@EqualsAndHashCode(callSuper = false)
public class PemSslCredentials extends AbstractSslCredentials {
private String certFile;
private String keyFile;
private String keyPassword;
private final String keyAlias = "serveralias";
@Override
protected boolean canUse() {
return ResourceUtils.resourceExists(this, this.certFile);
}
@Override
protected KeyStore loadKeyStore(boolean trustsOnly, char[] keyPasswordArray) throws IOException, GeneralSecurityException {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
List<X509Certificate> certificates = new ArrayList<>();
PrivateKey privateKey = null;
JcaX509CertificateConverter certConverter = new JcaX509CertificateConverter();
JcaPEMKeyConverter keyConverter = new JcaPEMKeyConverter();
try (InputStream inStream = ResourceUtils.getInputStream(this, this.certFile)) {
try (PEMParser pemParser = new PEMParser(new InputStreamReader(inStream))) {
Object object;
while((object = pemParser.readObject()) != null) {
if (object instanceof X509CertificateHolder) {
X509Certificate x509Cert = certConverter.getCertificate((X509CertificateHolder) object);
certificates.add(x509Cert);
} else if (object instanceof PEMEncryptedKeyPair) {
PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(keyPasswordArray);
privateKey = keyConverter.getKeyPair(((PEMEncryptedKeyPair) object).decryptKeyPair(decProv)).getPrivate();
} else if (object instanceof PEMKeyPair) {
privateKey = keyConverter.getKeyPair((PEMKeyPair) object).getPrivate();
} else if (object instanceof PrivateKeyInfo) {
privateKey = keyConverter.getPrivateKey((PrivateKeyInfo) object);
}
}
}
}
if (privateKey == null && !StringUtils.isEmpty(this.keyFile)) {
if (ResourceUtils.resourceExists(this, this.keyFile)) {
try (InputStream inStream = ResourceUtils.getInputStream(this, this.keyFile)) {
try (PEMParser pemParser = new PEMParser(new InputStreamReader(inStream))) {
Object object;
while ((object = pemParser.readObject()) != null) {
if (object instanceof PEMEncryptedKeyPair) {
PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(keyPasswordArray);
privateKey = keyConverter.getKeyPair(((PEMEncryptedKeyPair) object).decryptKeyPair(decProv)).getPrivate();
break;
} else if (object instanceof PEMKeyPair) {
privateKey = keyConverter.getKeyPair((PEMKeyPair) object).getPrivate();
break;
} else if (object instanceof PrivateKeyInfo) {
privateKey = keyConverter.getPrivateKey((PrivateKeyInfo) object);
}
}
}
}
}
}
if (certificates.isEmpty()) {
throw new IllegalArgumentException("No certificates found in certFile: " + this.certFile);
}
if (privateKey == null && !trustsOnly) {
throw new IllegalArgumentException("Unable to load private key neither from certFile: " + this.certFile + " nor from keyFile: " + this.keyFile);
}
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null);
if (trustsOnly) {
List<Certificate> unique = certificates.stream().distinct().collect(Collectors.toList());
for (int i = 0; i < unique.size(); i++) {
keyStore.setCertificateEntry("root-" + i, unique.get(i));
}
}
if (privateKey != null) {
CertificateFactory factory = CertificateFactory.getInstance("X.509");
CertPath certPath = factory.generateCertPath(certificates);
List<? extends Certificate> path = certPath.getCertificates();
Certificate[] x509Certificates = path.toArray(new Certificate[0]);
keyStore.setKeyEntry(this.keyAlias, privateKey, keyPasswordArray, x509Certificates);
}
return keyStore;
}
}

45
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentials.java

@ -0,0 +1,45 @@
/**
* Copyright © 2016-2021 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.transport.config.ssl;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.X509Certificate;
public interface SslCredentials {
void init(boolean trustsOnly) throws IOException, GeneralSecurityException;
PrivateKey getPrivateKey();
PublicKey getPublicKey();
X509Certificate[] getCertificateChain();
X509Certificate[] getTrustedCertificates();
TrustManagerFactory createTrustManagerFactory() throws NoSuchAlgorithmException, KeyStoreException;
KeyManagerFactory createKeyManagerFactory() throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException;
}

66
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsConfig.java

@ -0,0 +1,66 @@
/**
* Copyright © 2016-2021 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.transport.config.ssl;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.PostConstruct;
@Slf4j
@Data
public class SslCredentialsConfig {
private boolean enabled = true;
private SslCredentialsType type;
private PemSslCredentials pem;
private KeystoreSslCredentials keystore;
private SslCredentials credentials;
private final String name;
private final boolean trustsOnly;
public SslCredentialsConfig(String name, boolean trustsOnly) {
this.name = name;
this.trustsOnly = trustsOnly;
}
@PostConstruct
public void init() {
if (this.enabled) {
log.info("{}: Initializing SSL credentials.", name);
if (SslCredentialsType.PEM.equals(type) && pem.canUse()) {
this.credentials = this.pem;
} else if (keystore.canUse()) {
if (SslCredentialsType.PEM.equals(type)) {
log.warn("{}: Specified PEM configuration is not valid. Using SSL keystore configuration as fallback.", name);
}
this.credentials = this.keystore;
} else {
throw new RuntimeException(name + ": Invalid SSL credentials configuration. None of the PEM or KEYSTORE configurations can be used!");
}
try {
this.credentials.init(this.trustsOnly);
} catch (Exception e) {
throw new RuntimeException(name + ": Failed to init SSL credentials configuration.", e);
}
} else {
log.info("{}: Skipping initialization of disabled SSL credentials.", name);
}
}
}

21
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/config/ssl/SslCredentialsType.java

@ -0,0 +1,21 @@
/**
* Copyright © 2016-2021 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.transport.config.ssl;
public enum SslCredentialsType {
PEM,
KEYSTORE
}

7
dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java

@ -389,6 +389,7 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
} else {
entityTypeStr = "'" + entityType.name() + "'";
}
if (!StringUtils.isEmpty(entityFieldsSelection)) {
entityFieldsSelection = String.format("e.id id, %s entity_type, %s", entityTypeStr, entityFieldsSelection);
} else {
@ -442,10 +443,10 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
EntityKeyMapping sortOrderMapping = sortOrderMappingOpt.get();
String direction = sortOrder.getDirection() == EntityDataSortOrder.Direction.ASC ? "asc" : "desc";
if (sortOrderMapping.getEntityKey().getType() == EntityKeyType.ENTITY_FIELD) {
dataQuery = String.format("%s order by %s %s", dataQuery, sortOrderMapping.getValueAlias(), direction);
dataQuery = String.format("%s order by %s %s, result.id %s", dataQuery, sortOrderMapping.getValueAlias(), direction, direction);
} else {
dataQuery = String.format("%s order by %s %s, %s %s", dataQuery,
sortOrderMapping.getSortOrderNumAlias(), direction, sortOrderMapping.getSortOrderStrAlias(), direction);
dataQuery = String.format("%s order by %s %s, %s %s, result.id %s", dataQuery,
sortOrderMapping.getSortOrderNumAlias(), direction, sortOrderMapping.getSortOrderStrAlias(), direction, direction);
}
}
}

6
docker/haproxy/config/haproxy.cfg

@ -70,13 +70,12 @@ frontend http-in
acl transport_http_acl path_beg /api/v1/
acl letsencrypt_http_acl path_beg /.well-known/acme-challenge/
acl tb_api_acl path_beg /api/ /swagger /webjars /v2/ /static/rulenode/ /oauth2/ /login/oauth2/ /static/widgets/
acl tb_rulenode_assets path_reg ^/assets/help/.*/rulenode/.*$
redirect scheme https if !letsencrypt_http_acl !transport_http_acl { env(FORCE_HTTPS_REDIRECT) -m str true }
use_backend letsencrypt_http if letsencrypt_http_acl
use_backend tb-http-backend if transport_http_acl
use_backend tb-api-backend if tb_api_acl or tb_rulenode_assets
use_backend tb-api-backend if tb_api_acl
default_backend tb-web-backend
@ -89,10 +88,9 @@ frontend https_in
acl transport_http_acl path_beg /api/v1/
acl tb_api_acl path_beg /api/ /swagger /webjars /v2/ /static/rulenode/ /oauth2/ /login/oauth2/ /static/widgets/
acl tb_rulenode_assets path_reg ^/assets/help/.*/rulenode/.*$
use_backend tb-http-backend if transport_http_acl
use_backend tb-api-backend if tb_api_acl or tb_rulenode_assets
use_backend tb-api-backend if tb_api_acl
default_backend tb-web-backend

4
msa/web-ui/server.js

@ -83,10 +83,6 @@ var server;
apiProxy.web(req, res);
});
app.all('/assets/help/*/rulenode/**', (req, res) => {
apiProxy.web(req, res);
});
server.on('upgrade', (req, socket, head) => {
apiProxy.ws(req, socket, head);
});

9
rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java

@ -57,6 +57,7 @@ import org.thingsboard.server.common.data.EntityViewInfo;
import org.thingsboard.server.common.data.Event;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.Tenant;
@ -1131,11 +1132,8 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
public Optional<Device> saveDeviceWithCredentials(Device device, DeviceCredentials credentials) {
try {
Map<Class<?>, Object> deviceCredentials = new ConcurrentHashMap<>();
deviceCredentials.put(Device.class, device);
deviceCredentials.put(DeviceCredentials.class, credentials);
// return restTemplate.postForEntity(baseURL + "/api/lwm2m/device-credentials", deviceCredentials, Device.class).getBody();
ResponseEntity<Device> deviceOpt = restTemplate.postForEntity(baseURL + "/api/lwm2m/device-credentials", deviceCredentials, Device.class);
SaveDeviceWithCredentialsRequest request = new SaveDeviceWithCredentialsRequest(device, credentials);
ResponseEntity<Device> deviceOpt = restTemplate.postForEntity(baseURL + "/api/device-with-credentials", request, Device.class);
return Optional.ofNullable(deviceOpt.getBody());
} catch (HttpClientErrorException exception) {
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
@ -1146,7 +1144,6 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable {
}
}
public PageData<Device> getTenantDevices(String type, PageLink pageLink) {
Map<String, String> params = new HashMap<>();
params.put("type", type);

36
transport/coap/src/main/resources/tb-coap-transport.yml

@ -98,17 +98,33 @@ transport:
bind_address: "${COAP_DTLS_BIND_ADDRESS:0.0.0.0}"
# CoAP DTLS bind port
bind_port: "${COAP_DTLS_BIND_PORT:5684}"
# Path to the key store that holds the certificate
key_store: "${COAP_DTLS_KEY_STORE:coapserver.jks}"
# Password used to access the key store
key_store_password: "${COAP_DTLS_KEY_STORE_PASSWORD:server_ks_password}"
# Password used to access the key
key_password: "${COAP_DTLS_KEY_PASSWORD:server_key_password}"
# Key alias
key_alias: "${COAP_DTLS_KEY_ALIAS:serveralias}"
# Skip certificate validity check for client certificates.
skip_validity_check_for_client_cert: "${COAP_DTLS_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}"
# Server DTLS credentials
credentials:
# Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore)
type: "${COAP_DTLS_CREDENTIALS_TYPE:PEM}"
# PEM server credentials
pem:
# Path to the server certificate file (holds server certificate or certificate chain, may include server private key)
cert_file: "${COAP_DTLS_PEM_CERT:coapserver.pem}"
# Path to the server certificate private key file (optional)
key_file: "${COAP_DTLS_PEM_KEY:coapserver_key.pem}"
# Server certificate private key password (optional)
key_password: "${COAP_DTLS_PEM_KEY_PASSWORD:server_key_password}"
# Keystore server credentials
keystore:
# Type of the key store
type: "${COAP_DTLS_KEY_STORE_TYPE:JKS}"
# Path to the key store that holds the SSL certificate
store_file: "${COAP_DTLS_KEY_STORE:coapserver.jks}"
# Password used to access the key store
store_password: "${COAP_DTLS_KEY_STORE_PASSWORD:server_ks_password}"
# Password used to access the key
key_password: "${COAP_DTLS_KEY_PASSWORD:server_key_password}"
# Key alias
key_alias: "${COAP_DTLS_KEY_ALIAS:serveralias}"
x509:
# Skip certificate validity check for client certificates.
skip_validity_check_for_client_cert: "${TB_COAP_X509_DTLS_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}"
dtls_session_inactivity_timeout: "${TB_COAP_X509_DTLS_SESSION_INACTIVITY_TIMEOUT:86400000}"
dtls_session_report_timeout: "${TB_COAP_X509_DTLS_SESSION_REPORT_TIMEOUT:1800000}"
sessions:

83
transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml

@ -111,9 +111,33 @@ transport:
security:
bind_address: "${LWM2M_SECURITY_BIND_ADDRESS:0.0.0.0}"
bind_port: "${LWM2M_SECURITY_BIND_PORT:5686}"
# Server X509 Certificates support
credentials:
# Whether to enable LWM2M server X509 Certificate/RPK support
enabled: "${LWM2M_SERVER_CREDENTIALS_ENABLED:false}"
# Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore)
type: "${LWM2M_SERVER_CREDENTIALS_TYPE:PEM}"
# PEM server credentials
pem:
# Path to the server certificate file (holds server certificate or certificate chain, may include server private key)
cert_file: "${LWM2M_SERVER_PEM_CERT:lwm2mserver.pem}"
# Path to the server certificate private key file (optional)
key_file: "${LWM2M_SERVER_PEM_KEY:lwm2mserver_key.pem}"
# Server certificate private key password (optional)
key_password: "${LWM2M_SERVER_PEM_KEY_PASSWORD:server_key_password}"
# Keystore server credentials
keystore:
# Type of the key store
type: "${LWM2M_SERVER_KEY_STORE_TYPE:JKS}"
# Path to the key store that holds the SSL certificate
store_file: "${LWM2M_SERVER_KEY_STORE:lwm2mserver.jks}"
# Password used to access the key store
store_password: "${LWM2M_SERVER_KEY_STORE_PASSWORD:server_ks_password}"
# Password used to access the key
key_password: "${LWM2M_SERVER_KEY_PASSWORD:server_key_password}"
# Key alias
key_alias: "${LWM2M_SERVER_KEY_ALIAS:server}"
# Only Certificate_x509:
key_alias: "${LWM2M_SERVER_KEY_ALIAS:server}"
key_password: "${LWM2M_SERVER_KEY_PASSWORD:server_ks_password}"
skip_validity_check_for_client_cert: "${TB_LWM2M_SERVER_SECURITY_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}"
bootstrap:
enable: "${LWM2M_ENABLED_BS:true}"
@ -123,18 +147,51 @@ transport:
security:
bind_address: "${LWM2M_BS_SECURITY_BIND_ADDRESS:0.0.0.0}"
bind_port: "${LWM2M_BS_SECURITY_BIND_PORT:5688}"
# Only Certificate_x509:
key_alias: "${LWM2M_BS_KEY_ALIAS:bootstrap}"
key_password: "${LWM2M_BS_KEY_PASSWORD:server_ks_password}"
# Bootstrap server X509 Certificates support
credentials:
# Whether to enable LWM2M bootstrap server X509 Certificate/RPK support
enabled: "${LWM2M_BS_CREDENTIALS_ENABLED:false}"
# Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore)
type: "${LWM2M_BS_CREDENTIALS_TYPE:PEM}"
# PEM server credentials
pem:
# Path to the server certificate file (holds server certificate or certificate chain, may include server private key)
cert_file: "${LWM2M_BS_PEM_CERT:lwm2mserver.pem}"
# Path to the server certificate private key file (optional)
key_file: "${LWM2M_BS_PEM_KEY:lwm2mserver_key.pem}"
# Server certificate private key password (optional)
key_password: "${LWM2M_BS_PEM_KEY_PASSWORD:server_key_password}"
# Keystore server credentials
keystore:
# Type of the key store
type: "${LWM2M_BS_KEY_STORE_TYPE:JKS}"
# Path to the key store that holds the SSL certificate
store_file: "${LWM2M_BS_KEY_STORE:lwm2mserver.jks}"
# Password used to access the key store
store_password: "${LWM2M_BS_KEY_STORE_PASSWORD:server_ks_password}"
# Password used to access the key
key_password: "${LWM2M_BS_KEY_PASSWORD:server_key_password}"
# Key alias
key_alias: "${LWM2M_BS_KEY_ALIAS:bootstrap}"
security:
# Certificate_x509:
# To get helps about files format and how to generate it, see: https://github.com/eclipse/leshan/wiki/Credential-files-format
# Create new X509 Certificates: common/transport/lwm2m/src/main/resources/credentials/shell/lwM2M_credentials.sh
key_store_type: "${LWM2M_KEYSTORE_TYPE:JKS}"
# key_store_path_file: "${KEY_STORE_PATH_FILE:/common/transport/lwm2m/src/main/resources/credentials/serverKeyStore.jks"
key_store: "${LWM2M_KEYSTORE:lwm2mserver.jks}"
key_store_password: "${LWM2M_KEYSTORE_PASSWORD:server_ks_password}"
root_alias: "${LWM2M_SERVER_ROOT_CA_ALIAS:rootca}"
# X509 trust certificates
trust-credentials:
# Whether to load X509 trust certificates
enabled: "${LWM2M_TRUST_CREDENTIALS_ENABLED:false}"
# Trust certificates store type (PEM - pem certificates file; KEYSTORE - java keystore)
type: "${LWM2M_TRUST_CREDENTIALS_TYPE:PEM}"
# PEM certificates
pem:
# Path to the certificates file (holds trust certificates)
cert_file: "${LWM2M_TRUST_PEM_CERT:lwm2mserver.pem}"
# Keystore with trust certificates
keystore:
# Type of the key store
type: "${LWM2M_TRUST_KEY_STORE_TYPE:JKS}"
# Path to the key store that holds the X509 certificates
store_file: "${LWM2M_TRUST_KEY_STORE:lwm2mserver.jks}"
# Password used to access the key store
store_password: "${LWM2M_TRUST_KEY_STORE_PASSWORD:server_ks_password}"
recommended_ciphers: "${LWM2M_RECOMMENDED_CIPHERS:false}"
recommended_supported_groups: "${LWM2M_RECOMMENDED_SUPPORTED_GROUPS:true}"
timeout: "${LWM2M_TIMEOUT:120000}"

30
transport/mqtt/src/main/resources/tb-mqtt-transport.yml

@ -106,14 +106,28 @@ transport:
bind_port: "${MQTT_SSL_BIND_PORT:8883}"
# SSL protocol: See http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#SSLContext
protocol: "${MQTT_SSL_PROTOCOL:TLSv1.2}"
# Path to the key store that holds the SSL certificate
key_store: "${MQTT_SSL_KEY_STORE:mqttserver.jks}"
# Password used to access the key store
key_store_password: "${MQTT_SSL_KEY_STORE_PASSWORD:server_ks_password}"
# Password used to access the key
key_password: "${MQTT_SSL_KEY_PASSWORD:server_key_password}"
# Type of the key store
key_store_type: "${MQTT_SSL_KEY_STORE_TYPE:JKS}"
# Server SSL credentials
credentials:
# Server credentials type (PEM - pem certificate file; KEYSTORE - java keystore)
type: "${MQTT_SSL_CREDENTIALS_TYPE:PEM}"
# PEM server credentials
pem:
# Path to the server certificate file (holds server certificate or certificate chain, may include server private key)
cert_file: "${MQTT_SSL_PEM_CERT:mqttserver.pem}"
# Path to the server certificate private key file (optional)
key_file: "${MQTT_SSL_PEM_KEY:mqttserver_key.pem}"
# Server certificate private key password (optional)
key_password: "${MQTT_SSL_PEM_KEY_PASSWORD:server_key_password}"
# Keystore server credentials
keystore:
# Type of the key store
type: "${MQTT_SSL_KEY_STORE_TYPE:JKS}"
# Path to the key store that holds the SSL certificate
store_file: "${MQTT_SSL_KEY_STORE:mqttserver.jks}"
# Password used to access the key store
store_password: "${MQTT_SSL_KEY_STORE_PASSWORD:server_ks_password}"
# Password used to access the key
key_password: "${MQTT_SSL_KEY_PASSWORD:server_key_password}"
# Skip certificate validity check for client certificates.
skip_validity_check_for_client_cert: "${MQTT_SSL_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}"
sessions:

4
ui-ngx/proxy.conf.js

@ -26,10 +26,6 @@ const PROXY_CONFIG = {
"target": ruleNodeUiforwardUrl,
"secure": false,
},
"/assets/help/*/rulenode/**": {
"target": ruleNodeUiforwardUrl,
"secure": false,
},
"/static/widgets": {
"target": forwardUrl,
"secure": false,

43
ui-ngx/src/app/core/http/ui-settings.service.ts

@ -0,0 +1,43 @@
///
/// Copyright © 2016-2021 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { defaultHttpOptions, defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils';
import { Observable } from 'rxjs';
import { publishReplay, refCount } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class UiSettingsService {
private helpBaseUrlObservable: Observable<string>;
constructor(
private http: HttpClient
) { }
public getHelpBaseUrl(): Observable<string> {
if (!this.helpBaseUrlObservable) {
this.helpBaseUrlObservable = this.http.get('/api/uiSettings/helpBaseUrl', {responseType: 'text', ...defaultHttpOptions(true)}).pipe(
publishReplay(1),
refCount()
);
}
return this.helpBaseUrlObservable;
}
}

58
ui-ngx/src/app/core/services/help.service.ts

@ -18,23 +18,29 @@ import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { TranslateService } from '@ngx-translate/core';
import { Observable, of } from 'rxjs';
import { catchError, mergeMap, tap } from 'rxjs/operators';
import { helpBaseUrl } from '@shared/models/constants';
import { catchError, map, mergeMap, tap } from 'rxjs/operators';
import { helpBaseUrl as siteBaseUrl } from '@shared/models/constants';
import { UiSettingsService } from '@core/http/ui-settings.service';
const NOT_FOUND_CONTENT = '## Not found';
const localHelpBaseUrl = '/assets';
const NOT_FOUND_CONTENT: HelpData = {
content: '## Not found',
helpBaseUrl: localHelpBaseUrl
};
@Injectable({
providedIn: 'root'
})
export class HelpService {
private helpBaseUrl = helpBaseUrl;
private siteBaseUrl = siteBaseUrl;
private helpCache: {[lang: string]: {[key: string]: string}} = {};
constructor(
private translate: TranslateService,
private http: HttpClient
private http: HttpClient,
private uiSettingsService: UiSettingsService
) {}
getHelpContent(key: string): Observable<string> {
@ -70,13 +76,38 @@ export class HelpService {
}
}
private loadHelpContent(lang: string, key: string): Observable<string> {
return this.http.get(`/assets/help/${lang}/${key}.md`, {responseType: 'text'} );
private loadHelpContent(lang: string, key: string): Observable<HelpData> {
return this.uiSettingsService.getHelpBaseUrl().pipe(
mergeMap((helpBaseUrl) => {
return this.loadHelpContentFromBaseUrl(helpBaseUrl, lang, key).pipe(
catchError((e) => {
if (localHelpBaseUrl !== helpBaseUrl) {
return this.loadHelpContentFromBaseUrl(localHelpBaseUrl, lang, key);
} else {
throw e;
}
})
);
})
);
}
private loadHelpContentFromBaseUrl(helpBaseUrl: string, lang: string, key: string): Observable<HelpData> {
return this.http.get(`${helpBaseUrl}/help/${lang}/${key}.md`, {responseType: 'text'} ).pipe(
map((content) => {
return {
content,
helpBaseUrl
};
})
);
}
private processVariables(content: string): string {
const baseUrlReg = /\${baseUrl}/g;
return content.replace(baseUrlReg, this.helpBaseUrl);
private processVariables(helpData: HelpData): string {
const baseUrlReg = /\${siteBaseUrl}/g;
helpData.content = helpData.content.replace(baseUrlReg, this.siteBaseUrl);
const helpBaseUrlReg = /\${helpBaseUrl}/g;
return helpData.content.replace(helpBaseUrlReg, helpData.helpBaseUrl);
}
private processIncludes(content: string): Observable<string> {
@ -96,3 +127,8 @@ export class HelpService {
}
}
interface HelpData {
content: string;
helpBaseUrl: string;
}

7
ui-ngx/src/app/shared/components/help-popup.component.ts

@ -31,7 +31,7 @@ import { isDefinedAndNotNull } from '@core/utils';
@Component({
// tslint:disable-next-line:component-selector
selector: '[tb-help-popup]',
selector: '[tb-help-popup], [tb-help-popup-content]',
templateUrl: './help-popup.component.html',
styleUrls: ['./help-popup.component.scss'],
encapsulation: ViewEncapsulation.None
@ -44,6 +44,9 @@ export class HelpPopupComponent implements OnChanges, OnDestroy {
// tslint:disable-next-line:no-input-rename
@Input('tb-help-popup') helpId: string;
// tslint:disable-next-line:no-input-rename
@Input('tb-help-popup-content') helpContent: string;
// tslint:disable-next-line:no-input-rename
@Input('trigger-text') triggerText: string;
@ -82,7 +85,7 @@ export class HelpPopupComponent implements OnChanges, OnDestroy {
const trigger = this.textMode ? this.toggleHelpTextButton.nativeElement : this.toggleHelpButton.nativeElement;
this.popoverService.toggleHelpPopover(trigger, this.renderer, this.viewContainerRef,
this.helpId,
'',
this.helpContent,
(visible) => {
this.popoverVisible = visible;
}, (ready => {

70
ui-ngx/src/app/shared/components/marked-options.service.ts

@ -23,6 +23,7 @@ import { Tokenizer } from 'marked';
import * as marked from 'marked';
const copyCodeBlock = '{:copy-code}';
const codeStyleRegex = '^{:code-style="(.*)"}\n';
const autoBlock = '{:auto}';
const targetBlankBlock = '{:target=&quot;_blank&quot;}';
@ -70,13 +71,13 @@ export class MarkedOptionsService extends MarkedOptions {
};
marked.use({tokenizer});
this.renderer.code = (code: string, language: string | undefined, isEscaped: boolean) => {
if (code.endsWith(copyCodeBlock)) {
code = code.substring(0, code.length - copyCodeBlock.length);
const content = postProcessCodeContent(this.renderer2.code(code, language, isEscaped), code);
const codeContext = processCode(code);
if (codeContext.copyCode) {
const content = postProcessCodeContent(this.renderer2.code(codeContext.code, language, isEscaped), codeContext);
this.id++;
return this.wrapCopyCode(this.id, content, code);
return this.wrapCopyCode(this.id, content, codeContext);
} else {
return this.wrapDiv(postProcessCodeContent(this.renderer2.code(code, language, isEscaped), code));
return this.wrapDiv(postProcessCodeContent(this.renderer2.code(codeContext.code, language, isEscaped), codeContext));
}
};
this.renderer.table = (header: string, body: string) => {
@ -95,10 +96,11 @@ export class MarkedOptionsService extends MarkedOptions {
header: boolean;
align: 'center' | 'left' | 'right' | null;
}) => {
if (content.endsWith(copyCodeBlock)) {
content = content.substring(0, content.length - copyCodeBlock.length);
const codeContext = processCode(content);
codeContext.multiline = false;
if (codeContext.copyCode) {
this.id++;
content = this.wrapCopyCode(this.id, content, content);
content = this.wrapCopyCode(this.id, codeContext.code, codeContext);
}
return this.renderer2.tablecell(content, flags);
};
@ -119,10 +121,14 @@ export class MarkedOptionsService extends MarkedOptions {
return `<div>${content}</div>`;
}
private wrapCopyCode(id: number, content: string, code: string): string {
private wrapCopyCode(id: number, content: string, context: CodeContext): string {
let copyCodeButtonClass = 'clipboard-btn';
if (context.multiline) {
copyCodeButtonClass += ' multiline';
}
return `<div class="code-wrapper noChars" id="codeWrapper${id}" onClick="markdownCopyCode(${id})">${content}` +
`<span id="copyCodeId${id}" style="display: none;">${encodeURIComponent(code)}</span>` +
`<button class="clipboard-btn">\n` +
`<span id="copyCodeId${id}" style="display: none;">${encodeURIComponent(context.code)}</span>` +
`<button class="${copyCodeButtonClass}">\n` +
` <p>${this.translate.instant('markdown.copy-code')}</p>\n` +
` <div>\n` +
` <img src="/assets/copy-code-icon.svg" alt="${this.translate.instant('markdown.copy-code')}">\n` +
@ -187,13 +193,41 @@ export class MarkedOptionsService extends MarkedOptions {
}
}
function postProcessCodeContent(content: string, code: string): string {
const lineCount = code.trim().split('\n').length;
let replacement;
if (lineCount < 2) {
replacement = '<pre ngNonBindable class="no-line-numbers">';
} else {
replacement = '<pre ngNonBindable>';
interface CodeContext {
copyCode: boolean;
multiline: boolean;
codeStyle?: string;
code: string;
}
function processCode(code: string): CodeContext {
const context: CodeContext = {
copyCode: false,
multiline: false,
code
};
if (context.code.endsWith(copyCodeBlock)) {
context.code = context.code.substring(0, context.code.length - copyCodeBlock.length);
context.copyCode = true;
}
const codeStyleMatch = context.code.match(new RegExp(codeStyleRegex));
if (codeStyleMatch) {
context.codeStyle = codeStyleMatch[1];
context.code = context.code.replace(new RegExp(codeStyleRegex), '');
}
const lineCount = context.code.trim().split('\n').length;
context.multiline = lineCount > 1;
return context;
}
function postProcessCodeContent(content: string, context: CodeContext): string {
let replacement = '<pre ngNonBindable';
if (!context.multiline) {
replacement += ' class="no-line-numbers"';
}
if (context.codeStyle) {
replacement += ` style="${context.codeStyle}"`;
}
replacement += '>';
return content.replace('<pre>', replacement);
}

4
rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/clear_alarm_node_script_fn.md → ui-ngx/src/assets/help/en_US/rulenode/clear_alarm_node_script_fn.md

@ -58,11 +58,11 @@ return details;
<br>
More details about Alarms can be found in [this tutorial{:target="_blank"}](${baseUrl}/docs/user-guide/alarms/).
More details about Alarms can be found in [this tutorial{:target="_blank"}](${siteBaseUrl}/docs/user-guide/alarms/).
You can see the real life example, where this node is used, in the next tutorial:
- [Create and Clear Alarms{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/create-clear-alarms/)
- [Create and Clear Alarms{:target="_blank"}](${siteBaseUrl}/docs/user-guide/rule-engine-2-0/tutorials/create-clear-alarms/)
<br>
<br>

0
rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/common_node_script_args.md → ui-ngx/src/assets/help/en_US/rulenode/common_node_script_args.md

4
rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/create_alarm_node_script_fn.md → ui-ngx/src/assets/help/en_US/rulenode/create_alarm_node_script_fn.md

@ -59,11 +59,11 @@ return details;
<br>
More details about Alarms can be found in [this tutorial{:target="_blank"}](${baseUrl}/docs/user-guide/alarms/).
More details about Alarms can be found in [this tutorial{:target="_blank"}](${siteBaseUrl}/docs/user-guide/alarms/).
You can see the real life example, where this node is used, in the next tutorial:
- [Create and Clear Alarms{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/create-clear-alarms/)
- [Create and Clear Alarms{:target="_blank"}](${siteBaseUrl}/docs/user-guide/rule-engine-2-0/tutorials/create-clear-alarms/)
<br>
<br>

4
rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/filter_node_script_fn.md → ui-ngx/src/assets/help/en_US/rulenode/filter_node_script_fn.md

@ -61,8 +61,8 @@ return false;
You can see real life example, how to use this node in those tutorials:
- [Create and Clear Alarms{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/create-clear-alarms/#node-a-filter-script)
- [Reply to RPC Calls{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/rpc-reply-tutorial#add-filter-script-node)
- [Create and Clear Alarms{:target="_blank"}](${siteBaseUrl}/docs/user-guide/rule-engine-2-0/tutorials/create-clear-alarms/#node-a-filter-script)
- [Reply to RPC Calls{:target="_blank"}](${siteBaseUrl}/docs/user-guide/rule-engine-2-0/tutorials/rpc-reply-tutorial#add-filter-script-node)
<br>
<br>

0
rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/generator_node_script_fn.md → ui-ngx/src/assets/help/en_US/rulenode/generator_node_script_fn.md

2
rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/log_node_script_fn.md → ui-ngx/src/assets/help/en_US/rulenode/log_node_script_fn.md

@ -31,7 +31,7 @@ return 'Incoming message:\n' + JSON.stringify(msg) +
You can see real life example, how to use this node in this tutorial:
- [Reply to RPC Calls{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/rpc-reply-tutorial#log-unknown-request)
- [Reply to RPC Calls{:target="_blank"}](${siteBaseUrl}/docs/user-guide/rule-engine-2-0/tutorials/rpc-reply-tutorial#log-unknown-request)
<br>
<br>

2
rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/switch_node_script_fn.md → ui-ngx/src/assets/help/en_US/rulenode/switch_node_script_fn.md

@ -90,7 +90,7 @@ return [];
You can see real life example, how to use this node in this tutorial:
- [Data function based on telemetry from 2 devices{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/function-based-on-telemetry-from-two-devices#delta-temperature-rule-chain)
- [Data function based on telemetry from 2 devices{:target="_blank"}](${siteBaseUrl}/docs/user-guide/rule-engine-2-0/tutorials/function-based-on-telemetry-from-two-devices#delta-temperature-rule-chain)
<br>
<br>

4
rule-engine/rule-engine-components/src/main/resources/public/assets/help/en_US/rulenode/transformation_node_script_fn.md → ui-ngx/src/assets/help/en_US/rulenode/transformation_node_script_fn.md

@ -52,8 +52,8 @@ return {msg: msg, metadata: metadata, msgType: newType};
You can see real life example, how to use this node in those tutorials:
- [Transform incoming telemetry{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/transform-incoming-telemetry/)
- [Reply to RPC Calls{:target="_blank"}](${baseUrl}/docs/user-guide/rule-engine-2-0/tutorials/rpc-reply-tutorial#add-transform-script-node)
- [Transform incoming telemetry{:target="_blank"}](${siteBaseUrl}/docs/user-guide/rule-engine-2-0/tutorials/transform-incoming-telemetry/)
- [Reply to RPC Calls{:target="_blank"}](${siteBaseUrl}/docs/user-guide/rule-engine-2-0/tutorials/rpc-reply-tutorial#add-transform-script-node)
<br>
<br>

2
ui-ngx/src/assets/help/en_US/widget/action/custom_action_fn.md

@ -20,6 +20,7 @@ A JavaScript function performing custom action.
* Display alert dialog with entity information:
```javascript
{:code-style="max-height: 300px;"}
var title;
var content;
if (entityName) {
@ -52,6 +53,7 @@ function showAlertDialog(title, content) {
* Delete device after confirmation:
```javascript
{:code-style="max-height: 300px;"}
var $injector = widgetContext.$scope.$injector;
var dialogs = $injector.get(widgetContext.servicesMap.get('dialogs'));
var deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));

4
ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_html.md

@ -1,6 +1,7 @@
#### HTML template of dialog to create a device or an asset
```html
{:code-style="max-height: 400px;"}
<form #addEntityForm="ngForm" [formGroup]="addEntityFormGroup"
(ngSubmit)="save()" class="add-entity-form">
<mat-toolbar fxLayout="row" color="primary">
@ -158,3 +159,6 @@
</form>
{:copy-code}
```
<br>
<br>

4
ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_create_dialog_js.md

@ -1,6 +1,7 @@
#### Function displaying dialog to create a device or an asset
```javascript
{:code-style="max-height: 400px;"}
let $injector = widgetContext.$scope.$injector;
let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));
let assetService = $injector.get(widgetContext.servicesMap.get('assetService'));
@ -130,3 +131,6 @@ function AddEntityDialogController(instance) {
}
{:copy-code}
```
<br>
<br>

4
ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_html.md

@ -1,6 +1,7 @@
#### HTML template of dialog to edit a device or an asset
```html
{:code-style="max-height: 400px;"}
<form #editEntityForm="ngForm" [formGroup]="editEntityFormGroup"
(ngSubmit)="save()" class="edit-entity-form">
<mat-toolbar fxLayout="row" color="primary">
@ -190,3 +191,6 @@
</form>
{:copy-code}
```
<br>
<br>

4
ui-ngx/src/assets/help/en_US/widget/action/examples/custom_pretty_edit_dialog_js.md

@ -1,6 +1,7 @@
#### Function displaying dialog to edit a device or an asset
```javascript
{:code-style="max-height: 400px;"}
let $injector = widgetContext.$scope.$injector;
let customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));
let entityService = $injector.get(widgetContext.servicesMap.get('entityService'));
@ -218,3 +219,6 @@ function EditEntityDialogController(instance) {
}
{:copy-code}
```
<br>
<br>

2
ui-ngx/src/assets/help/en_US/widget/action/mobile_process_qr_code_fn.md

@ -34,7 +34,7 @@ function showQrCodeDialog(title, code, format) {
{:copy-code}
```
* Parse code as a device claiming info (in this case ```{deviceName: string, secretKey: string}```)<br>and then claim device (see [Claiming devices{:target="_blank"}](${baseUrl}/docs/user-guide/claiming-devices/) for details):
* Parse code as a device claiming info (in this case ```{deviceName: string, secretKey: string}```)<br>and then claim device (see [Claiming devices{:target="_blank"}](${siteBaseUrl}/docs/user-guide/claiming-devices/) for details):
```javascript
var $scope = widgetContext.$scope;

2
ui-ngx/src/assets/help/en_US/widget/editor/examples/alarm_widget.md

@ -127,7 +127,7 @@ self.onDataUpdated = function() {
- Click the **Run** button on the **Widget Editor Toolbar** in order to see the result in **Widget preview** section.
![image](${baseUrl}/images/user-guide/contribution/widgets/alarm-widget-sample.png)
![image](${helpBaseUrl}/help/images/widget/editor/examples/alarm-widget-sample.png)
In this example, the **alarmSource** and **alarms** properties of <span trigger-style="fontSize: 16px;" trigger-text="<b>subscription</b>" tb-help-popup="widget/editor/widget_js_subscription_object"></span> are assigned to **$scope** and become accessible within HTML template.

2
ui-ngx/src/assets/help/en_US/widget/editor/examples/ext_latest_values_example.md

@ -57,7 +57,7 @@ self.onDataUpdated = function() {
- Click the **Run** button on the **Widget Editor Toolbar** in order to see the result in **Widget preview** section.
![image](${baseUrl}/images/user-guide/contribution/widgets/external-js-widget-sample.png)
![image](${helpBaseUrl}/help/images/widget/editor/examples/external-js-widget-sample.png)
In this example, the external JS library API was used that becomes available after injecting the corresponding URL in **Resources** section.

2
ui-ngx/src/assets/help/en_US/widget/editor/examples/ext_timeseries_example.md

@ -98,7 +98,7 @@ self.onDataUpdated = function() {
- Click the **Run** button on the **Widget Editor Toolbar** in order to see the result in **Widget preview** section.
![image](${baseUrl}/images/user-guide/contribution/widgets/external-js-timeseries-widget-sample.png)
![image](${helpBaseUrl}/help/images/widget/editor/examples/external-js-timeseries-widget-sample.png)
In this example, the external JS library API was used that becomes available after injecting the corresponding URL in **Resources** section.

2
ui-ngx/src/assets/help/en_US/widget/editor/examples/latest_values_widget.md

@ -37,7 +37,7 @@ The **Widget Editor** will open, pre-populated with the content of the default *
- Click the **Run** button on the **Widget Editor Toolbar** in order to see the result in **Widget preview** section.
![image](${baseUrl}/images/user-guide/contribution/widgets/latest-values-widget-sample.png)
![image](${helpBaseUrl}/help/images/widget/editor/examples/latest-values-widget-sample.png)
In this example, the **data** property of <span trigger-style="fontSize: 16px;" trigger-text="<b>subscription</b>" tb-help-popup="widget/editor/widget_js_subscription_object"></span> is assigned to the **$scope** and becomes accessible within the HTML template.

18
ui-ngx/src/assets/help/en_US/widget/editor/examples/rpc_widget.md

@ -114,7 +114,7 @@ self.onInit = function() {
- Click the **Run** button on the **Widget Editor Toolbar** in order to see the result in **Widget preview** section.
- Click dashboard edit button on the preview section to change the size of the resulting widget. Then click dashboard apply button. The final widget should look like the image below.
![image](${baseUrl}/images/user-guide/contribution/widgets/control-widget-sample.png)
![image](${helpBaseUrl}/help/images/widget/editor/examples/control-widget-sample.png)
- Click the **Save** button on the **Widget Editor Toolbar** to save widget type.
@ -123,13 +123,13 @@ To test how this widget performs RPC commands, we will need to place it in a das
- Login as Tenant administrator.
- Navigate to **Devices** and create new device with some name, for ex. "My RPC Device".
- Open device details and click "Copy Access Token" button to copy device access token to clipboard.
- Download [mqtt-js-rpc-from-server.sh{:target="_blank"}](${baseUrl}/docs/reference/resources/mqtt-js-rpc-from-server.sh) and [mqtt-js-rpc-from-server.js{:target="_blank"}](${baseUrl}/docs/reference/resources/mqtt-js-rpc-from-server.js). Place these files in a folder.
- Download [mqtt-js-rpc-from-server.sh{:target="_blank"}](${siteBaseUrl}/docs/reference/resources/mqtt-js-rpc-from-server.sh) and [mqtt-js-rpc-from-server.js{:target="_blank"}](${siteBaseUrl}/docs/reference/resources/mqtt-js-rpc-from-server.js). Place these files in a folder.
Edit **mqtt-js-rpc-from-server.sh** - replace **$ACCESS_TOKEN** with your device access token from the clipboard. And install mqtt client library.
- Run **mqtt-js-rpc-from-server.sh** script. You should see a "connected" message in the console.
- Navigate to **Dashboards** and create a new dashboard with some name, for ex. "My first control dashboard". Open this dashboard.
- Click dashboard "edit" button. In the dashboard edit mode, click the "Entity aliases" button located on the dashboard toolbar.
![image](${baseUrl}/images/user-guide/contribution/widgets/dashboard-toolbar-entity-aliases.png)
![image](${helpBaseUrl}/help/images/widget/editor/examples/dashboard-toolbar-entity-aliases.png)
- Inside **Entity aliases** popup click "Add alias".
- Fill "Alias name" field, for ex. "My RPC Device Alias".
@ -137,12 +137,12 @@ To test how this widget performs RPC commands, we will need to place it in a das
- Choose "Device" in "Type" field.
- Select your device in "Entity list" field. In this example "My RPC Device".
![image](${baseUrl}/images/user-guide/contribution/widgets/add-rpc-device-alias.png)
![image](${helpBaseUrl}/help/images/widget/editor/examples/add-rpc-device-alias.png)
- Click "Add" and then "Save" in **Entity aliases**.
- Click dashboard "+" button then click "Create new widget" button.
![image](${baseUrl}/images/user-guide/contribution/widgets/dashboard-create-new-widget-button.png)
![image](${helpBaseUrl}/help/images/widget/editor/examples/dashboard-create-new-widget-button.png)
- Then select **Widget Bundle** where your RPC widget was saved. Select "Control widget" tab.
- Click your widget. In this example, "My first control widget".
@ -152,7 +152,7 @@ To test how this widget performs RPC commands, we will need to place it in a das
- Fill **RPC params** field with RPC params. For ex. "{ param1: "value1" }".
- Click **Send RPC command** button. You should see the following response in the widget.
![image](${baseUrl}/images/user-guide/contribution/widgets/control-widget-sample-response-one-way.png)
![image](${helpBaseUrl}/help/images/widget/editor/examples/control-widget-sample-response-one-way.png)
The following output should be printed in the device console:
@ -166,18 +166,18 @@ In order to test "Two way" RPC command mode, we need to change the corresponding
- Click dashboard "edit" button. In dashboard edit mode, click **Edit widget** button located in the header of Control widget.
- In the widget details, view select "Advanced" tab and uncheck "Is One Way Command" checkbox.
![image](${baseUrl}/images/user-guide/contribution/widgets/control-widget-sample-settings.png)
![image](${helpBaseUrl}/help/images/widget/editor/examples/control-widget-sample-settings.png)
- Click **Apply changes** button on the widget details header. Close details and click dashboard **Apply changes** button.
- Fill widget fields with RPC method name and params like in previous steps.
Click **Send RPC command** button. You should see the following response in the widget.
![image](${baseUrl}/images/user-guide/contribution/widgets/control-widget-sample-response-two-way.png)
![image](${helpBaseUrl}/help/images/widget/editor/examples/control-widget-sample-response-two-way.png)
- stop **mqtt-js-rpc-from-server.sh** script.
Click **Send RPC command** button. You should see the following response in the widget.
![image](${baseUrl}/images/user-guide/contribution/widgets/control-widget-sample-response-timeout.png)
![image](${helpBaseUrl}/help/images/widget/editor/examples/control-widget-sample-response-timeout.png)
In this example, **controlApi** is used to send RPC commands. Additionally, custom widget settings were introduced in order to configure RPC command mode and RPC request timeout.

2
ui-ngx/src/assets/help/en_US/widget/editor/examples/static_widget.md

@ -58,7 +58,7 @@ self.onInit = function() {
- Click the **Run** button on the **Widget Editor Toolbar** to see the resulting **Widget preview** section.
![image](${baseUrl}/images/user-guide/contribution/widgets/static-widget-sample.png)
![image](${helpBaseUrl}/help/images/widget/editor/examples/static-widget-sample.png)
This is just a static HTML widget. There is no subscription data and no special widget API was used.

2
ui-ngx/src/assets/help/en_US/widget/editor/examples/timeseries_widget.md

@ -75,7 +75,7 @@ self.onDataUpdated = function() {
- Click the **Run** button on the **Widget Editor Toolbar** in order to see the result in **Widget preview** section.
![image](${baseUrl}/images/user-guide/contribution/widgets/timeseries-widget-sample.png)
![image](${helpBaseUrl}/help/images/widget/editor/examples/timeseries-widget-sample.png)
In this example, the <span trigger-style="fontSize: 16px;" trigger-text="<b>subscription</b>" tb-help-popup="widget/editor/widget_js_subscription_object"></span> **datasources** and **data** properties are assigned to **$scope** and become accessible within the HTML template.

4
ui-ngx/src/assets/help/en_US/widget/editor/widget_js_fn.md

@ -3,7 +3,7 @@
<div class="divider"></div>
<br/>
All widget related JavaScript code according to the [Widget API{:target="_blank"}](${baseUrl}/docs/user-guide/contribution/widgets-development/#basic-widget-api).
All widget related JavaScript code according to the [Widget API{:target="_blank"}](${siteBaseUrl}/docs/user-guide/contribution/widgets-development/#basic-widget-api).
The built-in variable **self** is a reference to the widget instance.<br>
Each widget function should be defined as a property of the **self** variable.
**self** variable has property **ctx** of type [WidgetContext{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/models/widget-component.models.ts#L107) - a reference to widget context that has all necessary API and data used by widget instance.
@ -129,7 +129,7 @@ Browser debugger (if enabled) will automatically pause code execution at the deb
##### Further reading
For more information read [Widgets Development Guide{:target="_blank"}](${baseUrl}/docs/user-guide/contribution/widgets-development).
For more information read [Widgets Development Guide{:target="_blank"}](${siteBaseUrl}/docs/user-guide/contribution/widgets-development).
<br>
<br>

8
ui-ngx/src/assets/help/en_US/widget/editor/widget_js_subscription_object.md

@ -3,10 +3,10 @@
<div class="divider"></div>
<br/>
The widget subscription object is instance of [IWidgetSubscription{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/core/api/widget-api.models.ts#L264") and contains all subscription information, including current data, according to the [widget type{:target="_blank"}](${baseUrl}/docs/user-guide/ui/widget-library/#widget-types).
The widget subscription object is instance of [IWidgetSubscription{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/core/api/widget-api.models.ts#L264") and contains all subscription information, including current data, according to the [widget type{:target="_blank"}](${siteBaseUrl}/docs/user-guide/ui/widget-library/#widget-types).
Depending on widget type, subscription object provides different data structures.
For [Latest values{:target="_blank"}](${baseUrl}/docs/user-guide/ui/widget-library/#latest-values) and [Time-series{:target="_blank"}](${baseUrl}/docs/user-guide/ui/widget-library/#time-series) widget types, it provides the following properties:
For [Latest values{:target="_blank"}](${siteBaseUrl}/docs/user-guide/ui/widget-library/#latest-values) and [Time-series{:target="_blank"}](${siteBaseUrl}/docs/user-guide/ui/widget-library/#time-series) widget types, it provides the following properties:
- **datasources** - array of datasources (Array<[Datasource{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L279)>) used by this subscription, using the following structure:
@ -54,7 +54,7 @@ For [Latest values{:target="_blank"}](${baseUrl}/docs/user-guide/ui/widget-libra
]
```
For [Alarm widget{:target="_blank"}](${baseUrl}/docs/user-guide/ui/widget-library/#alarm-widget) type it provides the following properties:
For [Alarm widget{:target="_blank"}](${siteBaseUrl}/docs/user-guide/ui/widget-library/#alarm-widget) type it provides the following properties:
- **alarmSource** - ([Datasource{:target="_blank"}](https://github.com/thingsboard/thingsboard/blob/2627fe51d491055d4140f16617ed543f7f5bd8f6/ui-ngx/src/app/shared/models/widget.models.ts#L279)) information about entity for which alarms are fetched, using the following structure:
@ -110,4 +110,4 @@ For [Alarm widget{:target="_blank"}](${baseUrl}/docs/user-guide/ui/widget-librar
]
```
For [RPC{:target="_blank"}](${baseUrl}/docs/user-guide/ui/widget-library/#rpc-control-widget) or [Static{:target="_blank"}](${baseUrl}/docs/user-guide/ui/widget-library/#static) widget types, subscription object is optional and does not contain necessary information.
For [RPC{:target="_blank"}](${siteBaseUrl}/docs/user-guide/ui/widget-library/#rpc-control-widget) or [Static{:target="_blank"}](${siteBaseUrl}/docs/user-guide/ui/widget-library/#static) widget types, subscription object is optional and does not contain necessary information.

2
ui-ngx/src/assets/help/en_US/widget/lib/markdown/markdown_text_fn.md

@ -33,7 +33,7 @@ return '# Some title\n - Entity name: ' + data[0]['entityName'];
<ul>
<li>
Display greetings for currently logged-in user.<br>
Let's assume widget has first datasource configured using <code>Current User</code> <a target="_blank" href="${baseUrl}/docs/user-guide/ui/aliases/#single-entity">Single entity</a> alias<br>
Let's assume widget has first datasource configured using <code>Current User</code> <a target="_blank" href="${siteBaseUrl}/docs/user-guide/ui/aliases/#single-entity">Single entity</a> alias<br>
and has data keys for <code>firstName</code>, <code>lastName</code> and <code>name</code> entity fields:
</li>
</ul>

2
ui-ngx/src/assets/help/en_US/widget/lib/qrcode/qrcode_text_fn.md

@ -34,7 +34,7 @@ return data[0] ? data[0]['entityName'] : '';
<li>
Prepare QR code text to use as device claiming info (in this case <code>{deviceName: string, secretKey: string}</code>).<br>
Let's assume device has <code>claimingData</code> attribute with string JSON value containing <code>secretKey</code> field<br>
(see <a target="_blank" href="${baseUrl}/docs/user-guide/claiming-devices/">Claiming devices</a>):
(see <a target="_blank" href="${siteBaseUrl}/docs/user-guide/claiming-devices/">Claiming devices</a>):
</li>
</ul>

BIN
ui-ngx/src/assets/help/images/widget/editor/examples/add-rpc-device-alias.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
ui-ngx/src/assets/help/images/widget/editor/examples/alarm-widget-sample.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample-response-one-way.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample-response-timeout.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

BIN
ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample-response-two-way.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample-settings.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
ui-ngx/src/assets/help/images/widget/editor/examples/control-widget-sample.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
ui-ngx/src/assets/help/images/widget/editor/examples/dashboard-create-new-widget-button.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
ui-ngx/src/assets/help/images/widget/editor/examples/dashboard-toolbar-entity-aliases.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
ui-ngx/src/assets/help/images/widget/editor/examples/external-js-timeseries-widget-sample.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
ui-ngx/src/assets/help/images/widget/editor/examples/external-js-widget-sample.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
ui-ngx/src/assets/help/images/widget/editor/examples/latest-values-widget-sample.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
ui-ngx/src/assets/help/images/widget/editor/examples/static-widget-sample.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
ui-ngx/src/assets/help/images/widget/editor/examples/timeseries-widget-sample.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

3
ui-ngx/src/styles.scss

@ -741,6 +741,9 @@ mat-label {
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
&.multiline {
right: 44px;
}
p {
padding: 8px;
top: 1px;

Loading…
Cancel
Save