diff --git a/application/pom.xml b/application/pom.xml index 70d1a676bc..96c9ab7b39 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT thingsboard application diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index d5460689b8..92fdfc6e48 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.netty.channel.EventLoopGroup; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.StringUtils; import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.rule.engine.api.RuleEngineAlarmService; @@ -39,6 +38,7 @@ import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; @@ -46,7 +46,6 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; diff --git a/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java b/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java index bbeed33449..360d5e54c0 100644 --- a/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java +++ b/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java @@ -33,9 +33,9 @@ import org.springframework.security.web.util.UrlUtils; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.dao.oauth2.OAuth2Configuration; import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.service.security.auth.oauth2.TbOAuth2ParameterNames; diff --git a/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java index 86675a79a8..ae6ce20baf 100644 --- a/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java +++ b/application/src/main/java/org/thingsboard/server/config/RateLimitProcessingFilter.java @@ -16,7 +16,6 @@ package org.thingsboard.server.config; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.security.authentication.BadCredentialsException; @@ -24,6 +23,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.GenericFilterBean; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; diff --git a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java index e58b00f9e6..c53cf1b504 100644 --- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java @@ -17,7 +17,6 @@ package org.thingsboard.server.config; import com.fasterxml.classmate.TypeResolver; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -333,7 +332,7 @@ public class SwaggerConfiguration { errorResponse("401 ", "Unauthorized (**Expired credentials**)", List.of( errorExample("credentials-expired", "Expired credentials", - ThingsboardCredentialsExpiredResponse.of("User password expired!", RandomStringUtils.randomAlphanumeric(30))) + ThingsboardCredentialsExpiredResponse.of("User password expired!", StringUtils.randomAlphanumeric(30))) ), ThingsboardCredentialsExpiredResponse.class ) ); diff --git a/application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java b/application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java index 965975c0ee..e5a217893e 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AbstractRpcController.java @@ -22,10 +22,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; 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.DataConstants; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java index d2e11086c7..858a7ee3c4 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -18,7 +18,6 @@ package org.thingsboard.server.controller; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; @@ -30,6 +29,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; diff --git a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java index db78b87f70..33792a703f 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuditLogController.java @@ -17,7 +17,6 @@ package org.thingsboard.server.controller; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; -import org.apache.commons.lang3.StringUtils; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; @@ -26,6 +25,7 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.exception.ThingsboardException; diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 7d2d2cd255..1e9958cc25 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -23,7 +23,6 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.support.DefaultMessageSourceResolvable; @@ -47,6 +46,7 @@ import org.thingsboard.server.common.data.EntityViewInfo; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.OtaPackageInfo; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.Tenant; @@ -84,7 +84,6 @@ import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.id.WidgetsBundleId; -import org.thingsboard.server.common.data.page.PageDataIterableByTenantIdEntityId; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; @@ -146,15 +145,12 @@ import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import javax.mail.MessagingException; import javax.servlet.http.HttpServletResponse; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; -import static org.thingsboard.server.controller.ControllerConstants.DEFAULT_PAGE_SIZE; import static org.thingsboard.server.controller.ControllerConstants.INCORRECT_TENANT_ID; import static org.thingsboard.server.controller.UserController.YOU_DON_T_HAVE_PERMISSION_TO_PERFORM_THIS_OPERATION; import static org.thingsboard.server.dao.service.Validator.validateId; diff --git a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java index b8e5da25de..31f64254af 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ComponentDescriptorController.java @@ -17,7 +17,6 @@ package org.thingsboard.server.controller; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; -import org.apache.commons.lang3.StringUtils; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -25,6 +24,7 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java index e45a195a2d..0baf89e0dc 100644 --- a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java +++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java @@ -19,7 +19,6 @@ import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; @@ -33,6 +32,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileInfo; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.page.PageData; diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java index b9c929f909..4498b51c6c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java @@ -35,7 +35,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -49,6 +48,7 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.exception.ThingsboardException; diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java index 3565af1ab0..432b35fd80 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java @@ -16,7 +16,6 @@ package org.thingsboard.server.controller.plugin; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.BeanCreationNotAllowedException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -28,6 +27,7 @@ import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.adapter.NativeWebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; @@ -49,7 +49,6 @@ import javax.websocket.SendResult; import javax.websocket.Session; import java.io.IOException; import java.net.URI; -import java.nio.ByteBuffer; import java.security.InvalidParameterException; import java.util.Queue; import java.util.Set; diff --git a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java index 39064c210c..79968187b6 100644 --- a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java +++ b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java @@ -20,13 +20,13 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -41,7 +41,6 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.audit.AuditLogService; -import org.thingsboard.server.queue.util.TbCoreComponent; import java.util.List; import java.util.Map; @@ -199,7 +198,7 @@ public class EntityActionService { } } TbMsg tbMsg = TbMsg.newMsg(msgType, entityId, customerId, metaData, TbMsgDataType.JSON, json.writeValueAsString(entityNode)); - if (tenantId.isNullUid()) { + if (tenantId == null || tenantId.isNullUid()) { if (entity instanceof HasTenantId) { tenantId = ((HasTenantId) entity).getTenantId(); } diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultRateLimitService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultRateLimitService.java index 55b23a99cc..525386d884 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultRateLimitService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultRateLimitService.java @@ -16,8 +16,8 @@ package org.thingsboard.server.service.apiusage; import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.msg.tools.TbRateLimits; diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java index d5c86a67db..f6a590eee2 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java @@ -16,10 +16,8 @@ package org.thingsboard.server.service.apiusage; import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.checkerframework.checker.nullness.qual.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -34,6 +32,7 @@ import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageStateMailMessage; import org.thingsboard.server.common.data.ApiUsageStateValue; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.exception.ThingsboardException; @@ -80,7 +79,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; diff --git a/application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java index 68c9e3117e..a397a4d0e9 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java @@ -27,13 +27,13 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java index 8b2126905f..01f7fa282e 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java @@ -21,8 +21,6 @@ import com.fasterxml.jackson.databind.node.TextNode; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.lang3.RandomStringUtils; -import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; @@ -31,6 +29,7 @@ import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MClientCredential; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode; @@ -44,15 +43,15 @@ import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryMappingC import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportColumnType; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.queue.util.TbCoreComponent; -import org.thingsboard.server.service.sync.ie.importing.csv.AbstractBulkImportService; -import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportColumnType; import org.thingsboard.server.service.entitiy.device.TbDeviceService; import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.sync.ie.importing.csv.AbstractBulkImportService; import java.util.Collection; import java.util.Collections; @@ -157,7 +156,7 @@ public class DeviceBulkImportService extends AbstractBulkImportService { private void setUpAccessTokenCredentials(Map fields, DeviceCredentials credentials) { credentials.setCredentialsId(Optional.ofNullable(fields.get(BulkImportColumnType.ACCESS_TOKEN)) - .orElseGet(() -> RandomStringUtils.randomAlphanumeric(20))); + .orElseGet(() -> StringUtils.randomAlphanumeric(20))); } private void setUpBasicMqttCredentials(Map fields, DeviceCredentials credentials) { diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java index 7fd42740c4..c88cbd8156 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceProvisionServiceImpl.java @@ -20,14 +20,14 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; @@ -51,7 +51,6 @@ import org.thingsboard.server.dao.device.provision.ProvisionFailedException; import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.device.provision.ProvisionResponse; import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.queue.TbQueueCallback; @@ -189,7 +188,7 @@ public class DeviceProvisionServiceImpl implements DeviceProvisionService { private ProvisionResponse processCreateDevice(ProvisionRequest provisionRequest, DeviceProfile profile) { try { if (StringUtils.isEmpty(provisionRequest.getDeviceName())) { - String newDeviceName = RandomStringUtils.randomAlphanumeric(20); + String newDeviceName = StringUtils.randomAlphanumeric(20); log.info("Device name not found in provision request. Generated name is: {}", newDeviceName); provisionRequest.setDeviceName(newDeviceName); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java index d7e861a9fb..29ff49d626 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java @@ -20,6 +20,7 @@ import lombok.Data; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; +import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.dashboard.DashboardService; @@ -61,6 +62,9 @@ import org.thingsboard.server.service.executors.GrpcCallbackExecutorService; @Lazy public class EdgeContextComponent { + @Autowired + private TbClusterService clusterService; + @Autowired private EdgeService edgeService; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 2f9b4c0498..3f8950f281 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -211,6 +211,7 @@ public final class EdgeGrpcSession implements Closeable { @Override public void onSuccess(Void result) { syncCompleted = true; + ctx.getClusterService().onEdgeEventUpdate(edge.getTenantId(), edge.getId()); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java index c8fdcc4537..3c2db96b31 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/DeviceMsgConstructor.java @@ -16,7 +16,8 @@ package org.thingsboard.server.service.edge.rpc.constructor; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.protobuf.ByteString; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; @@ -28,6 +29,7 @@ import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.RpcRequestMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; +import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.TbCoreComponent; import java.util.UUID; @@ -36,7 +38,8 @@ import java.util.UUID; @TbCoreComponent public class DeviceMsgConstructor { - protected static final ObjectMapper mapper = new ObjectMapper(); + @Autowired + private DataDecodingEncodingService dataDecodingEncodingService; public DeviceUpdateMsg constructDeviceUpdatedMsg(UpdateMsgType msgType, Device device, CustomerId customerId, String conflictName) { DeviceUpdateMsg.Builder builder = DeviceUpdateMsg.newBuilder() @@ -66,6 +69,9 @@ public class DeviceMsgConstructor { if (conflictName != null) { builder.setConflictName(conflictName); } + if (device.getDeviceData() != null) { + builder.setDeviceDataBytes(ByteString.copyFrom(dataDecodingEncodingService.encode(device.getDeviceData()))); + } return builder.build(); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java index ee10c7859b..c86fb98502 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AdminSettingsEdgeEventFetcher.java @@ -23,10 +23,10 @@ import freemarker.template.Configuration; import freemarker.template.Template; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.text.WordUtils; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java index 0e1dee7132..6d0f66a884 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java @@ -22,14 +22,15 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.RandomStringUtils; -import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.device.data.DeviceData; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -60,9 +61,11 @@ import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.queue.TbQueueCallback; import org.thingsboard.server.queue.TbQueueMsgMetadata; +import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.rpc.FromDeviceRpcResponseActorMsg; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.locks.ReentrantLock; @@ -71,6 +74,9 @@ import java.util.concurrent.locks.ReentrantLock; @TbCoreComponent public class DeviceEdgeProcessor extends BaseEdgeProcessor { + @Autowired + private DataDecodingEncodingService dataDecodingEncodingService; + private static final ReentrantLock deviceCreationLock = new ReentrantLock(); public ListenableFuture processDeviceFromEdge(TenantId tenantId, Edge edge, DeviceUpdateMsg deviceUpdateMsg) { @@ -88,7 +94,7 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor { } else { log.info("[{}] Device with name '{}' already exists on the cloud, but not related to this edge [{}]. deviceUpdateMsg [{}]." + "Creating a new device with random prefix and relate to this edge", tenantId, deviceName, edge.getId(), deviceUpdateMsg); - String newDeviceName = deviceUpdateMsg.getName() + "_" + RandomStringUtils.randomAlphabetic(15); + String newDeviceName = deviceUpdateMsg.getName() + "_" + StringUtils.randomAlphabetic(15); Device newDevice; try { newDevice = createDevice(tenantId, edge, deviceUpdateMsg, newDeviceName); @@ -192,6 +198,11 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor { deviceUpdateMsg.getDeviceProfileIdLSB())); device.setDeviceProfileId(deviceProfileId); } + Optional deviceDataOpt = + dataDecodingEncodingService.decode(deviceUpdateMsg.getDeviceDataBytes().toByteArray()); + if (deviceDataOpt.isPresent()) { + device.setDeviceData(deviceDataOpt.get()); + } Device savedDevice = deviceService.saveDevice(device); tbClusterService.onDeviceUpdated(savedDevice, device); return saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, EdgeEventActionType.CREDENTIALS_REQUEST, deviceId, null); @@ -232,6 +243,11 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor { deviceUpdateMsg.getDeviceProfileIdLSB())); device.setDeviceProfileId(deviceProfileId); } + Optional deviceDataOpt = + dataDecodingEncodingService.decode(deviceUpdateMsg.getDeviceDataBytes().toByteArray()); + if (deviceDataOpt.isPresent()) { + device.setDeviceData(deviceDataOpt.get()); + } if (created) { deviceValidator.validate(device, Device::getTenantId); device.setId(deviceId); @@ -244,7 +260,7 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor { DeviceCredentials deviceCredentials = new DeviceCredentials(); deviceCredentials.setDeviceId(new DeviceId(savedDevice.getUuidId())); deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); - deviceCredentials.setCredentialsId(org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric(20)); + deviceCredentials.setCredentialsId(StringUtils.randomAlphanumeric(20)); deviceCredentialsService.createDeviceCredentials(device.getTenantId(), deviceCredentials); } createRelationFromEdge(tenantId, edge.getId(), device.getId()); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java index e098d6b605..8de6ec9aac 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/entityview/DefaultTbEntityViewService.java @@ -59,7 +59,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; -import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.thingsboard.server.common.data.StringUtils.isBlank; @Service @AllArgsConstructor diff --git a/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java b/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java index 2cff9841da..2d90b8a16a 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DatabaseHelper.java @@ -20,12 +20,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVParser; -import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.ShortCustomerInfo; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.dashboard.DashboardService; diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java index f193b10a62..38ff5aacec 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java +++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java @@ -20,10 +20,9 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.CustomerId; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; import org.thingsboard.server.common.data.rule.RuleChain; diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlTsDatabaseUpgradeService.java index c1d41a6f6e..003f544b56 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlTsDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlTsDatabaseUpgradeService.java @@ -16,11 +16,11 @@ package org.thingsboard.server.service.install; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.dao.util.SqlTsDao; import java.io.File; diff --git a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java index 245dc4503d..16c054b2fc 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java @@ -16,12 +16,12 @@ package org.thingsboard.server.service.install; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.dao.util.TimescaleDBTsDao; import java.io.File; diff --git a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraTsLatestToSqlMigrateService.java b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraTsLatestToSqlMigrateService.java index 142bacd7ff..1aa7602a35 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraTsLatestToSqlMigrateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/migrate/CassandraTsLatestToSqlMigrateService.java @@ -16,12 +16,12 @@ package org.thingsboard.server.service.install.migrate; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.UUIDConverter; import org.thingsboard.server.dao.cassandra.CassandraCluster; import org.thingsboard.server.dao.model.sqlts.dictionary.TsKvDictionary; diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index 1eda4e027d..d02f50b37b 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -37,7 +37,10 @@ import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.id.*; +import org.thingsboard.server.common.data.id.EntityViewId; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; @@ -46,7 +49,11 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.FilterPredicateValue; -import org.thingsboard.server.common.data.queue.*; +import org.thingsboard.server.common.data.queue.ProcessingStrategy; +import org.thingsboard.server.common.data.queue.ProcessingStrategyType; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.data.queue.SubmitStrategy; +import org.thingsboard.server.common.data.queue.SubmitStrategyType; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChain; @@ -74,12 +81,11 @@ import org.thingsboard.server.service.install.TbRuleEngineQueueConfigService; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; -import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.thingsboard.server.common.data.StringUtils.isBlank; @Service @Profile("install") diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/RateLimitsUpdater.java b/application/src/main/java/org/thingsboard/server/service/install/update/RateLimitsUpdater.java index c136f97b52..87179b26b5 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/RateLimitsUpdater.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/RateLimitsUpdater.java @@ -15,10 +15,10 @@ */ package org.thingsboard.server.service.install.update; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; diff --git a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java index aa8e019317..f433066c06 100644 --- a/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java +++ b/application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java @@ -19,10 +19,8 @@ import com.fasterxml.jackson.databind.JsonNode; import freemarker.template.Configuration; import freemarker.template.Template; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Lazy; import org.springframework.core.NestedRuntimeException; @@ -39,6 +37,7 @@ import org.thingsboard.server.common.data.ApiFeature; import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageStateMailMessage; import org.thingsboard.server.common.data.ApiUsageStateValue; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; @@ -49,15 +48,12 @@ import org.thingsboard.server.queue.usagestats.TbApiUsageClient; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; import javax.mail.internet.MimeMessage; import java.io.ByteArrayInputStream; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Properties; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 062e4128d4..8ec29672de 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -41,7 +41,6 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; @@ -53,7 +52,6 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; -import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; @@ -68,11 +66,11 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg; import org.thingsboard.server.queue.discovery.NotificationsTopicService; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.provider.TbQueueProducerProvider; +import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.service.gateway_device.GatewayNotificationsService; import org.thingsboard.server.service.ota.OtaPackageStateService; import org.thingsboard.server.service.profile.TbDeviceProfileCache; -import java.util.HashSet; import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; @@ -167,7 +165,7 @@ public class DefaultTbClusterService implements TbClusterService { @Override public void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, TbMsg tbMsg, TbQueueCallback callback) { - if (tenantId.isNullUid()) { + if (tenantId == null || tenantId.isNullUid()) { if (entityId.getEntityType().equals(EntityType.TENANT)) { tenantId = TenantId.fromUUID(entityId.getId()); } else { diff --git a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java index d83508220b..5853516311 100644 --- a/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java +++ b/application/src/main/java/org/thingsboard/server/service/resource/DefaultTbResourceService.java @@ -16,7 +16,6 @@ package org.thingsboard.server.service.resource; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.eclipse.leshan.core.model.DDFFileParser; import org.eclipse.leshan.core.model.DefaultDDFFileValidator; import org.eclipse.leshan.core.model.InvalidDDFFileException; @@ -24,6 +23,7 @@ import org.eclipse.leshan.core.model.ObjectModel; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.User; diff --git a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java index 4b2bcb99da..c90e104ff4 100644 --- a/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java +++ b/application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java @@ -22,7 +22,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java index 28794fc77e..7937641d16 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenProcessingFilter.java @@ -17,7 +17,6 @@ package org.thingsboard.server.service.security.auth.jwt; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; @@ -26,6 +25,7 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken; import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; import org.thingsboard.server.service.security.model.token.RawAccessJwtToken; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtHeaderTokenExtractor.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtHeaderTokenExtractor.java index b29164fb6b..0b90cd8047 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtHeaderTokenExtractor.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtHeaderTokenExtractor.java @@ -15,9 +15,9 @@ */ package org.thingsboard.server.service.security.auth.jwt.extractor; -import org.apache.commons.lang3.StringUtils; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.config.ThingsboardSecurityConfiguration; import javax.servlet.http.HttpServletRequest; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtQueryTokenExtractor.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtQueryTokenExtractor.java index 17ed090807..cb40379c22 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtQueryTokenExtractor.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/extractor/JwtQueryTokenExtractor.java @@ -15,9 +15,9 @@ */ package org.thingsboard.server.service.security.auth.jwt.extractor; -import org.apache.commons.lang3.StringUtils; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.config.ThingsboardSecurityConfiguration; import javax.servlet.http.HttpServletRequest; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java index 0972b378eb..0f36273f41 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java @@ -16,23 +16,23 @@ package org.thingsboard.server.service.security.auth.mfa; import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.LockedException; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; +import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig; +import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType; import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager; -import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; -import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig; -import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFaProvider; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.system.SystemSecurityService; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java index bafc1da9f4..ac8d76b52a 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java @@ -15,11 +15,11 @@ */ package org.thingsboard.server.service.security.auth.mfa.provider.impl; -import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.CollectionsUtil; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.security.model.mfa.account.BackupCodeTwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.BackupCodeTwoFaProviderConfig; @@ -49,7 +49,7 @@ public class BackupCodeTwoFaProvider implements TwoFaProvider generateCodes(int count, int length) { - return Stream.generate(() -> RandomStringUtils.random(length, "0123456789abcdef")) + return Stream.generate(() -> StringUtils.random(length, "0123456789abcdef")) .distinct().limit(count) .collect(Collectors.toSet()); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFaProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFaProvider.java index d3f83724cf..b75e81be6f 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFaProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFaProvider.java @@ -16,10 +16,10 @@ package org.thingsboard.server.service.security.auth.mfa.provider.impl; import lombok.Data; -import org.apache.commons.lang3.RandomStringUtils; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.security.model.mfa.account.OtpBasedTwoFaAccountConfig; import org.thingsboard.server.common.data.security.model.mfa.provider.OtpBasedTwoFaProviderConfig; @@ -40,7 +40,7 @@ public abstract class OtpBasedTwoFaProvider historyFuture; - if (cmd.getHistoryCmd() != null) { - log.trace("[{}][{}] Going to process history command: {}", session.getSessionId(), cmd.getCmdId(), cmd.getHistoryCmd()); - try { - historyFuture = handleHistoryCmd(ctx, cmd.getHistoryCmd()); - } catch (RuntimeException e) { - handleWsCmdRuntimeException(ctx.getSessionId(), e, cmd); - return; + + try { + List> cmdFutures = new ArrayList<>(); + if (cmd.getAggHistoryCmd() != null) { + cmdFutures.add(handleAggHistoryCmd(ctx, cmd.getAggHistoryCmd())); } - } else { - historyFuture = Futures.immediateFuture(ctx); + if (cmd.getAggTsCmd() != null) { + cmdFutures.add(handleAggTsCmd(ctx, cmd.getAggTsCmd())); + } + if (cmd.getHistoryCmd() != null) { + cmdFutures.add(handleHistoryCmd(ctx, cmd.getHistoryCmd())); + } + if (cmdFutures.isEmpty()) { + handleRegularCommands(ctx, cmd); + } else { + TbEntityDataSubCtx finalCtx = ctx; + Futures.addCallback(Futures.allAsList(cmdFutures), new FutureCallback<>() { + @Override + public void onSuccess(@Nullable List result) { + handleRegularCommands(finalCtx, cmd); + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}][{}] Failed to process command", finalCtx.getSessionId(), finalCtx.getCmdId()); + } + }, wsCallBackExecutor); + } + } catch (RuntimeException e) { + handleWsCmdRuntimeException(ctx.getSessionId(), e, cmd); + } + } + + private void handleRegularCommands(TbEntityDataSubCtx ctx, EntityDataCmd cmd) { + try { + if (cmd.getLatestCmd() != null || cmd.getTsCmd() != null) { + if (cmd.getLatestCmd() != null) { + handleLatestCmd(ctx, cmd.getLatestCmd()); + } + if (cmd.getTsCmd() != null) { + handleTimeSeriesCmd(ctx, cmd.getTsCmd()); + } + } else { + checkAndSendInitialData(ctx); + } + } catch (RuntimeException e) { + handleWsCmdRuntimeException(ctx.getSessionId(), e, cmd); } - Futures.addCallback(historyFuture, new FutureCallback<>() { - @Override - public void onSuccess(@Nullable TbEntityDataSubCtx theCtx) { + } + + private void checkAndSendInitialData(@Nullable TbEntityDataSubCtx theCtx) { + if (!theCtx.isInitialDataSent()) { + EntityDataUpdate update = new EntityDataUpdate(theCtx.getCmdId(), theCtx.getData(), null, theCtx.getMaxEntitiesPerDataSubscription()); + theCtx.sendWsMsg(update); + theCtx.setInitialDataSent(true); + } + } + + private ListenableFuture handleAggHistoryCmd(TbEntityDataSubCtx ctx, AggHistoryCmd cmd) { + ConcurrentMap queries = new ConcurrentHashMap<>(); + for (AggKey key : cmd.getKeys()) { + if (key.getPreviousValueOnly() == null || !key.getPreviousValueOnly()) { + var query = new BaseReadTsKvQuery(key.getKey(), cmd.getStartTs(), cmd.getEndTs(), cmd.getEndTs() - cmd.getStartTs(), 1, key.getAgg()); + queries.put(query.getId(), new ReadTsKvQueryInfo(key, query, false)); + } + if (key.getPreviousStartTs() != null && key.getPreviousEndTs() != null && key.getPreviousEndTs() >= key.getPreviousStartTs()) { + var query = new BaseReadTsKvQuery(key.getKey(), key.getPreviousStartTs(), key.getPreviousEndTs(), key.getPreviousEndTs() - key.getPreviousStartTs(), 1, key.getAgg()); + queries.put(query.getId(), new ReadTsKvQueryInfo(key, query, true)); + } + } + return handleAggCmd(ctx, cmd.getKeys(), queries, cmd.getStartTs(), cmd.getEndTs(), false); + } + + private ListenableFuture handleAggTsCmd(TbEntityDataSubCtx ctx, AggTimeSeriesCmd cmd) { + ConcurrentMap queries = new ConcurrentHashMap<>(); + for (AggKey key : cmd.getKeys()) { + var query = new BaseReadTsKvQuery(key.getKey(), cmd.getStartTs(), cmd.getStartTs() + cmd.getTimeWindow(), cmd.getTimeWindow(), 1, key.getAgg()); + queries.put(query.getId(), new ReadTsKvQueryInfo(key, query, false)); + } + return handleAggCmd(ctx, cmd.getKeys(), queries, cmd.getStartTs(), cmd.getStartTs() + cmd.getTimeWindow(), true); + } + + private ListenableFuture handleAggCmd(TbEntityDataSubCtx ctx, List keys, ConcurrentMap queries, + long startTs, long endTs, boolean subscribe) { + Map>> fetchResultMap = new HashMap<>(); + List entityDataList = ctx.getData().getData(); + List queryList = queries.values().stream().map(ReadTsKvQueryInfo::getQuery).collect(Collectors.toList()); + entityDataList.forEach(entityData -> fetchResultMap.put(entityData, + tsService.findAllByQueries(ctx.getTenantId(), entityData.getEntityId(), queryList))); + return Futures.transform(Futures.allAsList(fetchResultMap.values()), f -> { + // Map that holds last ts for each key for each entity. + Map> lastTsEntityMap = new HashMap<>(); + fetchResultMap.forEach((entityData, future) -> { try { - if (cmd.getLatestCmd() != null || cmd.getTsCmd() != null) { - if (cmd.getLatestCmd() != null) { - handleLatestCmd(theCtx, cmd.getLatestCmd()); - } - if (cmd.getTsCmd() != null) { - handleTimeSeriesCmd(theCtx, cmd.getTsCmd()); + Map lastTsMap = new HashMap<>(); + lastTsEntityMap.put(entityData, lastTsMap); + + List queryResults = future.get(); + if (queryResults != null) { + for (ReadTsKvQueryResult queryResult : queryResults) { + ReadTsKvQueryInfo queryInfo = queries.get(queryResult.getQueryId()); + ComparisonTsValue comparisonTsValue = entityData.getAggLatest().computeIfAbsent(queryInfo.getKey().getId(), agg -> new ComparisonTsValue()); + if (queryInfo.isPrevious()) { + comparisonTsValue.setPrevious(queryResult.toTsValue()); + } else { + comparisonTsValue.setCurrent(queryResult.toTsValue()); + lastTsMap.put(queryInfo.getQuery().getKey(), queryResult.getLastEntryTs()); + } } - } else if (!theCtx.isInitialDataSent()) { - EntityDataUpdate update = new EntityDataUpdate(theCtx.getCmdId(), theCtx.getData(), null, theCtx.getMaxEntitiesPerDataSubscription()); - theCtx.sendWsMsg(update); - theCtx.setInitialDataSent(true); } - } catch (RuntimeException e) { - handleWsCmdRuntimeException(theCtx.getSessionId(), e, cmd); + // Populate with empty values if no data found. + keys.forEach(key -> { + entityData.getAggLatest().putIfAbsent(key.getId(), new ComparisonTsValue(TsValue.EMPTY, TsValue.EMPTY)); + }); + } catch (InterruptedException | ExecutionException e) { + log.warn("[{}][{}][{}] Failed to fetch historical data", ctx.getSessionId(), ctx.getCmdId(), entityData.getEntityId(), e); + ctx.sendWsMsg(new EntityDataUpdate(ctx.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR.getCode(), "Failed to fetch historical data!")); } + }); + ctx.getWsLock().lock(); + try { + EntityDataUpdate update; + if (!ctx.isInitialDataSent()) { + update = new EntityDataUpdate(ctx.getCmdId(), ctx.getData(), null, ctx.getMaxEntitiesPerDataSubscription()); + ctx.setInitialDataSent(true); + } else { + update = new EntityDataUpdate(ctx.getCmdId(), null, entityDataList, ctx.getMaxEntitiesPerDataSubscription()); + } + if (subscribe) { + ctx.createTimeSeriesSubscriptions(lastTsEntityMap, startTs, endTs, true); + } + ctx.sendWsMsg(update); + entityDataList.forEach(ed -> ed.getTimeseries().clear()); + } finally { + ctx.getWsLock().unlock(); } - - @Override - public void onFailure(Throwable t) { - log.warn("[{}][{}] Failed to process command", session.getSessionId(), cmd.getCmdId()); - } + return ctx; }, wsCallBackExecutor); } @@ -416,36 +523,58 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc } private ListenableFuture handleGetTsCmd(TbEntityDataSubCtx ctx, GetTsCmd cmd, boolean subscribe) { + Map queriesKeys = new ConcurrentHashMap<>(); + List keys = cmd.getKeys(); List finalTsKvQueryList; - List tsKvQueryList = cmd.getKeys().stream().map(key -> new BaseReadTsKvQuery( - key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), cmd.getAgg() - )).collect(Collectors.toList()); + List tsKvQueryList = keys.stream().map(key -> { + var query = new BaseReadTsKvQuery( + key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), cmd.getAgg() + ); + queriesKeys.put(query.getId(), query.getKey()); + return query; + }).collect(Collectors.toList()); if (cmd.isFetchLatestPreviousPoint()) { finalTsKvQueryList = new ArrayList<>(tsKvQueryList); - finalTsKvQueryList.addAll(cmd.getKeys().stream().map(key -> new BaseReadTsKvQuery( - key, cmd.getStartTs() - TimeUnit.DAYS.toMillis(365), cmd.getStartTs(), cmd.getInterval(), 1, cmd.getAgg() - )).collect(Collectors.toList())); + finalTsKvQueryList.addAll(keys.stream().map(key -> { + var query = new BaseReadTsKvQuery( + key, cmd.getStartTs() - TimeUnit.DAYS.toMillis(365), cmd.getStartTs(), cmd.getInterval(), 1, cmd.getAgg()); + queriesKeys.put(query.getId(), query.getKey()); + return query; + } + ).collect(Collectors.toList())); } else { finalTsKvQueryList = tsKvQueryList; } - Map>> fetchResultMap = new HashMap<>(); - ctx.getData().getData().forEach(entityData -> fetchResultMap.put(entityData, - tsService.findAll(ctx.getTenantId(), entityData.getEntityId(), finalTsKvQueryList))); + Map>> fetchResultMap = new HashMap<>(); + List entityDataList = ctx.getData().getData(); + entityDataList.forEach(entityData -> fetchResultMap.put(entityData, + tsService.findAllByQueries(ctx.getTenantId(), entityData.getEntityId(), finalTsKvQueryList))); return Futures.transform(Futures.allAsList(fetchResultMap.values()), f -> { + // Map that holds last ts for each key for each entity. + Map> lastTsEntityMap = new HashMap<>(); fetchResultMap.forEach((entityData, future) -> { - Map> keyData = new LinkedHashMap<>(); - cmd.getKeys().forEach(key -> keyData.put(key, new ArrayList<>())); try { - List entityTsData = future.get(); - if (entityTsData != null) { - entityTsData.forEach(entry -> keyData.get(entry.getKey()).add(new TsValue(entry.getTs(), entry.getValueAsString()))); + Map lastTsMap = new HashMap<>(); + lastTsEntityMap.put(entityData, lastTsMap); + + List queryResults = future.get(); + if (queryResults != null) { + for (ReadTsKvQueryResult queryResult : queryResults) { + String queryKey = queriesKeys.get(queryResult.getQueryId()); + entityData.getTimeseries().put(queryKey, queryResult.toTsValues()); + lastTsMap.put(queryKey, queryResult.getLastEntryTs()); + } } - keyData.forEach((k, v) -> entityData.getTimeseries().put(k, v.toArray(new TsValue[v.size()]))); + // Populate with empty values if no data found. + keys.forEach(key -> { + if (!entityData.getTimeseries().containsKey(key)) { + entityData.getTimeseries().put(key, new TsValue[0]); + } + }); + if (cmd.isFetchLatestPreviousPoint()) { - entityData.getTimeseries().values().forEach(dataArray -> { - Arrays.sort(dataArray, (o1, o2) -> Long.compare(o2.getTs(), o1.getTs())); - }); + entityData.getTimeseries().values().forEach(dataArray -> Arrays.sort(dataArray, (o1, o2) -> Long.compare(o2.getTs(), o1.getTs()))); } } catch (InterruptedException | ExecutionException e) { log.warn("[{}][{}][{}] Failed to fetch historical data", ctx.getSessionId(), ctx.getCmdId(), entityData.getEntityId(), e); @@ -459,13 +588,13 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc update = new EntityDataUpdate(ctx.getCmdId(), ctx.getData(), null, ctx.getMaxEntitiesPerDataSubscription()); ctx.setInitialDataSent(true); } else { - update = new EntityDataUpdate(ctx.getCmdId(), null, ctx.getData().getData(), ctx.getMaxEntitiesPerDataSubscription()); + update = new EntityDataUpdate(ctx.getCmdId(), null, entityDataList, ctx.getMaxEntitiesPerDataSubscription()); } if (subscribe) { - ctx.createTimeseriesSubscriptions(keys.stream().map(key -> new EntityKey(EntityKeyType.TIME_SERIES, key)).collect(Collectors.toList()), cmd.getStartTs(), cmd.getEndTs()); + ctx.createTimeSeriesSubscriptions(lastTsEntityMap, cmd.getStartTs(), cmd.getEndTs()); } ctx.sendWsMsg(update); - ctx.getData().getData().forEach(ed -> ed.getTimeseries().clear()); + entityDataList.forEach(ed -> ed.getTimeseries().clear()); } finally { ctx.getWsLock().unlock(); } @@ -533,11 +662,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc ctx.getWsLock().lock(); try { ctx.createLatestValuesSubscriptions(latestCmd.getKeys()); - if (!ctx.isInitialDataSent()) { - EntityDataUpdate update = new EntityDataUpdate(ctx.getCmdId(), ctx.getData(), null, ctx.getMaxEntitiesPerDataSubscription()); - ctx.sendWsMsg(update); - ctx.setInitialDataSent(true); - } + checkAndSendInitialData(ctx); } finally { ctx.getWsLock().unlock(); } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/ReadTsKvQueryInfo.java b/application/src/main/java/org/thingsboard/server/service/subscription/ReadTsKvQueryInfo.java new file mode 100644 index 0000000000..28ca2f6729 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/subscription/ReadTsKvQueryInfo.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.subscription; + +import lombok.Data; +import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.service.telemetry.cmd.v2.AggKey; + +@Data +public class ReadTsKvQueryInfo { + + private final AggKey key; + private final ReadTsKvQuery query; + private final boolean previous; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java index a8283a1e4d..26e97b6f2c 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java @@ -86,7 +86,7 @@ public abstract class TbAbstractDataSubCtx newDataMap = newData.getData().stream().collect(Collectors.toMap(EntityData::getEntityId, Function.identity(), (a,b)-> a)); + Map newDataMap = newData.getData().stream().collect(Collectors.toMap(EntityData::getEntityId, Function.identity(), (a, b) -> a)); if (oldDataMap.size() == newDataMap.size() && oldDataMap.keySet().equals(newDataMap.keySet())) { log.trace("[{}][{}] No updates to entity data found", sessionRef.getSessionId(), cmdId); } else { @@ -122,8 +122,17 @@ public abstract class TbAbstractDataSubCtx keys, long startTs, long endTs) { - createSubscriptions(keys, false, startTs, endTs); + public void createTimeSeriesSubscriptions(Map> entityKeyStates, long startTs, long endTs) { + createTimeSeriesSubscriptions(entityKeyStates, startTs, endTs, false); + } + + public void createTimeSeriesSubscriptions(Map> entityKeyStates, long startTs, long endTs, boolean resultToLatestValues) { + entityKeyStates.forEach((entityData, keyStates) -> { + int subIdx = sessionRef.getSessionSubIdSeq().incrementAndGet(); + subToEntityIdMap.put(subIdx, entityData.getEntityId()); + localSubscriptionService.addSubscription( + createTsSub(entityData, subIdx, false, startTs, endTs, keyStates, resultToLatestValues)); + }); } private void createSubscriptions(List keys, boolean latestValues, long startTs, long endTs) { @@ -191,6 +200,14 @@ public abstract class TbAbstractDataSubCtx keyStates) { + return createTsSub(entityData, subIdx, latestValues, startTs, endTs, keyStates, latestValues); + } + + private TbTimeseriesSubscription createTsSub(EntityData entityData, int subIdx, boolean latestValues, long startTs, long endTs, Map keyStates, boolean resultToLatestValues) { log.trace("[{}][{}][{}] Creating time-series subscription for [{}] with keys: {}", serviceId, cmdId, subIdx, entityData.getEntityId(), keyStates); return TbTimeseriesSubscription.builder() .serviceId(serviceId) @@ -198,7 +215,7 @@ public abstract class TbAbstractDataSubCtx sendWsMsg(sessionId, subscriptionUpdate, EntityKeyType.TIME_SERIES, latestValues)) + .updateConsumer((sessionId, subscriptionUpdate) -> sendWsMsg(sessionId, subscriptionUpdate, EntityKeyType.TIME_SERIES, resultToLatestValues)) .allKeys(false) .keyStates(keyStates) .latestValues(latestValues) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java index becf92771f..43ebfc4afc 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java @@ -20,7 +20,6 @@ import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import lombok.Data; import lombok.SneakyThrows; -import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.context.SecurityContext; @@ -29,6 +28,7 @@ import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.EntityId; diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java index 0551d7e114..d9beb1ed33 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java @@ -26,11 +26,11 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; import org.springframework.web.socket.CloseStatus; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -716,7 +716,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi "Cmd id is negative value!"); sendWsMsg(sessionRef, update); return false; - } else if (cmd.getQuery() == null && cmd.getLatestCmd() == null && cmd.getHistoryCmd() == null && cmd.getTsCmd() == null) { + } else if (cmd.getQuery() == null && !cmd.hasAnyCmd()) { TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, "Query is empty!"); sendWsMsg(sessionRef, update); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MBootstrapServersConfiguration.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggHistoryCmd.java similarity index 79% rename from common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MBootstrapServersConfiguration.java rename to application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggHistoryCmd.java index e5bdec684a..423c55bd0d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MBootstrapServersConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggHistoryCmd.java @@ -13,15 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap; +package org.thingsboard.server.service.telemetry.cmd.v2; import lombok.Data; import java.util.List; @Data -public class LwM2MBootstrapServersConfiguration { +public class AggHistoryCmd { - List bootstrap; + private List keys; + private long startTs; + private long endTs; } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggKey.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggKey.java new file mode 100644 index 0000000000..d567149d01 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggKey.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +import lombok.Data; +import org.thingsboard.server.common.data.kv.Aggregation; + +@Data +public class AggKey { + + private int id; + private String key; + private Aggregation agg; + + private Long previousStartTs; + private Long previousEndTs; + private Boolean previousValueOnly; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggTimeSeriesCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggTimeSeriesCmd.java new file mode 100644 index 0000000000..1f4d88d2c3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggTimeSeriesCmd.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.telemetry.cmd.v2; + +import lombok.Data; + +import java.util.List; + +@Data +public class AggTimeSeriesCmd { + + private List keys; + private long startTs; + private long timeWindow; + +} diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataCmd.java b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataCmd.java index 04335ed9a5..8a187afa7b 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataCmd.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataCmd.java @@ -16,6 +16,7 @@ package org.thingsboard.server.service.telemetry.cmd.v2; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; import org.thingsboard.server.common.data.query.EntityDataQuery; @@ -30,17 +31,35 @@ public class EntityDataCmd extends DataCmd { private final LatestValueCmd latestCmd; @Getter private final TimeSeriesCmd tsCmd; + @Getter + private final AggHistoryCmd aggHistoryCmd; + @Getter + private final AggTimeSeriesCmd aggTsCmd; + + public EntityDataCmd(int cmdId, EntityDataQuery query, EntityHistoryCmd historyCmd, LatestValueCmd latestCmd, TimeSeriesCmd tsCmd) { + this(cmdId, query, historyCmd, latestCmd, tsCmd, null, null); + } @JsonCreator public EntityDataCmd(@JsonProperty("cmdId") int cmdId, @JsonProperty("query") EntityDataQuery query, @JsonProperty("historyCmd") EntityHistoryCmd historyCmd, @JsonProperty("latestCmd") LatestValueCmd latestCmd, - @JsonProperty("tsCmd") TimeSeriesCmd tsCmd) { + @JsonProperty("tsCmd") TimeSeriesCmd tsCmd, + @JsonProperty("aggHistoryCmd") AggHistoryCmd aggHistoryCmd, + @JsonProperty("aggTsCmd") AggTimeSeriesCmd aggTsCmd) { super(cmdId); this.query = query; this.historyCmd = historyCmd; this.latestCmd = latestCmd; this.tsCmd = tsCmd; + this.aggHistoryCmd = aggHistoryCmd; + this.aggTsCmd = aggTsCmd; } + + @JsonIgnore + public boolean hasAnyCmd() { + return historyCmd != null || latestCmd != null || tsCmd != null || aggHistoryCmd != null || aggTsCmd != null; + } + } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 83ff290476..9edd5d0ed5 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -39,7 +39,7 @@ server: key_password: "${SSL_PEM_KEY_PASSWORD:server_key_password}" # Keystore server credentials keystore: - # Type of the key store + # Type of the key store (JKS or PKCS12) type: "${SSL_KEY_STORE_TYPE:PKCS12}" # Path to the key store that holds the SSL certificate store_file: "${SSL_KEY_STORE:classpath:keystore/keystore.p12}" @@ -148,7 +148,7 @@ ui: # 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.4}" + base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-3.4.1}" database: ts_max_intervals: "${DATABASE_TS_MAX_INTERVALS:700}" # Max number of DB queries generated by single API call to fetch telemetry records @@ -260,7 +260,7 @@ sql: stats_print_interval_ms: "${SQL_EVENTS_BATCH_STATS_PRINT_MS:10000}" batch_threads: "${SQL_EVENTS_BATCH_THREADS:3}" # batch thread count have to be a prime number like 3 or 5 to gain perfect hash distribution partition_size: "${SQL_EVENTS_REGULAR_PARTITION_SIZE_HOURS:168}" # Number of hours to partition the events. The current value corresponds to one week. - debug_partition_size: "${SQL_EVENTS_REGULAR_PARTITION_SIZE_HOURS:1}" # Number of hours to partition the debug events. The current value corresponds to one hour. + debug_partition_size: "${SQL_EVENTS_DEBUG_PARTITION_SIZE_HOURS:1}" # Number of hours to partition the debug events. The current value corresponds to one hour. edge_events: batch_size: "${SQL_EDGE_EVENTS_BATCH_SIZE:1000}" batch_max_delay: "${SQL_EDGE_EVENTS_BATCH_MAX_DELAY_MS:100}" @@ -685,7 +685,7 @@ transport: key_password: "${MQTT_SSL_PEM_KEY_PASSWORD:server_key_password}" # Keystore server credentials keystore: - # Type of the key store + # Type of the key store (JKS or PKCS12) 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}" @@ -728,7 +728,7 @@ transport: key_password: "${COAP_DTLS_PEM_KEY_PASSWORD:server_key_password}" # Keystore server credentials keystore: - # Type of the key store + # Type of the key store (JKS or PKCS12) 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}" @@ -770,7 +770,7 @@ transport: key_password: "${LWM2M_SERVER_PEM_KEY_PASSWORD:server_key_password}" # Keystore server credentials keystore: - # Type of the key store + # Type of the key store (JKS or PKCS12) 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}" @@ -806,7 +806,7 @@ transport: key_password: "${LWM2M_BS_PEM_KEY_PASSWORD:server_key_password}" # Keystore server credentials keystore: - # Type of the key store + # Type of the key store (JKS or PKCS12) 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}" @@ -829,7 +829,7 @@ transport: cert_file: "${LWM2M_TRUST_PEM_CERT:lwm2mtruststorechain.pem}" # Keystore with trust certificates keystore: - # Type of the key store + # Type of the key store (JKS or PKCS12) type: "${LWM2M_TRUST_KEY_STORE_TYPE:JKS}" # Path to the key store that holds the X509 certificates store_file: "${LWM2M_TRUST_KEY_STORE:lwm2mtruststorechain.jks}" diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index 75d3a0aa97..09c40f263c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -26,9 +26,8 @@ import io.jsonwebtoken.Header; import io.jsonwebtoken.Jwt; import io.jsonwebtoken.Jwts; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.RandomStringUtils; -import org.apache.commons.lang3.StringUtils; import org.hamcrest.Matcher; +import org.hibernate.exception.ConstraintViolationException; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -36,6 +35,7 @@ import org.junit.Rule; import org.junit.rules.TestRule; import org.junit.rules.TestWatcher; import org.junit.runner.Description; +import org.mockito.BDDMockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -56,6 +56,7 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; @@ -68,28 +69,33 @@ import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadCo import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; -import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; +import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.config.ThingsboardSecurityConfiguration; +import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.service.mail.TestMailService; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest; import java.io.IOException; +import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Comparator; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; @@ -658,7 +664,11 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { } protected T readResponse(ResultActions result, TypeReference type) throws Exception { - byte[] content = result.andReturn().getResponse().getContentAsByteArray(); + return readResponse(result.andReturn(), type); + } + + protected T readResponse(MvcResult result, TypeReference type) throws Exception { + byte[] content = result.getResponse().getContentAsByteArray(); return mapper.readerFor(type).readValue(content); } @@ -686,8 +696,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { edge.setTenantId(tenantId); edge.setName(name); edge.setType(type); - edge.setSecret(RandomStringUtils.randomAlphanumeric(20)); - edge.setRoutingKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setSecret(StringUtils.randomAlphanumeric(20)); + edge.setRoutingKey(StringUtils.randomAlphanumeric(20)); return edge; } @@ -701,4 +711,49 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return Futures.allAsList(futures); } + protected void testEntityDaoWithRelationsOk(EntityId entityIdFrom, EntityId entityTo, String urlDelete) throws Exception { + createEntityRelation(entityIdFrom, entityTo, "TEST_TYPE"); + assertThat(findRelationsByTo(entityTo)).hasSize(1); + + doDelete(urlDelete).andExpect(status().isOk()); + + assertThat(findRelationsByTo(entityTo)).hasSize(0); + } + + protected void testEntityDaoWithRelationsTransactionalException(Dao dao, EntityId entityIdFrom, EntityId entityTo, + String urlDelete) throws Exception { + entityDaoRemoveByIdWithException (dao); + createEntityRelation(entityIdFrom, entityTo, "TEST_TRANSACTIONAL_TYPE"); + assertThat(findRelationsByTo(entityTo)).hasSize(1); + + doDelete(urlDelete) + .andExpect(status().isInternalServerError()); + + assertThat(findRelationsByTo(entityTo)).hasSize(1); + } + + protected void entityDaoRemoveByIdWithException (Dao dao) throws Exception { + BDDMockito.willThrow(new ConstraintViolationException("mock message", new SQLException(), "MOCK_CONSTRAINT")) + .given(dao).removeById(any(), any()); + } + + protected void afterTestEntityDaoRemoveByIdWithException (Dao dao) throws Exception { + BDDMockito.willCallRealMethod().given(dao).removeById(any(), any()); + } + + protected void createEntityRelation(EntityId entityIdFrom, EntityId entityIdTo, String typeRelation) throws Exception { + EntityRelation relation = new EntityRelation(entityIdFrom, entityIdTo, typeRelation); + doPost("/api/relation", relation); + } + + protected List findRelationsByTo(EntityId entityId) throws Exception { + String url = String.format("/api/relations?toId=%s&toType=%s", entityId.getId(), entityId.getEntityType().name()); + MvcResult mvcResult = doGet(url).andReturn(); + + switch (mvcResult.getResponse().getStatus()) { + case 200: return readResponse(mvcResult, new TypeReference<>() {}); + case 404: return Collections.emptyList(); + } + throw new AssertionError("Unexpected status " + mvcResult.getResponse().getStatus()); + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java index 14e260ab14..a2b8b908d5 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java @@ -23,6 +23,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; @@ -31,7 +32,9 @@ import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.dao.alarm.AlarmDao; import java.util.LinkedList; import java.util.List; @@ -46,6 +49,10 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { protected Device customerDevice; + + @SpyBean + private AlarmDao alarmDao; + @Before public void setup() throws Exception { loginTenantAdmin(); @@ -64,6 +71,9 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { @After public void teardown() throws Exception { loginSysAdmin(); + + afterTestEntityDaoRemoveByIdWithException (alarmDao); + deleteDifferentTenant(); } @@ -421,6 +431,21 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { Assert.assertTrue("Created alarm doesn't match the found one!", equals); } + + @Test + public void testDeleteAlarmWithDeleteRelationsOk() throws Exception { + loginCustomerUser(); + AlarmId alarmId = createAlarm("Alarm for Test WithRelationsOk").getId(); + testEntityDaoWithRelationsOk(customerDevice.getId(), alarmId, "/api/alarm/" + alarmId); + } + + @Test + public void testDeleteAlarmExceptionWithRelationsTransactional() throws Exception { + loginCustomerUser(); + AlarmId alarmId = createAlarm("Alarm for Test WithRelations Transactional Exception").getId(); + testEntityDaoWithRelationsTransactionalException(alarmDao, customerDevice.getId(), alarmId, "/api/alarm/" + alarmId); + } + private Alarm createAlarm(String type) throws Exception { Alarm alarm = Alarm.builder() .tenantId(tenantId) diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java index 0aac87cea0..4e9c01e4cc 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java @@ -17,24 +17,27 @@ package org.thingsboard.server.controller; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.core.type.TypeReference; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.asset.AssetDao; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.service.stats.DefaultRuleEngineStatisticsService; @@ -54,6 +57,9 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { private Tenant savedTenant; private User tenantAdmin; + @SpyBean + private AssetDao assetDao; + @Before public void beforeTest() throws Exception { loginSysAdmin(); @@ -77,6 +83,8 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { public void afterTest() throws Exception { loginSysAdmin(); + afterTestEntityDaoRemoveByIdWithException (assetDao); + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) .andExpect(status().isOk()); } @@ -117,7 +125,7 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { @Test public void testSaveAssetWithViolationOfLengthValidation() throws Exception { Asset asset = new Asset(); - asset.setName(RandomStringUtils.randomAlphabetic(300)); + asset.setName(StringUtils.randomAlphabetic(300)); asset.setType("default"); Mockito.reset(tbClusterService, auditLogService); @@ -132,7 +140,7 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { Mockito.reset(tbClusterService, auditLogService); asset.setName("Normal name"); - asset.setType(RandomStringUtils.randomAlphabetic(300)); + asset.setType(StringUtils.randomAlphabetic(300)); msgError = msgErrorFieldLength("type"); doPost("/api/asset", asset) .andExpect(status().isBadRequest()) @@ -143,7 +151,7 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { Mockito.reset(tbClusterService, auditLogService); asset.setType("default"); - asset.setLabel(RandomStringUtils.randomAlphabetic(300)); + asset.setLabel(StringUtils.randomAlphabetic(300)); msgError = msgErrorFieldLength("label"); doPost("/api/asset", asset) .andExpect(status().isBadRequest()) @@ -471,7 +479,7 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { List assetsTitle1 = new ArrayList<>(); for (int i = 0; i < 143; i++) { Asset asset = new Asset(); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); asset.setName(name); @@ -482,7 +490,7 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { List assetsTitle2 = new ArrayList<>(); for (int i = 0; i < 75; i++) { Asset asset = new Asset(); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); asset.setName(name); @@ -557,7 +565,7 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { List assetsType1 = new ArrayList<>(); for (int i = 0; i < 143; i++) { Asset asset = new Asset(); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); asset.setName(name); @@ -569,7 +577,7 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { List assetsType2 = new ArrayList<>(); for (int i = 0; i < 75; i++) { Asset asset = new Asset(); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); asset.setName(name); @@ -684,7 +692,7 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { List assetsTitle1 = new ArrayList<>(); for (int i = 0; i < 125; i++) { Asset asset = new Asset(); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); asset.setName(name); @@ -697,7 +705,7 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { List assetsTitle2 = new ArrayList<>(); for (int i = 0; i < 143; i++) { Asset asset = new Asset(); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); asset.setName(name); @@ -779,7 +787,7 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { List assetsType1 = new ArrayList<>(); for (int i = 0; i < 125; i++) { Asset asset = new Asset(); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); asset.setName(name); @@ -793,7 +801,7 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { List assetsType2 = new ArrayList<>(); for (int i = 0; i < 143; i++) { Asset asset = new Asset(); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); asset.setName(name); @@ -905,4 +913,23 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest { Assert.assertEquals(0, pageData.getData().size()); } + + @Test + public void testDeleteAssetWithDeleteRelationsOk() throws Exception { + AssetId assetId = createAsset("Asset for Test WithRelationsOk").getId(); + testEntityDaoWithRelationsOk(savedTenant.getId(), assetId, "/api/asset/" + assetId); + } + + @Test + public void testDeleteAssetExceptionWithRelationsTransactional() throws Exception { + AssetId assetId = createAsset("Asset for Test WithRelations Transactional Exception").getId(); + testEntityDaoWithRelationsTransactionalException(assetDao, savedTenant.getId(), assetId, "/api/asset/" + assetId); + } + + private Asset createAsset(String name) { + Asset asset = new Asset(); + asset.setName(name); + asset.setType("default"); + return doPost("/api/asset", asset, Asset.class); + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseCustomerControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseCustomerControllerTest.java index bd0a416ccf..d3cc1761c7 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseCustomerControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseCustomerControllerTest.java @@ -20,14 +20,15 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; @@ -36,6 +37,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.exception.DataValidationException; import java.util.ArrayList; @@ -55,6 +57,9 @@ public abstract class BaseCustomerControllerTest extends AbstractControllerTest private Tenant savedTenant; private User tenantAdmin; + @SpyBean + private CustomerDao customerDao; + @Before public void beforeTest() throws Exception { executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass())); @@ -82,6 +87,8 @@ public abstract class BaseCustomerControllerTest extends AbstractControllerTest loginSysAdmin(); + afterTestEntityDaoRemoveByIdWithException (customerDao); + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) .andExpect(status().isOk()); } @@ -122,7 +129,7 @@ public abstract class BaseCustomerControllerTest extends AbstractControllerTest @Test public void testSaveCustomerWithViolationOfValidation() throws Exception { Customer customer = new Customer(); - customer.setTitle(RandomStringUtils.randomAlphabetic(300)); + customer.setTitle(StringUtils.randomAlphabetic(300)); Mockito.reset(tbClusterService, auditLogService); @@ -137,7 +144,7 @@ public abstract class BaseCustomerControllerTest extends AbstractControllerTest Mockito.reset(tbClusterService, auditLogService); customer.setTitle("Normal title"); - customer.setCity(RandomStringUtils.randomAlphabetic(300)); + customer.setCity(StringUtils.randomAlphabetic(300)); msgError = msgErrorFieldLength("city"); doPost("/api/customer", customer) .andExpect(status().isBadRequest()) @@ -148,7 +155,7 @@ public abstract class BaseCustomerControllerTest extends AbstractControllerTest Mockito.reset(tbClusterService, auditLogService); customer.setCity("Normal city"); - customer.setCountry(RandomStringUtils.randomAlphabetic(300)); + customer.setCountry(StringUtils.randomAlphabetic(300)); msgError = msgErrorFieldLength("country"); doPost("/api/customer", customer) .andExpect(status().isBadRequest()) @@ -159,7 +166,7 @@ public abstract class BaseCustomerControllerTest extends AbstractControllerTest Mockito.reset(tbClusterService, auditLogService); customer.setCountry("Ukraine"); - customer.setPhone(RandomStringUtils.randomAlphabetic(300)); + customer.setPhone(StringUtils.randomAlphabetic(300)); msgError = msgErrorFieldLength("phone"); doPost("/api/customer", customer) .andExpect(status().isBadRequest()) @@ -170,7 +177,7 @@ public abstract class BaseCustomerControllerTest extends AbstractControllerTest Mockito.reset(tbClusterService, auditLogService); customer.setPhone("+3892555554512"); - customer.setState(RandomStringUtils.randomAlphabetic(300)); + customer.setState(StringUtils.randomAlphabetic(300)); msgError = msgErrorFieldLength("state"); doPost("/api/customer", customer) .andExpect(status().isBadRequest()) @@ -181,7 +188,7 @@ public abstract class BaseCustomerControllerTest extends AbstractControllerTest Mockito.reset(tbClusterService, auditLogService); customer.setState("Normal state"); - customer.setZip(RandomStringUtils.randomAlphabetic(300)); + customer.setZip(StringUtils.randomAlphabetic(300)); msgError = msgErrorFieldLength("zip or postal code"); doPost("/api/customer", customer) .andExpect(status().isBadRequest()) @@ -339,7 +346,7 @@ public abstract class BaseCustomerControllerTest extends AbstractControllerTest for (int i = 0; i < 143; i++) { Customer customer = new Customer(); customer.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); + String suffix = StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); String title = title1 + suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); customer.setTitle(title); @@ -353,7 +360,7 @@ public abstract class BaseCustomerControllerTest extends AbstractControllerTest for (int i = 0; i < 175; i++) { Customer customer = new Customer(); customer.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); + String suffix = StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); String title = title2 + suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); customer.setTitle(title); @@ -402,4 +409,22 @@ public abstract class BaseCustomerControllerTest extends AbstractControllerTest Assert.assertFalse(pageData.hasNext()); Assert.assertEquals(0, pageData.getData().size()); } + + @Test + public void testDeleteCustomerWithDeleteRelationsOk() throws Exception { + CustomerId customerId = createCustomer("Customer for Test WithRelationsOk").getId(); + testEntityDaoWithRelationsOk(savedTenant.getId(), customerId, "/api/customer/" + customerId); + } + + @Test + public void testDeleteCustomerExceptionWithRelationsTransactional() throws Exception { + CustomerId customerId = createCustomer("Customer for Test WithRelations Transactional Exception").getId(); + testEntityDaoWithRelationsTransactionalException(customerDao, savedTenant.getId(), customerId, "/api/customer/" + customerId); + } + + private Customer createCustomer(String title) { + Customer customer = new Customer(); + customer.setTitle(title); + return doPost("/api/customer", customer, Customer.class); + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java index 66fc1a5178..50050c6306 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java @@ -17,23 +17,26 @@ package org.thingsboard.server.controller; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.core.type.TypeReference; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.dashboard.DashboardDao; import org.thingsboard.server.dao.exception.DataValidationException; import java.util.ArrayList; @@ -50,6 +53,9 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest private Tenant savedTenant; private User tenantAdmin; + @SpyBean + private DashboardDao dashboardDao; + @Before public void beforeTest() throws Exception { loginSysAdmin(); @@ -73,45 +79,16 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest public void afterTest() throws Exception { loginSysAdmin(); + afterTestEntityDaoRemoveByIdWithException (dashboardDao); + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) .andExpect(status().isOk()); } - @Test - public void testSaveDashboard() throws Exception { - Dashboard dashboard = new Dashboard(); - dashboard.setTitle("My dashboard"); - - Mockito.reset(tbClusterService, auditLogService); - - Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class); - - testNotifyEntityOneTimeMsgToEdgeServiceNever(savedDashboard, savedDashboard.getId(), savedDashboard.getId(), savedTenant.getId(), - tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED); - - Assert.assertNotNull(savedDashboard); - Assert.assertNotNull(savedDashboard.getId()); - Assert.assertTrue(savedDashboard.getCreatedTime() > 0); - Assert.assertEquals(savedTenant.getId(), savedDashboard.getTenantId()); - Assert.assertEquals(dashboard.getTitle(), savedDashboard.getTitle()); - - savedDashboard.setTitle("My new dashboard"); - - Mockito.reset(tbClusterService, auditLogService); - - doPost("/api/dashboard", savedDashboard, Dashboard.class); - - testNotifyEntityAllOneTime(savedDashboard, savedDashboard.getId(), savedDashboard.getId(), savedTenant.getId(), - tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.UPDATED); - - Dashboard foundDashboard = doGet("/api/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class); - Assert.assertEquals(foundDashboard.getTitle(), savedDashboard.getTitle()); - } - @Test public void testSaveDashboardInfoWithViolationOfValidation() throws Exception { Dashboard dashboard = new Dashboard(); - dashboard.setTitle(RandomStringUtils.randomAlphabetic(300)); + dashboard.setTitle(StringUtils.randomAlphabetic(300)); String msgError = msgErrorFieldLength("title"); Mockito.reset(tbClusterService, auditLogService); @@ -335,7 +312,7 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest int cntEntity = 134; for (int i = 0; i < cntEntity; i++) { Dashboard dashboard = new Dashboard(); - String suffix = RandomStringUtils.randomAlphanumeric((int) (Math.random() * 15)); + String suffix = StringUtils.randomAlphanumeric((int) (Math.random() * 15)); String title = title1 + suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); dashboard.setTitle(title); @@ -346,7 +323,7 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest for (int i = 0; i < 112; i++) { Dashboard dashboard = new Dashboard(); - String suffix = RandomStringUtils.randomAlphanumeric((int) (Math.random() * 15)); + String suffix = StringUtils.randomAlphanumeric((int) (Math.random() * 15)); String title = title2 + suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); dashboard.setTitle(title); @@ -495,4 +472,21 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest Assert.assertEquals(0, pageData.getData().size()); } + @Test + public void testDeleteDashboardWithDeleteRelationsOk() throws Exception { + DashboardId dashboardId = createDashboard("Dashboard for Test WithRelationsOk").getId(); + testEntityDaoWithRelationsOk(savedTenant.getId(), dashboardId, "/api/dashboard/" + dashboardId); + } + + @Test + public void testDeleteDashboardExceptionWithRelationsTransactional() throws Exception { + DashboardId dashboardId = createDashboard("Dashboard for Test WithRelations Transactional Exception").getId(); + testEntityDaoWithRelationsTransactionalException(dashboardDao, savedTenant.getId(), dashboardId, "/api/dashboard/" + dashboardId); + } + + private Dashboard createDashboard(String title) { + Dashboard dashboard = new Dashboard(); + dashboard.setTitle(title); + return doPost("/api/dashboard", dashboard, Dashboard.class); + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java index ee4d005233..abe8b78918 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java @@ -21,7 +21,6 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -35,6 +34,7 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.SaveOtaPackageInfoRequest; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; @@ -50,6 +50,7 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import org.thingsboard.server.dao.device.DeviceDao; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.dao.model.ModelConstants; @@ -82,6 +83,10 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { @SpyBean private GatewayNotificationsService gatewayNotificationsService; + @SpyBean + private DeviceDao deviceDao; + + @Before public void beforeTest() throws Exception { executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass())); @@ -109,6 +114,8 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { loginSysAdmin(); + afterTestEntityDaoRemoveByIdWithException (deviceDao); + doDelete("/api/tenant/" + savedTenant.getId().getId()) .andExpect(status().isOk()); } @@ -164,7 +171,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { @Test public void saveDeviceWithViolationOfValidation() throws Exception { Device device = new Device(); - device.setName(RandomStringUtils.randomAlphabetic(300)); + device.setName(StringUtils.randomAlphabetic(300)); device.setType("default"); Mockito.reset(tbClusterService, auditLogService, gatewayNotificationsService); @@ -181,7 +188,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { device.setTenantId(savedTenant.getId()); msgError = msgErrorFieldLength("type"); - device.setType(RandomStringUtils.randomAlphabetic(300)); + device.setType(StringUtils.randomAlphabetic(300)); doPost("/api/device", device) .andExpect(status().isBadRequest()) .andExpect(statusReason(containsString(msgError))); @@ -193,7 +200,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { msgError = msgErrorFieldLength("label"); device.setType("Normal type"); - device.setLabel(RandomStringUtils.randomAlphabetic(300)); + device.setLabel(StringUtils.randomAlphabetic(300)); doPost("/api/device", device) .andExpect(status().isBadRequest()) .andExpect(statusReason(containsString(msgError))); @@ -709,7 +716,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { futures = new ArrayList<>(143); for (int i = 0; i < 143; i++) { Device device = new Device(); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); device.setName(name); @@ -723,7 +730,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { futures = new ArrayList<>(75); for (int i = 0; i < 75; i++) { Device device = new Device(); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); device.setName(name); @@ -783,7 +790,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { futures = new ArrayList<>(143); for (int i = 0; i < 143; i++) { Device device = new Device(); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); device.setName(name); @@ -801,7 +808,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { futures = new ArrayList<>(75); for (int i = 0; i < 75; i++) { Device device = new Device(); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); device.setName(name); @@ -920,7 +927,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { futures = new ArrayList<>(125); for (int i = 0; i < 125; i++) { Device device = new Device(); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); device.setName(name); @@ -936,7 +943,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { futures = new ArrayList<>(143); for (int i = 0; i < 143; i++) { Device device = new Device(); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); device.setName(name); @@ -1003,7 +1010,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { futures = new ArrayList<>(125); for (int i = 0; i < 125; i++) { Device device = new Device(); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); device.setName(name); @@ -1023,7 +1030,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { futures = new ArrayList<>(143); for (int i = 0; i < 143; i++) { Device device = new Device(); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); device.setName(name); @@ -1203,4 +1210,23 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest { protected void testNotificationDeleteGatewayNever() { Mockito.verify(gatewayNotificationsService, never()).onDeviceDeleted(Mockito.any(Device.class)); } + + @Test + public void testDeleteDashboardWithDeleteRelationsOk() throws Exception { + DeviceId deviceId = createDevice("Device for Test WithRelationsOk").getId(); + testEntityDaoWithRelationsOk(savedTenant.getId(), deviceId, "/api/device/" + deviceId); + } + + @Test + public void testDeleteDeviceExceptionWithRelationsTransactional() throws Exception { + DeviceId deviceId = createDevice("Device for Test WithRelations Transactional Exception").getId(); + testEntityDaoWithRelationsTransactionalException(deviceDao, savedTenant.getId(), deviceId, "/api/device/" + deviceId); + } + + private Device createDevice(String name) { + Device device = new Device(); + device.setName(name); + device.setType("default"); + return doPost("/api/device", device, Device.class); + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java index 4f782b15b3..0c042d5006 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java @@ -22,12 +22,12 @@ import com.google.protobuf.DynamicMessage; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.util.JsonFormat; import com.squareup.wire.schema.internal.parser.ProtoFileElement; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.Device; @@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.SaveOtaPackageInfoRequest; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; @@ -46,10 +47,12 @@ import org.thingsboard.server.common.data.device.profile.JsonTransportPayloadCon import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; +import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.device.DeviceProfileDao; import org.thingsboard.server.dao.exception.DataValidationException; import java.util.ArrayList; @@ -74,6 +77,9 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController private Tenant savedTenant; private User tenantAdmin; + @SpyBean + private DeviceProfileDao deviceProfileDao; + @Before public void beforeTest() throws Exception { loginSysAdmin(); @@ -97,6 +103,8 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController public void afterTest() throws Exception { loginSysAdmin(); + afterTestEntityDaoRemoveByIdWithException (deviceProfileDao); + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) .andExpect(status().isOk()); } @@ -138,7 +146,7 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController Mockito.reset(tbClusterService, auditLogService); - DeviceProfile createDeviceProfile = this.createDeviceProfile(RandomStringUtils.randomAlphabetic(300)); + DeviceProfile createDeviceProfile = this.createDeviceProfile(StringUtils.randomAlphabetic(300)); doPost("/api/deviceProfile", createDeviceProfile) .andExpect(status().isBadRequest()) .andExpect(statusReason(containsString(msgError))); @@ -1125,4 +1133,20 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController return JsonFormat.printer().includingDefaultValueFields().print(dynamicMessage); } + @Test + public void testDeleteDeviceProfileWithDeleteRelationsOk() throws Exception { + DeviceProfileId deviceProfileId = savedDeviceProfile("DeviceProfile for Test WithRelationsOk").getId(); + testEntityDaoWithRelationsOk(savedTenant.getId(), deviceProfileId, "/api/deviceProfile/" + deviceProfileId); + } + + @Test + public void testDeleteDeviceProfileExceptionWithRelationsTransactional() throws Exception { + DeviceProfileId deviceProfileId = savedDeviceProfile("DeviceProfile for Test WithRelations Transactional Exception").getId(); + testEntityDaoWithRelationsTransactionalException(deviceProfileDao, savedTenant.getId(), deviceProfileId, "/api/deviceProfile/" + deviceProfileId); + } + + private DeviceProfile savedDeviceProfile(String name) { + DeviceProfile deviceProfile = createDeviceProfile(name); + return doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java index 8bf763c637..c3d747cb35 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java @@ -17,26 +17,29 @@ package org.thingsboard.server.controller; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.core.type.TypeReference; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.TestPropertySource; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.edge.EdgeDao; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.edge.imitator.EdgeImitator; @@ -72,6 +75,9 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { private TenantId tenantId; private User tenantAdmin; + @SpyBean + private EdgeDao edgeDao; + @Before public void beforeTest() throws Exception { loginSysAdmin(); @@ -96,6 +102,8 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { public void afterTest() throws Exception { loginSysAdmin(); + afterTestEntityDaoRemoveByIdWithException (edgeDao); + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) .andExpect(status().isOk()); } @@ -133,7 +141,7 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { @Test public void testSaveEdgeWithViolationOfLengthValidation() throws Exception { - Edge edge = constructEdge(RandomStringUtils.randomAlphabetic(300), "default"); + Edge edge = constructEdge(StringUtils.randomAlphabetic(300), "default"); String msgError = msgErrorFieldLength("name"); Mockito.reset(tbClusterService, auditLogService); @@ -148,7 +156,7 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { msgError = msgErrorFieldLength("type"); edge.setName("normal name"); - edge.setType(RandomStringUtils.randomAlphabetic(300)); + edge.setType(StringUtils.randomAlphabetic(300)); doPost("/api/edge", edge) .andExpect(status().isBadRequest()) .andExpect(statusReason(containsString(msgError))); @@ -159,7 +167,7 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { msgError = msgErrorFieldLength("label"); edge.setType("normal type"); - edge.setLabel(RandomStringUtils.randomAlphabetic(300)); + edge.setLabel(StringUtils.randomAlphabetic(300)); doPost("/api/edge", edge) .andExpect(status().isBadRequest()) .andExpect(statusReason(containsString(msgError))); @@ -389,7 +397,7 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { String title1 = "Edge title 1"; List edgesTitle1 = new ArrayList<>(); for (int i = 0; i < 143; i++) { - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); Edge edge = constructEdge(name, "default"); @@ -398,7 +406,7 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { String title2 = "Edge title 2"; List edgesTitle2 = new ArrayList<>(); for (int i = 0; i < 75; i++) { - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); Edge edge = constructEdge(name, "default"); @@ -471,7 +479,7 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { String type1 = "typeA"; List edgesType1 = new ArrayList<>(); for (int i = 0; i < 143; i++) { - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); Edge edge = constructEdge(name, type1); @@ -481,7 +489,7 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { String type2 = "typeB"; List edgesType2 = new ArrayList<>(); for (int i = 0; i < 75; i++) { - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); Edge edge = constructEdge(name, type2); @@ -600,7 +608,7 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { String title1 = "Edge title 1"; List edgesTitle1 = new ArrayList<>(); for (int i = 0; i < 125; i++) { - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); Edge edge = constructEdge(name, "default"); @@ -611,7 +619,7 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { String title2 = "Edge title 2"; List edgesTitle2 = new ArrayList<>(); for (int i = 0; i < 143; i++) { - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); Edge edge = constructEdge(name, "default"); @@ -698,7 +706,7 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { String type1 = "typeC"; List edgesType1 = new ArrayList<>(); for (int i = 0; i < 125; i++) { - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); Edge edge = constructEdge(name, type1); @@ -710,7 +718,7 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { String type2 = "typeD"; List edgesType2 = new ArrayList<>(); for (int i = 0; i < 143; i++) { - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); Edge edge = constructEdge(name, type2); @@ -837,4 +845,20 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } + @Test + public void testDeleteEdgeWithDeleteRelationsOk() throws Exception { + EdgeId edgeId = savedEdge("Edge for Test WithRelationsOk").getId(); + testEntityDaoWithRelationsOk(savedTenant.getId(), edgeId, "/api/edge/" + edgeId); + } + + @Test + public void testDeleteEdgeExceptionWithRelationsTransactional() throws Exception { + EdgeId edgeId = savedEdge("Edge for Test WithRelations Transactional Exception").getId(); + testEntityDaoWithRelationsTransactionalException(edgeDao, savedTenant.getId(), edgeId, "/api/edge/" + edgeId); + } + + private Edge savedEdge(String name) { + Edge edge = constructEdge(name, "default"); + return doPost("/api/edge", edge, Edge.class); + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java index f1121dfb14..5bddb9b3a7 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java @@ -22,7 +22,6 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.RandomStringUtils; import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; import org.eclipse.paho.client.mqttv3.MqttAsyncClient; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; @@ -33,6 +32,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.ResultActions; import org.thingsboard.common.util.ThingsBoardExecutors; @@ -40,11 +40,13 @@ import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.EntityViewInfo; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.objects.AttributesEntityView; import org.thingsboard.server.common.data.objects.TelemetryEntityView; import org.thingsboard.server.common.data.page.PageData; @@ -54,6 +56,7 @@ import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.dao.entityview.EntityViewDao; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; @@ -93,6 +96,9 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes List> deleteFutures = new ArrayList<>(); ListeningExecutorService executor; + @SpyBean + private EntityViewDao entityViewDao; + @Before public void beforeTest() throws Exception { executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass())); @@ -114,6 +120,9 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes @After public void afterTest() throws Exception { + + afterTestEntityDaoRemoveByIdWithException (entityViewDao); + executor.shutdownNow(); } @@ -170,7 +179,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes @Test public void testSaveEntityViewWithViolationOfValidation() throws Exception { - EntityView entityView = createEntityView(RandomStringUtils.randomAlphabetic(300), 0, 0); + EntityView entityView = createEntityView(StringUtils.randomAlphabetic(300), 0, 0); Mockito.reset(tbClusterService, auditLogService); @@ -186,7 +195,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes entityView.setName("Normal name"); msgError = msgErrorFieldLength("type"); - entityView.setType(RandomStringUtils.randomAlphabetic(300)); + entityView.setType(StringUtils.randomAlphabetic(300)); doPost("/api/entityView", entityView) .andExpect(status().isBadRequest()) .andExpect(statusReason(containsString(msgError))); @@ -722,7 +731,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes }); viewNameFutures.add(Futures.transform(customerFuture, customerId -> { - String fullName = partOfName + ' ' + RandomStringUtils.randomAlphanumeric(15); + String fullName = partOfName + ' ' + StringUtils.randomAlphanumeric(15); fullName = even ? fullName.toLowerCase() : fullName.toUpperCase(); EntityView view = getNewSavedEntityView(fullName); view.setCustomerId(customerId); @@ -786,4 +795,16 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes Assert.assertEquals(0, pageData.getData().size()); } + + @Test + public void testDeleteEntityViewWithDeleteRelationsOk() throws Exception { + EntityViewId entityViewId = getNewSavedEntityView("EntityView for Test WithRelationsOk").getId(); + testEntityDaoWithRelationsOk(tenantId, entityViewId, "/api/entityView/" + entityViewId); + } + + @Test + public void testDeleteEntityViewExceptionWithRelationsTransactional() throws Exception { + EntityViewId entityViewId = getNewSavedEntityView("EntityView for Test WithRelations Transactional Exception").getId(); + testEntityDaoWithRelationsTransactionalException(entityViewDao, tenantId, entityViewId, "/api/entityView/" + entityViewId); + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseOtaPackageControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseOtaPackageControllerTest.java index 18d76d96e8..c256a6b845 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseOtaPackageControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseOtaPackageControllerTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -30,6 +29,7 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.SaveOtaPackageInfoRequest; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; @@ -137,7 +137,7 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes SaveOtaPackageInfoRequest firmwareInfo = new SaveOtaPackageInfoRequest(); firmwareInfo.setDeviceProfileId(deviceProfileId); firmwareInfo.setType(FIRMWARE); - firmwareInfo.setTitle(RandomStringUtils.randomAlphabetic(300)); + firmwareInfo.setTitle(StringUtils.randomAlphabetic(300)); firmwareInfo.setVersion(VERSION); firmwareInfo.setUsesUrl(false); String msgError = msgErrorFieldLength("title"); @@ -154,7 +154,7 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes ActionType.ADDED, new DataValidationException(msgError)); firmwareInfo.setTitle(TITLE); - firmwareInfo.setVersion(RandomStringUtils.randomAlphabetic(300)); + firmwareInfo.setVersion(StringUtils.randomAlphabetic(300)); msgError = msgErrorFieldLength("version"); doPost("/api/otaPackage", firmwareInfo) .andExpect(status().isBadRequest()) @@ -168,7 +168,7 @@ public abstract class BaseOtaPackageControllerTest extends AbstractControllerTes firmwareInfo.setVersion(VERSION); firmwareInfo.setUsesUrl(true); msgError = msgErrorFieldLength("url"); - firmwareInfo.setUrl(RandomStringUtils.randomAlphabetic(300)); + firmwareInfo.setUrl(StringUtils.randomAlphabetic(300)); doPost("/api/otaPackage", firmwareInfo) .andExpect(status().isBadRequest()) .andExpect(statusReason(containsString(msgError))); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseRuleChainControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseRuleChainControllerTest.java index 33602bbfa9..bec15dac8c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseRuleChainControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseRuleChainControllerTest.java @@ -16,22 +16,25 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.rule.RuleChainDao; import java.util.ArrayList; import java.util.Collections; @@ -47,6 +50,9 @@ public abstract class BaseRuleChainControllerTest extends AbstractControllerTest private Tenant savedTenant; private User tenantAdmin; + @SpyBean + private RuleChainDao ruleChainDao; + @Before public void beforeTest() throws Exception { loginSysAdmin(); @@ -70,6 +76,8 @@ public abstract class BaseRuleChainControllerTest extends AbstractControllerTest public void afterTest() throws Exception { loginSysAdmin(); + afterTestEntityDaoRemoveByIdWithException(ruleChainDao); + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) .andExpect(status().isOk()); } @@ -107,7 +115,7 @@ public abstract class BaseRuleChainControllerTest extends AbstractControllerTest Mockito.reset(tbClusterService, auditLogService); RuleChain ruleChain = new RuleChain(); - ruleChain.setName(RandomStringUtils.randomAlphabetic(300)); + ruleChain.setName(StringUtils.randomAlphabetic(300)); String msgError = msgErrorFieldLength("name"); doPost("/api/ruleChain", ruleChain) .andExpect(status().isBadRequest()) @@ -223,4 +231,21 @@ public abstract class BaseRuleChainControllerTest extends AbstractControllerTest Assert.assertEquals(1, pageData.getTotalElements()); } + @Test + public void testDeleteRuleChainWithDeleteRelationsOk() throws Exception { + RuleChainId ruleChainId = createRuleChain("RuleChain for Test WithRelationsOk").getId(); + testEntityDaoWithRelationsOk(savedTenant.getId(), ruleChainId, "/api/ruleChain/" + ruleChainId); + } + + @Test + public void testDeleteRuleChainExceptionWithRelationsTransactional() throws Exception { + RuleChainId ruleChainId = createRuleChain("RuleChain for Test WithRelations Transactional Exception").getId(); + testEntityDaoWithRelationsTransactionalException(ruleChainDao, savedTenant.getId(), ruleChainId, "/api/ruleChain/" + ruleChainId); + } + + private RuleChain createRuleChain(String name) { + RuleChain ruleChain = new RuleChain(); + ruleChain.setName(name); + return doPost("/api/ruleChain", ruleChain, RuleChain.class); + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseTbResourceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseTbResourceControllerTest.java index d3153f57bc..cd9ea6f357 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseTbResourceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseTbResourceControllerTest.java @@ -16,13 +16,13 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.Tenant; @@ -118,7 +118,7 @@ public abstract class BaseTbResourceControllerTest extends AbstractControllerTes public void saveResourceInfoWithViolationOfLengthValidation() throws Exception { TbResource resource = new TbResource(); resource.setResourceType(ResourceType.JKS); - resource.setTitle(RandomStringUtils.randomAlphabetic(300)); + resource.setTitle(StringUtils.randomAlphabetic(300)); resource.setFileName(DEFAULT_FILE_NAME); resource.setData("Test Data"); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java index 1840f1f8a0..4cba7c1ee0 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java @@ -21,7 +21,6 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -31,6 +30,7 @@ import org.mockito.Mockito; import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.ResultActions; import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantInfo; import org.thingsboard.server.common.data.TenantProfile; @@ -120,7 +120,7 @@ public abstract class BaseTenantControllerTest extends AbstractControllerTest { public void testSaveTenantWithViolationOfValidation() throws Exception { loginSysAdmin(); Tenant tenant = new Tenant(); - tenant.setTitle(RandomStringUtils.randomAlphanumeric(300)); + tenant.setTitle(StringUtils.randomAlphanumeric(300)); Mockito.reset(tbClusterService); @@ -259,7 +259,7 @@ public abstract class BaseTenantControllerTest extends AbstractControllerTest { List> createFutures = new ArrayList<>(134); for (int i = 0; i < 134; i++) { Tenant tenant = new Tenant(); - String suffix = RandomStringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); + String suffix = StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); String title = title1 + suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); tenant.setTitle(title); @@ -274,7 +274,7 @@ public abstract class BaseTenantControllerTest extends AbstractControllerTest { createFutures = new ArrayList<>(127); for (int i = 0; i < 127; i++) { Tenant tenant = new Tenant(); - String suffix = RandomStringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); + String suffix = StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); String title = title2 + suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); tenant.setTitle(title); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseTenantProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseTenantProfileControllerTest.java index 11174618ed..0a4f7ac60b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseTenantProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseTenantProfileControllerTest.java @@ -16,12 +16,12 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.Assert; import org.junit.Test; import org.mockito.ArgumentMatcher; import org.mockito.Mockito; import org.thingsboard.server.common.data.EntityInfo; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.TenantProfileId; @@ -84,7 +84,7 @@ public abstract class BaseTenantProfileControllerTest extends AbstractController Mockito.reset(tbClusterService); - TenantProfile tenantProfile = this.createTenantProfile(RandomStringUtils.randomAlphabetic(300)); + TenantProfile tenantProfile = this.createTenantProfile(StringUtils.randomAlphabetic(300)); doPost("/api/tenantProfile", tenantProfile) .andExpect(status().isBadRequest()) .andExpect(statusReason(containsString(msgErrorFieldLength("name")))); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java index 6e8c15cc0e..141896ed4b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java @@ -18,21 +18,25 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.lang3.RandomStringUtils; +import org.junit.After; import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.http.HttpHeaders; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.user.UserDao; import org.thingsboard.server.service.mail.TestMailService; import java.util.ArrayList; @@ -53,6 +57,16 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { private CustomerId customerNUULId = (CustomerId) createEntityId_NULL_UUID(new Customer()); + @SpyBean + private UserDao userDao; + + @After + public void afterTest() throws Exception { + loginSysAdmin(); + + afterTestEntityDaoRemoveByIdWithException(userDao); + } + @Test public void testSaveUser() throws Exception { loginSysAdmin(); @@ -131,7 +145,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { user.setAuthority(Authority.TENANT_ADMIN); user.setTenantId(tenantId); user.setEmail(email); - user.setFirstName(RandomStringUtils.randomAlphabetic(300)); + user.setFirstName(StringUtils.randomAlphabetic(300)); user.setLastName("Downs"); String msgError = msgErrorFieldLength("first name"); doPost("/api/user", user) @@ -145,7 +159,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { user.setFirstName("Normal name"); msgError = msgErrorFieldLength("last name"); - user.setLastName(RandomStringUtils.randomAlphabetic(300)); + user.setLastName(StringUtils.randomAlphabetic(300)); doPost("/api/user", user) .andExpect(status().isBadRequest()) .andExpect(statusReason(containsString(msgError))); @@ -440,7 +454,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { User user = new User(); user.setAuthority(Authority.TENANT_ADMIN); user.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); + String suffix = StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); String email = email1 + suffix + "@thingsboard.org"; email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase(); user.setEmail(email); @@ -454,7 +468,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { User user = new User(); user.setAuthority(Authority.TENANT_ADMIN); user.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); + String suffix = StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); String email = email2 + suffix + "@thingsboard.org"; email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase(); user.setEmail(email); @@ -605,7 +619,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { User user = new User(); user.setAuthority(Authority.CUSTOMER_USER); user.setCustomerId(customerId); - String suffix = RandomStringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); + String suffix = StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); String email = email1 + suffix + "@thingsboard.org"; email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase(); user.setEmail(email); @@ -619,7 +633,7 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { User user = new User(); user.setAuthority(Authority.CUSTOMER_USER); user.setCustomerId(customerId); - String suffix = RandomStringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); + String suffix = StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); String email = email2 + suffix + "@thingsboard.org"; email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase(); user.setEmail(email); @@ -688,4 +702,29 @@ public abstract class BaseUserControllerTest extends AbstractControllerTest { doDelete("/api/customer/" + customerId.getId().toString()) .andExpect(status().isOk()); } + + + @Test + public void testDeleteUserWithDeleteRelationsOk() throws Exception { + UserId userId = createUser().getId(); + testEntityDaoWithRelationsOk(tenantId, userId, "/api/user/" + userId); + } + + @Test + public void testDeleteUserExceptionWithRelationsTransactional() throws Exception { + UserId userId = createUser().getId(); + testEntityDaoWithRelationsTransactionalException(userDao, tenantId, userId, "/api/user/" + userId); + } + + private User createUser() throws Exception { + loginSysAdmin(); + String email = "tenant2@thingsboard.org"; + User user = new User(); + user.setAuthority(Authority.TENANT_ADMIN); + user.setTenantId(tenantId); + user.setEmail(email); + user.setFirstName("Joe"); + user.setLastName("Downs"); + return doPost("/api/user", user, User.class); + } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseWidgetsBundleControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseWidgetsBundleControllerTest.java index 68ed3636e9..5794c08633 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseWidgetsBundleControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseWidgetsBundleControllerTest.java @@ -16,13 +16,13 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; @@ -109,7 +109,7 @@ public abstract class BaseWidgetsBundleControllerTest extends AbstractController @Test public void testSaveWidgetBundleWithViolationOfLengthValidation() throws Exception { WidgetsBundle widgetsBundle = new WidgetsBundle(); - widgetsBundle.setTitle(RandomStringUtils.randomAlphabetic(300)); + widgetsBundle.setTitle(StringUtils.randomAlphabetic(300)); Mockito.reset(tbClusterService); diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java index 9c30ebbbd0..9839843b0c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java @@ -17,8 +17,6 @@ package org.thingsboard.server.controller; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; -import org.apache.commons.lang3.RandomStringUtils; -import org.apache.commons.lang3.StringUtils; import org.jboss.aerogear.security.otp.Totp; import org.junit.After; import org.junit.Before; @@ -28,6 +26,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; import org.thingsboard.rule.engine.api.SmsService; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionStatus; import org.thingsboard.server.common.data.audit.ActionType; @@ -178,7 +177,7 @@ public abstract class TwoFactorAuthTest extends AbstractControllerTest { logInWithPreVerificationToken(username, password); - Stream.generate(() -> RandomStringUtils.randomNumeric(6)) + Stream.generate(() -> StringUtils.randomNumeric(6)) .limit(9) .forEach(incorrectVerificationCode -> { try { @@ -190,11 +189,11 @@ public abstract class TwoFactorAuthTest extends AbstractControllerTest { } }); - String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + RandomStringUtils.randomNumeric(6)) + String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + StringUtils.randomNumeric(6)) .andExpect(status().isUnauthorized())); assertThat(errorMessage).containsIgnoringCase("account was locked due to exceeded 2fa verification attempts"); - errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + RandomStringUtils.randomNumeric(6)) + errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + StringUtils.randomNumeric(6)) .andExpect(status().isUnauthorized())); assertThat(errorMessage).containsIgnoringCase("user is disabled"); } diff --git a/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java index 2cc217e0c1..dcd354ace5 100644 --- a/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java @@ -26,7 +26,6 @@ import com.google.protobuf.AbstractMessage; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.MessageLite; -import org.apache.commons.lang3.RandomStringUtils; import org.awaitility.Awaitility; import org.junit.After; import org.junit.Assert; @@ -44,10 +43,12 @@ import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.SaveOtaPackageInfoRequest; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; @@ -56,17 +57,32 @@ import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; +import org.thingsboard.server.common.data.device.data.DeviceData; +import org.thingsboard.server.common.data.device.data.MqttDeviceTransportConfiguration; +import org.thingsboard.server.common.data.device.data.PowerMode; +import org.thingsboard.server.common.data.device.data.PowerSavingConfiguration; import org.thingsboard.server.common.data.device.profile.AlarmCondition; import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter; import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey; import org.thingsboard.server.common.data.device.profile.AlarmConditionKeyType; import org.thingsboard.server.common.data.device.profile.AlarmRule; import org.thingsboard.server.common.data.device.profile.AllowCreateNewDevicesDeviceProfileProvisionConfiguration; +import org.thingsboard.server.common.data.device.profile.CoapDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.DefaultCoapDeviceTypeConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.JsonTransportPayloadConfiguration; -import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.SimpleAlarmConditionSpec; +import org.thingsboard.server.common.data.device.profile.SnmpDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.lwm2m.OtherConfiguration; +import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryMappingConfiguration; +import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.AbstractLwM2MBootstrapServerCredential; +import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MBootstrapServerCredential; +import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.NoSecLwM2MBootstrapServerCredential; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; @@ -77,6 +93,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.data.page.PageData; @@ -99,6 +116,9 @@ import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.transport.snmp.SnmpMapping; +import org.thingsboard.server.common.data.transport.snmp.config.SnmpCommunicationConfig; +import org.thingsboard.server.common.data.transport.snmp.config.impl.TelemetryQueryingSnmpCommunicationConfig; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.msg.queue.ServiceType; @@ -139,9 +159,13 @@ import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg; import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg; import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.queue.util.DataDecodingEncodingService; +import org.thingsboard.server.transport.AbstractTransportIntegrationTest; +import org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -172,6 +196,9 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { @Autowired private EdgeEventService edgeEventService; + @Autowired + private DataDecodingEncodingService dataDecodingEncodingService; + @Autowired private TbClusterService clusterService; @@ -201,12 +228,36 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { installation(); edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret()); - edgeImitator.expectMessageAmount(14); + edgeImitator.expectMessageAmount(15); edgeImitator.connect(); + requestEdgeRuleChainMetadata(); + verifyEdgeConnectionAndInitialData(); } + private void requestEdgeRuleChainMetadata() throws Exception { + RuleChainId rootRuleChainId = getEdgeRootRuleChainId(); + RuleChainMetadataRequestMsg.Builder builder = RuleChainMetadataRequestMsg.newBuilder() + .setRuleChainIdMSB(rootRuleChainId.getId().getMostSignificantBits()) + .setRuleChainIdLSB(rootRuleChainId.getId().getLeastSignificantBits()); + testAutoGeneratedCodeByProtobuf(builder); + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder() + .addRuleChainMetadataRequestMsg(builder.build()); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + } + + private RuleChainId getEdgeRootRuleChainId() throws Exception { + List edgeRuleChains = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/ruleChains?", + new TypeReference>() {}, new PageLink(100)).getData(); + for (RuleChain edgeRuleChain : edgeRuleChains) { + if (edgeRuleChain.isRoot()) { + return edgeRuleChain.getId(); + } + } + throw new RuntimeException("Root rule chain not found"); + } + @After public void afterTest() throws Exception { try { @@ -222,10 +273,8 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { private void installation() throws Exception { edge = doPost("/api/edge", constructEdge("Test Edge", "test"), Edge.class); - MqttDeviceProfileTransportConfiguration transportConfiguration = new MqttDeviceProfileTransportConfiguration(); - transportConfiguration.setTransportPayloadTypeConfiguration(new JsonTransportPayloadConfiguration()); - - thermostatDeviceProfile = this.createDeviceProfile(THERMOSTAT_DEVICE_PROFILE_NAME, transportConfiguration); + thermostatDeviceProfile = this.createDeviceProfile(THERMOSTAT_DEVICE_PROFILE_NAME, + createMqttDeviceProfileTransportConfiguration(new JsonTransportPayloadConfiguration(), false)); extendDeviceProfileData(thermostatDeviceProfile); thermostatDeviceProfile = doPost("/api/deviceProfile", thermostatDeviceProfile, DeviceProfile.class); @@ -281,7 +330,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { DeviceUpdateMsg deviceUpdateMsg = deviceUpdateMsgOpt.get(); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); UUID deviceUUID = new UUID(deviceUpdateMsg.getIdMSB(), deviceUpdateMsg.getIdLSB()); - Device device = doGet("/api/device/" + deviceUUID.toString(), Device.class); + Device device = doGet("/api/device/" + deviceUUID, Device.class); Assert.assertNotNull(device); List edgeDevices = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/devices?", new TypeReference>() {}, new PageLink(100)).getData(); @@ -295,7 +344,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { DeviceProfileUpdateMsg deviceProfileUpdateMsg = deviceProfileUpdateMsgOpt.get(); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); UUID deviceProfileUUID = new UUID(deviceProfileUpdateMsg.getIdMSB(), deviceProfileUpdateMsg.getIdLSB()); - DeviceProfile deviceProfile = doGet("/api/deviceProfile/" + deviceProfileUUID.toString(), DeviceProfile.class); + DeviceProfile deviceProfile = doGet("/api/deviceProfile/" + deviceProfileUUID, DeviceProfile.class); Assert.assertNotNull(deviceProfile); Assert.assertNotNull(deviceProfile.getProfileData()); Assert.assertNotNull(deviceProfile.getProfileData().getAlarms()); @@ -321,7 +370,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { RuleChainUpdateMsg ruleChainUpdateMsg = ruleChainUpdateMsgOpt.get(); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, ruleChainUpdateMsg.getMsgType()); UUID ruleChainUUID = new UUID(ruleChainUpdateMsg.getIdMSB(), ruleChainUpdateMsg.getIdLSB()); - RuleChain ruleChain = doGet("/api/ruleChain/" + ruleChainUUID.toString(), RuleChain.class); + RuleChain ruleChain = doGet("/api/ruleChain/" + ruleChainUUID, RuleChain.class); Assert.assertNotNull(ruleChain); List edgeRuleChains = doGetTypedWithPageLink("/api/edge/" + edge.getUuidId() + "/ruleChains?", new TypeReference>() {}, new PageLink(100)).getData(); @@ -329,6 +378,13 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { testAutoGeneratedCodeByProtobuf(ruleChainUpdateMsg); + Optional ruleChainMetadataUpdateOpt = edgeImitator.findMessageByType(RuleChainMetadataUpdateMsg.class); + Assert.assertTrue(ruleChainMetadataUpdateOpt.isPresent()); + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = ruleChainMetadataUpdateOpt.get(); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, ruleChainMetadataUpdateMsg.getMsgType()); + Assert.assertEquals(ruleChainUpdateMsg.getIdMSB(), ruleChainMetadataUpdateMsg.getRuleChainIdMSB()); + Assert.assertEquals(ruleChainUpdateMsg.getIdLSB(), ruleChainMetadataUpdateMsg.getRuleChainIdLSB()); + validateAdminSettings(); } @@ -408,6 +464,226 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { Assert.assertEquals(deviceProfileUpdateMsg.getIdLSB(), deviceProfile.getUuidId().getLeastSignificantBits()); } + @Test + public void testDeviceProfiles_snmp() throws Exception { + DeviceProfile deviceProfile = createDeviceProfileAndDoBasicAssert("SNMP", createSnmpDeviceProfileTransportConfiguration()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceProfileUpdateMsg); + DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); + Assert.assertEquals(deviceProfileUpdateMsg.getIdMSB(), deviceProfile.getUuidId().getMostSignificantBits()); + Assert.assertEquals(deviceProfileUpdateMsg.getIdLSB(), deviceProfile.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(DeviceTransportType.SNMP.name(), deviceProfileUpdateMsg.getTransportType()); + + Optional deviceProfileDataOpt = + dataDecodingEncodingService.decode(deviceProfileUpdateMsg.getProfileDataBytes().toByteArray()); + + Assert.assertTrue(deviceProfileDataOpt.isPresent()); + DeviceProfileData deviceProfileData = deviceProfileDataOpt.get(); + + Assert.assertTrue(deviceProfileData.getTransportConfiguration() instanceof SnmpDeviceProfileTransportConfiguration); + SnmpDeviceProfileTransportConfiguration transportConfiguration = + (SnmpDeviceProfileTransportConfiguration) deviceProfileData.getTransportConfiguration(); + Assert.assertEquals(Integer.valueOf(1000), transportConfiguration.getTimeoutMs()); + Assert.assertEquals(Integer.valueOf(3), transportConfiguration.getRetries()); + + Assert.assertFalse(transportConfiguration.getCommunicationConfigs().isEmpty()); + SnmpCommunicationConfig communicationConfig = transportConfiguration.getCommunicationConfigs().get(0); + Assert.assertTrue(communicationConfig instanceof TelemetryQueryingSnmpCommunicationConfig); + TelemetryQueryingSnmpCommunicationConfig snmpCommunicationConfig = + (TelemetryQueryingSnmpCommunicationConfig) communicationConfig; + + Assert.assertEquals(Long.valueOf(500L), snmpCommunicationConfig.getQueryingFrequencyMs()); + Assert.assertFalse(snmpCommunicationConfig.getMappings().isEmpty()); + + SnmpMapping snmpMapping = snmpCommunicationConfig.getMappings().get(0); + Assert.assertEquals("temperature", snmpMapping.getKey()); + Assert.assertEquals("1.3.3.5.6.7.8.9.1", snmpMapping.getOid()); + Assert.assertEquals(DataType.DOUBLE, snmpMapping.getDataType()); + + removeDeviceProfileAndDoBasicAssert(deviceProfile); + } + + @Test + public void testDeviceProfiles_lwm2m() throws Exception { + DeviceProfile deviceProfile = createDeviceProfileAndDoBasicAssert("LWM2M", createLwm2mDeviceProfileTransportConfiguration()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceProfileUpdateMsg); + DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); + Assert.assertEquals(deviceProfileUpdateMsg.getIdMSB(), deviceProfile.getUuidId().getMostSignificantBits()); + Assert.assertEquals(deviceProfileUpdateMsg.getIdLSB(), deviceProfile.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(DeviceTransportType.LWM2M.name(), deviceProfileUpdateMsg.getTransportType()); + + Optional deviceProfileDataOpt = + dataDecodingEncodingService.decode(deviceProfileUpdateMsg.getProfileDataBytes().toByteArray()); + + Assert.assertTrue(deviceProfileDataOpt.isPresent()); + DeviceProfileData deviceProfileData = deviceProfileDataOpt.get(); + + Assert.assertTrue(deviceProfileData.getTransportConfiguration() instanceof Lwm2mDeviceProfileTransportConfiguration); + Lwm2mDeviceProfileTransportConfiguration transportConfiguration = + (Lwm2mDeviceProfileTransportConfiguration) deviceProfileData.getTransportConfiguration(); + + OtherConfiguration clientLwM2mSettings = transportConfiguration.getClientLwM2mSettings(); + Assert.assertEquals(PowerMode.DRX, clientLwM2mSettings.getPowerMode()); + Assert.assertEquals(Integer.valueOf(1), clientLwM2mSettings.getFwUpdateStrategy()); + Assert.assertEquals(Integer.valueOf(1), clientLwM2mSettings.getSwUpdateStrategy()); + Assert.assertEquals(Integer.valueOf(1), clientLwM2mSettings.getClientOnlyObserveAfterConnect()); + + Assert.assertTrue(transportConfiguration.isBootstrapServerUpdateEnable()); + + Assert.assertFalse(transportConfiguration.getBootstrap().isEmpty()); + LwM2MBootstrapServerCredential lwM2MBootstrapServerCredential = transportConfiguration.getBootstrap().get(0); + Assert.assertTrue(lwM2MBootstrapServerCredential instanceof NoSecLwM2MBootstrapServerCredential); + NoSecLwM2MBootstrapServerCredential noSecLwM2MBootstrapServerCredential = (NoSecLwM2MBootstrapServerCredential) lwM2MBootstrapServerCredential; + + Assert.assertEquals("PUBLIC_KEY", noSecLwM2MBootstrapServerCredential.getServerPublicKey()); + Assert.assertEquals(Integer.valueOf(123), noSecLwM2MBootstrapServerCredential.getShortServerId()); + Assert.assertTrue(noSecLwM2MBootstrapServerCredential.isBootstrapServerIs()); + Assert.assertEquals("localhost", noSecLwM2MBootstrapServerCredential.getHost()); + Assert.assertEquals(Integer.valueOf(5687), noSecLwM2MBootstrapServerCredential.getPort()); + + TelemetryMappingConfiguration observeAttr = transportConfiguration.getObserveAttr(); + Assert.assertEquals("batteryLevel", observeAttr.getKeyName().get("/3_1.0/0/9")); + Assert.assertTrue(observeAttr.getObserve().isEmpty()); + Assert.assertTrue(observeAttr.getAttribute().isEmpty()); + Assert.assertFalse(observeAttr.getTelemetry().isEmpty()); + Assert.assertTrue(observeAttr.getTelemetry().contains("/3_1.0/0/9")); + Assert.assertTrue(observeAttr.getAttributeLwm2m().isEmpty()); + + removeDeviceProfileAndDoBasicAssert(deviceProfile); + } + + @Test + public void testDeviceProfiles_coap() throws Exception { + DeviceProfile deviceProfile = createDeviceProfileAndDoBasicAssert("COAP", createCoapDeviceProfileTransportConfiguration()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceProfileUpdateMsg); + DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); + Assert.assertEquals(deviceProfileUpdateMsg.getIdMSB(), deviceProfile.getUuidId().getMostSignificantBits()); + Assert.assertEquals(deviceProfileUpdateMsg.getIdLSB(), deviceProfile.getUuidId().getLeastSignificantBits()); + Assert.assertEquals(DeviceTransportType.COAP.name(), deviceProfileUpdateMsg.getTransportType()); + + Optional deviceProfileDataOpt = + dataDecodingEncodingService.decode(deviceProfileUpdateMsg.getProfileDataBytes().toByteArray()); + + Assert.assertTrue(deviceProfileDataOpt.isPresent()); + DeviceProfileData deviceProfileData = deviceProfileDataOpt.get(); + + Assert.assertTrue(deviceProfileData.getTransportConfiguration() instanceof CoapDeviceProfileTransportConfiguration); + CoapDeviceProfileTransportConfiguration transportConfiguration = + (CoapDeviceProfileTransportConfiguration) deviceProfileData.getTransportConfiguration(); + + PowerSavingConfiguration clientSettings = transportConfiguration.getClientSettings(); + + Assert.assertEquals(PowerMode.DRX, clientSettings.getPowerMode()); + Assert.assertEquals(Long.valueOf(1L), clientSettings.getEdrxCycle()); + Assert.assertEquals(Long.valueOf(1L), clientSettings.getPsmActivityTimer()); + Assert.assertEquals(Long.valueOf(1L), clientSettings.getPagingTransmissionWindow()); + + Assert.assertTrue(transportConfiguration.getCoapDeviceTypeConfiguration() instanceof DefaultCoapDeviceTypeConfiguration); + DefaultCoapDeviceTypeConfiguration coapDeviceTypeConfiguration = + (DefaultCoapDeviceTypeConfiguration) transportConfiguration.getCoapDeviceTypeConfiguration(); + + Assert.assertTrue(coapDeviceTypeConfiguration.getTransportPayloadTypeConfiguration() instanceof ProtoTransportPayloadConfiguration); + + ProtoTransportPayloadConfiguration protoTransportPayloadConfiguration = + (ProtoTransportPayloadConfiguration) coapDeviceTypeConfiguration.getTransportPayloadTypeConfiguration(); + + Assert.assertEquals(AbstractTransportIntegrationTest.DEVICE_TELEMETRY_PROTO_SCHEMA, protoTransportPayloadConfiguration.getDeviceTelemetryProtoSchema()); + Assert.assertEquals(AbstractTransportIntegrationTest.DEVICE_ATTRIBUTES_PROTO_SCHEMA, protoTransportPayloadConfiguration.getDeviceAttributesProtoSchema()); + Assert.assertEquals(AbstractTransportIntegrationTest.DEVICE_RPC_RESPONSE_PROTO_SCHEMA, protoTransportPayloadConfiguration.getDeviceRpcResponseProtoSchema()); + Assert.assertEquals(AbstractTransportIntegrationTest.DEVICE_RPC_REQUEST_PROTO_SCHEMA, protoTransportPayloadConfiguration.getDeviceRpcRequestProtoSchema()); + + removeDeviceProfileAndDoBasicAssert(deviceProfile); + } + + private DeviceProfile createDeviceProfileAndDoBasicAssert(String deviceProfileName, DeviceProfileTransportConfiguration deviceProfileTransportConfiguration) throws Exception { + DeviceProfile deviceProfile = this.createDeviceProfile(deviceProfileName, deviceProfileTransportConfiguration); + extendDeviceProfileData(deviceProfile); + edgeImitator.expectMessageAmount(1); + deviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + return deviceProfile; + } + + private void removeDeviceProfileAndDoBasicAssert(DeviceProfile deviceProfile) throws Exception { + edgeImitator.expectMessageAmount(1); + doDelete("/api/deviceProfile/" + deviceProfile.getUuidId()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof DeviceProfileUpdateMsg); + DeviceProfileUpdateMsg deviceProfileUpdateMsg = (DeviceProfileUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, deviceProfileUpdateMsg.getMsgType()); + Assert.assertEquals(deviceProfileUpdateMsg.getIdMSB(), deviceProfile.getUuidId().getMostSignificantBits()); + Assert.assertEquals(deviceProfileUpdateMsg.getIdLSB(), deviceProfile.getUuidId().getLeastSignificantBits()); + } + + private SnmpDeviceProfileTransportConfiguration createSnmpDeviceProfileTransportConfiguration() { + SnmpDeviceProfileTransportConfiguration transportConfiguration = new SnmpDeviceProfileTransportConfiguration(); + List communicationConfigs = new ArrayList<>(); + TelemetryQueryingSnmpCommunicationConfig communicationConfig = new TelemetryQueryingSnmpCommunicationConfig(); + communicationConfig.setQueryingFrequencyMs(500L); + List mappings = new ArrayList<>(); + mappings.add(new SnmpMapping("1.3.3.5.6.7.8.9.1", "temperature", DataType.DOUBLE)); + communicationConfig.setMappings(mappings); + communicationConfigs.add(communicationConfig); + transportConfiguration.setCommunicationConfigs(communicationConfigs); + transportConfiguration.setTimeoutMs(1000); + transportConfiguration.setRetries(3); + return transportConfiguration; + } + + private Lwm2mDeviceProfileTransportConfiguration createLwm2mDeviceProfileTransportConfiguration() { + Lwm2mDeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration(); + + OtherConfiguration clientLwM2mSettings = JacksonUtil.fromString(AbstractLwM2MIntegrationTest.CLIENT_LWM2M_SETTINGS, OtherConfiguration.class); + transportConfiguration.setClientLwM2mSettings(clientLwM2mSettings); + + transportConfiguration.setBootstrapServerUpdateEnable(true); + + TelemetryMappingConfiguration observeAttrConfiguration = + JacksonUtil.fromString(AbstractLwM2MIntegrationTest.OBSERVE_ATTRIBUTES_WITH_PARAMS, TelemetryMappingConfiguration.class); + transportConfiguration.setObserveAttr(observeAttrConfiguration); + + List bootstrap = new ArrayList<>(); + AbstractLwM2MBootstrapServerCredential bootstrapServerCredential = new NoSecLwM2MBootstrapServerCredential(); + bootstrapServerCredential.setServerPublicKey("PUBLIC_KEY"); + bootstrapServerCredential.setShortServerId(123); + bootstrapServerCredential.setBootstrapServerIs(true); + bootstrapServerCredential.setHost("localhost"); + bootstrapServerCredential.setPort(5687); + bootstrap.add(bootstrapServerCredential); + transportConfiguration.setBootstrap(bootstrap); + + return transportConfiguration; + } + + private CoapDeviceProfileTransportConfiguration createCoapDeviceProfileTransportConfiguration() { + CoapDeviceProfileTransportConfiguration transportConfiguration = new CoapDeviceProfileTransportConfiguration(); + PowerSavingConfiguration clientSettings = new PowerSavingConfiguration(); + clientSettings.setPowerMode(PowerMode.DRX); + clientSettings.setEdrxCycle(1L); + clientSettings.setPsmActivityTimer(1L); + clientSettings.setPagingTransmissionWindow(1L); + transportConfiguration.setClientSettings(clientSettings); + DefaultCoapDeviceTypeConfiguration coapDeviceTypeConfiguration = new DefaultCoapDeviceTypeConfiguration(); + ProtoTransportPayloadConfiguration transportPayloadTypeConfiguration = new ProtoTransportPayloadConfiguration(); + transportPayloadTypeConfiguration.setDeviceTelemetryProtoSchema(AbstractTransportIntegrationTest.DEVICE_TELEMETRY_PROTO_SCHEMA); + transportPayloadTypeConfiguration.setDeviceAttributesProtoSchema(AbstractTransportIntegrationTest.DEVICE_ATTRIBUTES_PROTO_SCHEMA); + transportPayloadTypeConfiguration.setDeviceRpcResponseProtoSchema(AbstractTransportIntegrationTest.DEVICE_RPC_RESPONSE_PROTO_SCHEMA); + transportPayloadTypeConfiguration.setDeviceRpcRequestProtoSchema(AbstractTransportIntegrationTest.DEVICE_RPC_REQUEST_PROTO_SCHEMA); + coapDeviceTypeConfiguration.setTransportPayloadTypeConfiguration(transportPayloadTypeConfiguration); + transportConfiguration.setCoapDeviceTypeConfiguration(coapDeviceTypeConfiguration); + return transportConfiguration; + } + @Test public void testDevices() throws Exception { // 1 @@ -1233,7 +1509,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { @Test public void testSendDeviceToCloudWithNameThatAlreadyExistsOnCloud() throws Exception { - String deviceOnCloudName = RandomStringUtils.randomAlphanumeric(15); + String deviceOnCloudName = StringUtils.randomAlphanumeric(15); Device deviceOnCloud = saveDevice(deviceOnCloudName, "Default"); UUID uuid = Uuids.timeBased(); @@ -1878,8 +2154,16 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { OtaPackageInfo firmwareOtaPackageInfo = saveOtaPackageInfo(thermostatDeviceProfile.getId()); Assert.assertTrue(edgeImitator.waitForMessages()); - Device savedDevice = saveDevice(RandomStringUtils.randomAlphanumeric(15), thermostatDeviceProfile.getName()); + Device savedDevice = saveDevice(StringUtils.randomAlphanumeric(15), thermostatDeviceProfile.getName()); savedDevice.setFirmwareId(firmwareOtaPackageInfo.getId()); + + DeviceData deviceData = new DeviceData(); + deviceData.setConfiguration(new DefaultDeviceConfiguration()); + MqttDeviceTransportConfiguration transportConfiguration = new MqttDeviceTransportConfiguration(); + transportConfiguration.getProperties().put("topic", "tb_rule_engine.thermostat"); + deviceData.setTransportConfiguration(transportConfiguration); + savedDevice.setDeviceData(deviceData); + savedDevice = doPost("/api/device", savedDevice, Device.class); // wait until device UPDATED event is sent to edge notification service @@ -1899,6 +2183,14 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { Assert.assertEquals(deviceUpdateMsg.getType(), savedDevice.getType()); Assert.assertEquals(firmwareOtaPackageInfo.getUuidId().getMostSignificantBits(), deviceUpdateMsg.getFirmwareIdMSB()); Assert.assertEquals(firmwareOtaPackageInfo.getUuidId().getLeastSignificantBits(), deviceUpdateMsg.getFirmwareIdLSB()); + Optional deviceDataOpt = + dataDecodingEncodingService.decode(deviceUpdateMsg.getDeviceDataBytes().toByteArray()); + Assert.assertTrue(deviceDataOpt.isPresent()); + deviceData = deviceDataOpt.get(); + Assert.assertTrue(deviceData.getTransportConfiguration() instanceof MqttDeviceTransportConfiguration); + MqttDeviceTransportConfiguration mqttDeviceTransportConfiguration = + (MqttDeviceTransportConfiguration) deviceData.getTransportConfiguration(); + Assert.assertEquals("tb_rule_engine.thermostat", mqttDeviceTransportConfiguration.getProperties().get("topic")); return savedDevice; } @@ -1942,7 +2234,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest { SaveOtaPackageInfoRequest firmwareInfo = new SaveOtaPackageInfoRequest(); firmwareInfo.setDeviceProfileId(deviceProfileId); firmwareInfo.setType(FIRMWARE); - firmwareInfo.setTitle("Firmware Edge " + RandomStringUtils.randomAlphanumeric(3)); + firmwareInfo.setTitle("Firmware Edge " + StringUtils.randomAlphanumeric(3)); firmwareInfo.setVersion("v1.0"); firmwareInfo.setTag("My firmware #1 v1.0"); firmwareInfo.setUsesUrl(true); diff --git a/application/src/test/java/org/thingsboard/server/service/sms/smpp/SmppSmsSenderTest.java b/application/src/test/java/org/thingsboard/server/service/sms/smpp/SmppSmsSenderTest.java index 7cbc8c03d2..220013b430 100644 --- a/application/src/test/java/org/thingsboard/server/service/sms/smpp/SmppSmsSenderTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sms/smpp/SmppSmsSenderTest.java @@ -15,28 +15,23 @@ */ package org.thingsboard.server.service.sms.smpp; -import org.apache.commons.lang3.StringUtils; -import org.assertj.core.api.Assertions; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; import org.smpp.Session; import org.smpp.pdu.SubmitSMResp; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.sms.config.SmppSmsProviderConfiguration; import java.lang.reflect.Constructor; -import java.net.UnknownHostException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; diff --git a/application/src/test/java/org/thingsboard/server/transport/AbstractTransportIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/AbstractTransportIntegrationTest.java index 09acd96d47..bdda6d52ed 100644 --- a/application/src/test/java/org/thingsboard/server/transport/AbstractTransportIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/AbstractTransportIntegrationTest.java @@ -33,7 +33,7 @@ public abstract class AbstractTransportIntegrationTest extends AbstractControlle protected static final AtomicInteger atomicInteger = new AtomicInteger(2); - protected static final String DEVICE_TELEMETRY_PROTO_SCHEMA = "syntax =\"proto3\";\n" + + public static final String DEVICE_TELEMETRY_PROTO_SCHEMA = "syntax =\"proto3\";\n" + "\n" + "package test;\n" + "\n" + @@ -54,7 +54,7 @@ public abstract class AbstractTransportIntegrationTest extends AbstractControlle " }\n" + "}"; - protected static final String DEVICE_ATTRIBUTES_PROTO_SCHEMA = "syntax =\"proto3\";\n" + + public static final String DEVICE_ATTRIBUTES_PROTO_SCHEMA = "syntax =\"proto3\";\n" + "\n" + "package test;\n" + "\n" + @@ -75,14 +75,14 @@ public abstract class AbstractTransportIntegrationTest extends AbstractControlle " }\n" + "}"; - protected static final String DEVICE_RPC_RESPONSE_PROTO_SCHEMA = "syntax =\"proto3\";\n" + + public static final String DEVICE_RPC_RESPONSE_PROTO_SCHEMA = "syntax =\"proto3\";\n" + "package rpc;\n" + "\n" + "message RpcResponseMsg {\n" + " optional string payload = 1;\n" + "}"; - protected static final String DEVICE_RPC_REQUEST_PROTO_SCHEMA = "syntax =\"proto3\";\n" + + public static final String DEVICE_RPC_REQUEST_PROTO_SCHEMA = "syntax =\"proto3\";\n" + "package rpc;\n" + "\n" + "message RpcRequestMsg {\n" + diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java index ca5b40efef..2d034d3b41 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/rpc/AbstractCoapServerSideRpcIntegrationTest.java @@ -22,13 +22,13 @@ import com.google.protobuf.DynamicMessage; import com.google.protobuf.InvalidProtocolBufferException; import com.squareup.wire.schema.internal.parser.ProtoFileElement; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.eclipse.californium.core.CoapHandler; import org.eclipse.californium.core.CoapObserveRelation; import org.eclipse.californium.core.CoapResponse; import org.eclipse.californium.core.coap.CoAP; import org.eclipse.californium.core.coap.MediaTypeRegistry; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.device.profile.CoapDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.CoapDeviceTypeConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultCoapDeviceTypeConfiguration; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index 9f04d382fb..ac272d0bb4 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -144,7 +144,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractControllerTes " \"attributeLwm2m\": {}\n" + " }"; - protected final String OBSERVE_ATTRIBUTES_WITH_PARAMS = + public static final String OBSERVE_ATTRIBUTES_WITH_PARAMS = " {\n" + " \"keyName\": {\n" + @@ -159,7 +159,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractControllerTes " \"attributeLwm2m\": {}\n" + " }"; - protected final String CLIENT_LWM2M_SETTINGS = + public static final String CLIENT_LWM2M_SETTINGS = " {\n" + " \"edrxCycle\": null,\n" + " \"powerMode\": \"DRX\",\n" + diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java index 9fbdff3fa5..917e41d1b4 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java @@ -17,19 +17,14 @@ package org.thingsboard.server.transport.mqtt; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.MqttAsyncClient; -import org.eclipse.paho.client.mqttv3.MqttConnectOptions; -import org.eclipse.paho.client.mqttv3.MqttException; -import org.eclipse.paho.client.mqttv3.MqttMessage; -import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; import org.springframework.test.context.TestPropertySource; -import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.device.profile.AllowCreateNewDevicesDeviceProfileProvisionConfiguration; import org.thingsboard.server.common.data.device.profile.CheckPreProvisionedDevicesDeviceProfileProvisionConfiguration; diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/credentials/BasicMqttCredentialsTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/credentials/BasicMqttCredentialsTest.java index 8ef02a63dc..0884f1457c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/credentials/BasicMqttCredentialsTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/credentials/BasicMqttCredentialsTest.java @@ -16,12 +16,12 @@ package org.thingsboard.server.transport.mqtt.credentials; import com.fasterxml.jackson.core.type.TypeReference; -import org.apache.commons.lang3.RandomStringUtils; import org.eclipse.paho.client.mqttv3.MqttSecurityException; import org.junit.Before; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; @@ -102,8 +102,8 @@ public class BasicMqttCredentialsTest extends AbstractMqttIntegrationTest { mqttTestClient4.connectAndWait(USER_NAME2, PASSWORD); // Also correct. Random clientId and password, but matches access token - MqttTestClient mqttTestClient5 = new MqttTestClient(RandomStringUtils.randomAlphanumeric(10)); - mqttTestClient5.connectAndWait(USER_NAME2, RandomStringUtils.randomAlphanumeric(10)); + MqttTestClient mqttTestClient5 = new MqttTestClient(StringUtils.randomAlphanumeric(10)); + mqttTestClient5.connectAndWait(USER_NAME2, StringUtils.randomAlphanumeric(10)); testTelemetryIsDelivered(accessTokenDevice, mqttTestClient1); testTelemetryIsDelivered(clientIdDevice, mqttTestClient2); @@ -131,7 +131,7 @@ public class BasicMqttCredentialsTest extends AbstractMqttIntegrationTest { } private void testTelemetryIsDelivered(Device device, MqttTestClient client, boolean ok) throws Exception { - String randomKey = RandomStringUtils.randomAlphanumeric(10); + String randomKey = StringUtils.randomAlphanumeric(10); List expectedKeys = Arrays.asList(randomKey); client.publishAndWait(DEVICE_TELEMETRY_TOPIC, JacksonUtil.toString(JacksonUtil.newObjectNode().put(randomKey, true)).getBytes()); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java index 0abd0f1d6c..067af192e4 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java @@ -25,12 +25,12 @@ import com.nimbusds.jose.util.StandardCharset; import com.squareup.wire.schema.internal.parser.ProtoFileElement; import io.netty.handler.codec.mqtt.MqttQoS; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; diff --git a/common/actor/pom.xml b/common/actor/pom.xml index a565ed9052..268b8e698b 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT common org.thingsboard.common diff --git a/common/cache/pom.xml b/common/cache/pom.xml index d3f2c838c8..e2b743574f 100644 --- a/common/cache/pom.xml +++ b/common/cache/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT common org.thingsboard.common diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java index 2cf67e35aa..f68dabf9eb 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TBRedisClusterConfiguration.java @@ -15,7 +15,7 @@ */ package org.thingsboard.server.cache; -import org.apache.commons.lang3.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index 62175cae08..3df2d5e8bc 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT common org.thingsboard.common diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml index 5eba5c7175..596aa90e98 100644 --- a/common/coap-server/pom.xml +++ b/common/coap-server/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT common org.thingsboard.common diff --git a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsCertificateVerifier.java b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsCertificateVerifier.java index ce8b093b4d..15807c4154 100644 --- a/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsCertificateVerifier.java +++ b/common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsCertificateVerifier.java @@ -27,7 +27,7 @@ import org.eclipse.californium.scandium.dtls.HandshakeException; import org.eclipse.californium.scandium.dtls.HandshakeResultHandler; import org.eclipse.californium.scandium.dtls.x509.NewAdvancedCertificateVerifier; import org.eclipse.californium.scandium.util.ServerNames; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.msg.EncryptionUtil; diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index 4b15effa44..c75186c0a0 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT common org.thingsboard.common diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraDriverOptions.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraDriverOptions.java index 986be6d515..3c6d8ce1dd 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraDriverOptions.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/cassandra/CassandraDriverOptions.java @@ -23,7 +23,7 @@ import com.datastax.oss.driver.api.core.config.ProgrammaticDriverConfigLoaderBui import com.datastax.oss.driver.api.core.metrics.DefaultSessionMetric; import com.datastax.oss.driver.api.core.metrics.DefaultNodeMetric; import lombok.Data; -import org.apache.commons.lang3.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java index b8688b1524..cf17eec88d 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; import org.thingsboard.server.common.data.kv.TsKvEntry; @@ -32,6 +33,8 @@ import java.util.List; */ public interface TimeseriesService { + ListenableFuture> findAllByQueries(TenantId tenantId, EntityId entityId, List queries); + ListenableFuture> findAll(TenantId tenantId, EntityId entityId, List queries); ListenableFuture> findLatest(TenantId tenantId, EntityId entityId, Collection keys); diff --git a/common/data/pom.xml b/common/data/pom.xml index 75758021c8..decbba55aa 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT common org.thingsboard.common diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityFieldsData.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityFieldsData.java index c3cd2b12b4..04c526b6fe 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityFieldsData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityFieldsData.java @@ -50,6 +50,10 @@ public class EntityFieldsData { } public String getFieldValue(String field) { + return getFieldValue(field, false); + } + + public String getFieldValue(String field, boolean ignoreNullStrings) { String[] fieldsTree = field.split("\\."); JsonNode current = fieldsData; for (String key : fieldsTree) { @@ -61,6 +65,9 @@ public class EntityFieldsData { } } if (current != null) { + if(current.isNull() && ignoreNullStrings){ + return null; + } if (current.isValueNode()) { return current.asText(); } else { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java index f76757f522..18d3dfff1e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java @@ -16,11 +16,11 @@ package org.thingsboard.server.common.data; import com.google.common.base.Splitter; +import org.apache.commons.lang3.RandomStringUtils; import static org.apache.commons.lang3.StringUtils.repeat; public class StringUtils { - public static final String EMPTY = ""; public static final int INDEX_NOT_FOUND = -1; @@ -45,7 +45,7 @@ public class StringUtils { if (isEmpty(str) || isEmpty(remove)) { return str; } - if (str.startsWith(remove)){ + if (str.startsWith(remove)) { return str.substring(remove.length()); } return str; @@ -98,4 +98,72 @@ public class StringUtils { return Splitter.fixedLength(maxPartSize).split(value); } + public static boolean equalsIgnoreCase(String str1, String str2) { + return str1 == null ? str2 == null : str1.equalsIgnoreCase(str2); + } + + public static String join(String[] keyArray, String lwm2mSeparatorPath) { + return org.apache.commons.lang3.StringUtils.join(keyArray, lwm2mSeparatorPath); + } + + public static String trimToNull(String toString) { + return org.apache.commons.lang3.StringUtils.trimToNull(toString); + } + + public static boolean isNoneEmpty(String str) { + return org.apache.commons.lang3.StringUtils.isNoneEmpty(str); + } + + public static boolean endsWith(String str, String suffix) { + return org.apache.commons.lang3.StringUtils.endsWith(str, suffix); + } + + public static boolean hasLength(String str) { + return org.springframework.util.StringUtils.hasLength(str); + } + + public static boolean isNoneBlank(String... str) { + return org.apache.commons.lang3.StringUtils.isNoneBlank(str); + } + + public static boolean hasText(String str) { + return org.springframework.util.StringUtils.hasText(str); + } + + public static String defaultString(String s, String defaultValue) { + return org.apache.commons.lang3.StringUtils.defaultString(s, defaultValue); + } + + public static boolean isNumeric(String str) { + return org.apache.commons.lang3.StringUtils.isNumeric(str); + } + + public static boolean equals(String str1, String str2) { + return org.apache.commons.lang3.StringUtils.equals(str1, str2); + } + + public static String substringAfterLast(String str, String sep) { + return org.apache.commons.lang3.StringUtils.substringAfterLast(str, sep); + } + + public static String randomNumeric(int length) { + return RandomStringUtils.randomNumeric(length); + } + + public static String random(int length) { + return RandomStringUtils.random(length); + } + + public static String random(int length, String chars) { + return RandomStringUtils.random(length, chars); + } + + public static String randomAlphanumeric(int count) { + return RandomStringUtils.randomAlphanumeric(count); + } + + public static String randomAlphabetic(int count) { + return RandomStringUtils.randomAlphabetic(count); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/CoapDeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/CoapDeviceTransportConfiguration.java index f38749a868..e603d74198 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/CoapDeviceTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/CoapDeviceTransportConfiguration.java @@ -27,6 +27,8 @@ import java.util.Map; @Data public class CoapDeviceTransportConfiguration extends PowerSavingConfiguration implements DeviceTransportConfiguration { + private static final long serialVersionUID = 6061442236008925609L; + @JsonIgnore private Map properties = new HashMap<>(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceConfiguration.java index 3e03dffd66..268cf2da6e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DefaultDeviceConfiguration.java @@ -23,6 +23,8 @@ import org.thingsboard.server.common.data.DeviceProfileType; @Data public class DefaultDeviceConfiguration implements DeviceConfiguration { + private static final long serialVersionUID = -2225378639573611325L; + @Override public DeviceProfileType getType() { return DeviceProfileType.DEFAULT; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceConfiguration.java index 83ed57fb84..c7f53a84c1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceConfiguration.java @@ -22,6 +22,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import io.swagger.annotations.ApiModel; import org.thingsboard.server.common.data.DeviceProfileType; +import java.io.Serializable; + @ApiModel @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( @@ -30,7 +32,7 @@ import org.thingsboard.server.common.data.DeviceProfileType; property = "type") @JsonSubTypes({ @JsonSubTypes.Type(value = DefaultDeviceConfiguration.class, name = "DEFAULT")}) -public interface DeviceConfiguration { +public interface DeviceConfiguration extends Serializable { @JsonIgnore DeviceProfileType getType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceData.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceData.java index 03187f180b..8a87c18059 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceData.java @@ -19,9 +19,13 @@ import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; +import java.io.Serializable; + @ApiModel @Data -public class DeviceData { +public class DeviceData implements Serializable { + + private static final long serialVersionUID = -3771567735290681274L; @ApiModelProperty(position = 1, value = "Device configuration for device profile type. DEFAULT is only supported value for now") private DeviceConfiguration configuration; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/PowerSavingConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/PowerSavingConfiguration.java index ebed17c585..51870b67c9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/PowerSavingConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/PowerSavingConfiguration.java @@ -17,8 +17,13 @@ package org.thingsboard.server.common.data.device.data; import lombok.Data; +import java.io.Serializable; + @Data -public class PowerSavingConfiguration { +public class PowerSavingConfiguration implements Serializable { + + private static final long serialVersionUID = 2905389805488525362L; + private PowerMode powerMode; private Long psmActivityTimer; private Long edrxCycle; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/SnmpDeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/SnmpDeviceTransportConfiguration.java index 041fe4752c..e3f46a532b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/SnmpDeviceTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/SnmpDeviceTransportConfiguration.java @@ -18,15 +18,12 @@ package org.thingsboard.server.common.data.device.data; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import lombok.ToString; -import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.transport.snmp.AuthenticationProtocol; import org.thingsboard.server.common.data.transport.snmp.PrivacyProtocol; import org.thingsboard.server.common.data.transport.snmp.SnmpProtocolVersion; -import java.util.Objects; - @Data @ToString(of = {"host", "port", "protocolVersion"}) public class SnmpDeviceTransportConfiguration implements DeviceTransportConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CoapDeviceTypeConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CoapDeviceTypeConfiguration.java index e60f421635..a4bbe9c2c3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CoapDeviceTypeConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/CoapDeviceTypeConfiguration.java @@ -21,6 +21,8 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.thingsboard.server.common.data.CoapDeviceType; +import java.io.Serializable; + @JsonIgnoreProperties(ignoreUnknown = true) @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -29,7 +31,7 @@ import org.thingsboard.server.common.data.CoapDeviceType; @JsonSubTypes({ @JsonSubTypes.Type(value = DefaultCoapDeviceTypeConfiguration.class, name = "DEFAULT"), @JsonSubTypes.Type(value = EfentoCoapDeviceTypeConfiguration.class, name = "EFENTO")}) -public interface CoapDeviceTypeConfiguration { +public interface CoapDeviceTypeConfiguration extends Serializable { @JsonIgnore CoapDeviceType getCoapDeviceType(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultCoapDeviceTypeConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultCoapDeviceTypeConfiguration.java index c835d58af4..cfa5c08cd1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultCoapDeviceTypeConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultCoapDeviceTypeConfiguration.java @@ -21,6 +21,8 @@ import org.thingsboard.server.common.data.CoapDeviceType; @Data public class DefaultCoapDeviceTypeConfiguration implements CoapDeviceTypeConfiguration { + private static final long serialVersionUID = -4287100699186773773L; + private TransportPayloadTypeConfiguration transportPayloadTypeConfiguration; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/EfentoCoapDeviceTypeConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/EfentoCoapDeviceTypeConfiguration.java index 4ac85b4336..29cc1983e6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/EfentoCoapDeviceTypeConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/EfentoCoapDeviceTypeConfiguration.java @@ -21,6 +21,8 @@ import org.thingsboard.server.common.data.CoapDeviceType; @Data public class EfentoCoapDeviceTypeConfiguration implements CoapDeviceTypeConfiguration { + private static final long serialVersionUID = -8523081152598707064L; + @Override public CoapDeviceType getCoapDeviceType() { return CoapDeviceType.EFENTO; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/ObjectAttributes.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/ObjectAttributes.java index bcaedaa801..60245f6869 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/ObjectAttributes.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/ObjectAttributes.java @@ -18,9 +18,13 @@ package org.thingsboard.server.common.data.device.profile.lwm2m; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Data; +import java.io.Serializable; + @Data @JsonInclude(JsonInclude.Include.NON_NULL) -public class ObjectAttributes { +public class ObjectAttributes implements Serializable { + + private static final long serialVersionUID = 4765123984733721312L; private Long dim; private String ver; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java index 73caea088d..13bf9f4793 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/OtherConfiguration.java @@ -18,10 +18,12 @@ package org.thingsboard.server.common.data.device.profile.lwm2m; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.device.data.PowerMode; import org.thingsboard.server.common.data.device.data.PowerSavingConfiguration; +@EqualsAndHashCode(callSuper = true) @Data @NoArgsConstructor @AllArgsConstructor diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java index cb6f4e7695..dac6f76e6a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java @@ -19,13 +19,16 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import java.io.Serializable; import java.util.Map; import java.util.Set; @Data @NoArgsConstructor @AllArgsConstructor -public class TelemetryMappingConfiguration { +public class TelemetryMappingConfiguration implements Serializable { + + private static final long serialVersionUID = -7594999741305410419L; private Map keyName; private Set observe; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MBootstrapServerCredential.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MBootstrapServerCredential.java index 6a563c8418..6367036ecb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MBootstrapServerCredential.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/LwM2MBootstrapServerCredential.java @@ -21,6 +21,8 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode; +import java.io.Serializable; + @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, property = "securityMode") @@ -31,7 +33,7 @@ import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurity @JsonSubTypes.Type(value = X509LwM2MBootstrapServerCredential.class, name = "X509") }) @JsonIgnoreProperties(ignoreUnknown = true) -public interface LwM2MBootstrapServerCredential { +public interface LwM2MBootstrapServerCredential extends Serializable { @JsonIgnore LwM2MSecurityMode getSecurityMode(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/NoSecLwM2MBootstrapServerCredential.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/NoSecLwM2MBootstrapServerCredential.java index 2c0e615cfb..301d033ab9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/NoSecLwM2MBootstrapServerCredential.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/NoSecLwM2MBootstrapServerCredential.java @@ -18,6 +18,9 @@ package org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode; public class NoSecLwM2MBootstrapServerCredential extends AbstractLwM2MBootstrapServerCredential { + + private static final long serialVersionUID = 5540417758424747066L; + @Override public LwM2MSecurityMode getSecurityMode() { return LwM2MSecurityMode.NO_SEC; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/PSKLwM2MBootstrapServerCredential.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/PSKLwM2MBootstrapServerCredential.java index 08901459d2..2fd73af720 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/PSKLwM2MBootstrapServerCredential.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/PSKLwM2MBootstrapServerCredential.java @@ -18,6 +18,9 @@ package org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode; public class PSKLwM2MBootstrapServerCredential extends AbstractLwM2MBootstrapServerCredential { + + private static final long serialVersionUID = -1639587501559199887L; + @Override public LwM2MSecurityMode getSecurityMode() { return LwM2MSecurityMode.PSK; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/RPKLwM2MBootstrapServerCredential.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/RPKLwM2MBootstrapServerCredential.java index 477973cf6b..99ecf4dcff 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/RPKLwM2MBootstrapServerCredential.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/RPKLwM2MBootstrapServerCredential.java @@ -18,6 +18,9 @@ package org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode; public class RPKLwM2MBootstrapServerCredential extends AbstractLwM2MBootstrapServerCredential { + + private static final long serialVersionUID = 6692464656059120166L; + @Override public LwM2MSecurityMode getSecurityMode() { return LwM2MSecurityMode.RPK; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/X509LwM2MBootstrapServerCredential.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/X509LwM2MBootstrapServerCredential.java index 3f4e0f0aa8..a229d5fceb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/X509LwM2MBootstrapServerCredential.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/bootstrap/X509LwM2MBootstrapServerCredential.java @@ -18,6 +18,9 @@ package org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MSecurityMode; public class X509LwM2MBootstrapServerCredential extends AbstractLwM2MBootstrapServerCredential { + + private static final long serialVersionUID = -3740860424558547405L; + @Override public LwM2MSecurityMode getSecurityMode() { return LwM2MSecurityMode.X509; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/AggTsKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/AggTsKvEntry.java new file mode 100644 index 0000000000..85330d3577 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/AggTsKvEntry.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2022 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.kv; + +import lombok.ToString; +import org.thingsboard.server.common.data.query.TsValue; + +@ToString +public class AggTsKvEntry extends BasicTsKvEntry { + + private static final long serialVersionUID = -1933884317450255935L; + + private final long count; + + public AggTsKvEntry(long ts, KvEntry kv, long count) { + super(ts, kv); + this.count = count; + } + + @Override + public TsValue toTsValue() { + return new TsValue(ts, getValueAsString(), count); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseReadTsKvQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseReadTsKvQuery.java index fc111da8e2..29b98d5e83 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseReadTsKvQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseReadTsKvQuery.java @@ -31,8 +31,7 @@ public class BaseReadTsKvQuery extends BaseTsKvQuery implements ReadTsKvQuery { this(key, startTs, endTs, interval, limit, aggregation, "DESC"); } - public BaseReadTsKvQuery(String key, long startTs, long endTs, long interval, int limit, Aggregation aggregation, - String order) { + public BaseReadTsKvQuery(String key, long startTs, long endTs, long interval, int limit, Aggregation aggregation, String order) { super(key, startTs, endTs); this.interval = interval; this.limit = limit; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseTsKvQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseTsKvQuery.java index 4102b30a6f..245af53d55 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseTsKvQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseTsKvQuery.java @@ -20,11 +20,16 @@ import lombok.Data; @Data public class BaseTsKvQuery implements TsKvQuery { + private static final ThreadLocal idSeq = ThreadLocal.withInitial(() -> 0); + + private final int id; private final String key; private final long startTs; private final long endTs; public BaseTsKvQuery(String key, long startTs, long endTs) { + this.id = idSeq.get(); + idSeq.set(id + 1); this.key = key; this.startTs = startTs; this.endTs = endTs; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java index 6fc32e627b..e5a7ac65e0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java @@ -20,7 +20,7 @@ import java.util.Optional; public class BasicTsKvEntry implements TsKvEntry { private static final int MAX_CHARS_PER_DATA_POINT = 512; - private final long ts; + protected final long ts; private final KvEntry kv; public BasicTsKvEntry(long ts, KvEntry kv) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/ReadTsKvQueryResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/ReadTsKvQueryResult.java new file mode 100644 index 0000000000..9c7a2f1754 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/ReadTsKvQueryResult.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2022 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.kv; + +import lombok.Data; +import org.thingsboard.server.common.data.query.TsValue; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class ReadTsKvQueryResult { + + private final int queryId; + // Holds the data list; + private final List data; + // Holds the max ts of the records that match aggregation intervals (not the ts of the aggregation window, but the ts of the last record among all the intervals) + private final long lastEntryTs; + + public TsValue[] toTsValues() { + if (data != null && !data.isEmpty()) { + List queryValues = new ArrayList<>(); + for (TsKvEntry v : data) { + queryValues.add(v.toTsValue()); // TODO: add count here. + } + return queryValues.toArray(new TsValue[queryValues.size()]); + } else { + return new TsValue[0]; + } + } + + public TsValue toTsValue() { + if (data == null || data.isEmpty()) { + return TsValue.EMPTY; + } + if (data.size() > 1) { + throw new RuntimeException("Query Result has multiple data points!"); + } + return data.get(0).toTsValue(); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java index 20d25d5399..06ac0d84a3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java @@ -16,10 +16,11 @@ package org.thingsboard.server.common.data.kv; import com.fasterxml.jackson.annotation.JsonIgnore; +import org.thingsboard.server.common.data.query.TsValue; /** * Represents time series KV data entry - * + * * @author ashvayka * */ @@ -30,4 +31,9 @@ public interface TsKvEntry extends KvEntry { @JsonIgnore int getDataPoints(); + @JsonIgnore + default TsValue toTsValue() { + return new TsValue(getTs(), getValueAsString()); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntryAggWrapper.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntryAggWrapper.java new file mode 100644 index 0000000000..2825008485 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntryAggWrapper.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2022 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.kv; + +import lombok.Data; + +@Data +public class TsKvEntryAggWrapper { + + private final TsKvEntry entry; + private final long lastEntryTs; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvQuery.java index cf01c322a5..78bf01c0e9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvQuery.java @@ -17,6 +17,8 @@ package org.thingsboard.server.common.data.kv; public interface TsKvQuery { + int getId(); + String getKey(); long getStartTs(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/ComparisonTsValue.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/ComparisonTsValue.java new file mode 100644 index 0000000000..301fbdb0ee --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/ComparisonTsValue.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2022 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.query; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ComparisonTsValue { + + private TsValue current; + private TsValue previous; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityData.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityData.java index c0cc778280..2b24866ac4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityData.java @@ -16,15 +16,21 @@ package org.thingsboard.server.common.data.query; import lombok.Data; +import lombok.RequiredArgsConstructor; import org.thingsboard.server.common.data.id.EntityId; import java.util.Map; @Data +@RequiredArgsConstructor public class EntityData { private final EntityId entityId; private final Map> latest; private final Map timeseries; + private final Map aggLatest; + public EntityData(EntityId entityId, Map> latest, Map timeseries) { + this(entityId, latest, timeseries, null); + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKey.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKey.java index 5b4b5cd257..6aacc90060 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKey.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/EntityKey.java @@ -23,6 +23,8 @@ import java.io.Serializable; @ApiModel @Data public class EntityKey implements Serializable { + private static final long serialVersionUID = -6421575477523085543L; + private final EntityKeyType type; private final String key; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/TsValue.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/TsValue.java index a086f1c21f..9dbfdfbb28 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/TsValue.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/TsValue.java @@ -15,11 +15,23 @@ */ package org.thingsboard.server.common.data.query; +import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Data; +import lombok.RequiredArgsConstructor; @Data +@RequiredArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) public class TsValue { + public static final TsValue EMPTY = new TsValue(0, ""); + private final long ts; private final String value; + private final Long count; + + public TsValue(long ts, String value) { + this(ts, value, null); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityLoadError.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityLoadError.java index cbfe9f6457..1f54a7c262 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityLoadError.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityLoadError.java @@ -19,7 +19,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Builder; import lombok.Data; import org.apache.commons.lang3.ClassUtils; -import org.apache.commons.lang3.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.EntityId; import java.io.Serializable; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpMapping.java b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpMapping.java index 44fe42512b..c184b70ed4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpMapping.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/SnmpMapping.java @@ -19,15 +19,19 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -import org.apache.commons.lang3.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.kv.DataType; +import java.io.Serializable; import java.util.regex.Pattern; @Data @AllArgsConstructor @NoArgsConstructor -public class SnmpMapping { +public class SnmpMapping implements Serializable { + + private static final long serialVersionUID = 2042438869374145944L; + private String oid; private String key; private DataType dataType; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/SnmpCommunicationConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/SnmpCommunicationConfig.java index 22678620cf..6bb8c66bf2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/SnmpCommunicationConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/SnmpCommunicationConfig.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.transport.snmp.config.impl.SharedAttri import org.thingsboard.server.common.data.transport.snmp.config.impl.TelemetryQueryingSnmpCommunicationConfig; import org.thingsboard.server.common.data.transport.snmp.config.impl.ToDeviceRpcRequestSnmpCommunicationConfig; +import java.io.Serializable; import java.util.List; @JsonIgnoreProperties(ignoreUnknown = true) @@ -38,7 +39,7 @@ import java.util.List; @Type(value = SharedAttributesSettingSnmpCommunicationConfig.class, name = "SHARED_ATTRIBUTES_SETTING"), @Type(value = ToDeviceRpcRequestSnmpCommunicationConfig.class, name = "TO_DEVICE_RPC_REQUEST") }) -public interface SnmpCommunicationConfig { +public interface SnmpCommunicationConfig extends Serializable { SnmpCommunicationSpec getSpec(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/impl/ClientAttributesQueryingSnmpCommunicationConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/impl/ClientAttributesQueryingSnmpCommunicationConfig.java index 2eec1e336c..2601e9accc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/impl/ClientAttributesQueryingSnmpCommunicationConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/impl/ClientAttributesQueryingSnmpCommunicationConfig.java @@ -20,6 +20,8 @@ import org.thingsboard.server.common.data.transport.snmp.config.RepeatingQueryin public class ClientAttributesQueryingSnmpCommunicationConfig extends RepeatingQueryingSnmpCommunicationConfig { + private static final long serialVersionUID = 536740834893462914L; + @Override public SnmpCommunicationSpec getSpec() { return SnmpCommunicationSpec.CLIENT_ATTRIBUTES_QUERYING; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/impl/SharedAttributesSettingSnmpCommunicationConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/impl/SharedAttributesSettingSnmpCommunicationConfig.java index c034b6c26e..b80aaf3583 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/impl/SharedAttributesSettingSnmpCommunicationConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/impl/SharedAttributesSettingSnmpCommunicationConfig.java @@ -21,6 +21,8 @@ import org.thingsboard.server.common.data.transport.snmp.config.MultipleMappings public class SharedAttributesSettingSnmpCommunicationConfig extends MultipleMappingsSnmpCommunicationConfig { + private static final long serialVersionUID = 8981224974190924703L; + @Override public SnmpCommunicationSpec getSpec() { return SnmpCommunicationSpec.SHARED_ATTRIBUTES_SETTING; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/impl/TelemetryQueryingSnmpCommunicationConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/impl/TelemetryQueryingSnmpCommunicationConfig.java index d4b16e9e0b..4b59dbd5e4 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/impl/TelemetryQueryingSnmpCommunicationConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/impl/TelemetryQueryingSnmpCommunicationConfig.java @@ -24,6 +24,8 @@ import org.thingsboard.server.common.data.transport.snmp.config.RepeatingQueryin @Data public class TelemetryQueryingSnmpCommunicationConfig extends RepeatingQueryingSnmpCommunicationConfig { + private static final long serialVersionUID = -1367743866881596885L; + @Override public SnmpCommunicationSpec getSpec() { return SnmpCommunicationSpec.TELEMETRY_QUERYING; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/impl/ToDeviceRpcRequestSnmpCommunicationConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/impl/ToDeviceRpcRequestSnmpCommunicationConfig.java index 2a587f4243..c86c330cae 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/impl/ToDeviceRpcRequestSnmpCommunicationConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/transport/snmp/config/impl/ToDeviceRpcRequestSnmpCommunicationConfig.java @@ -19,6 +19,9 @@ import org.thingsboard.server.common.data.transport.snmp.SnmpCommunicationSpec; import org.thingsboard.server.common.data.transport.snmp.config.MultipleMappingsSnmpCommunicationConfig; public class ToDeviceRpcRequestSnmpCommunicationConfig extends MultipleMappingsSnmpCommunicationConfig { + + private static final long serialVersionUID = -8607312598073845460L; + @Override public SnmpCommunicationSpec getSpec() { return SnmpCommunicationSpec.TO_DEVICE_RPC_REQUEST; diff --git a/common/edge-api/pom.xml b/common/edge-api/pom.xml index aa4a095c99..576f2aeced 100644 --- a/common/edge-api/pom.xml +++ b/common/edge-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT common org.thingsboard.common diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index f7ecbfcb8d..7272e6f96a 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -200,6 +200,7 @@ message DeviceUpdateMsg { optional string conflictName = 12; optional int64 firmwareIdMSB = 13; optional int64 firmwareIdLSB = 14; + optional bytes deviceDataBytes = 15; } message DeviceProfileUpdateMsg { diff --git a/common/message/pom.xml b/common/message/pom.xml index ab4d73f3ee..de18be2e21 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT common org.thingsboard.common diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java index f8eb8c663e..55b9f4dbd5 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java @@ -22,7 +22,7 @@ import lombok.AccessLevel; import lombok.Data; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; diff --git a/common/pom.xml b/common/pom.xml index 33abef1062..770cb402d2 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT thingsboard common diff --git a/common/queue/pom.xml b/common/queue/pom.xml index e116645618..c246ab358f 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT common org.thingsboard.common diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java index a3744c3946..0a4a7e6acd 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java @@ -21,7 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbTransportService; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.gen.transport.TransportProtos.ServiceInfo; @@ -60,7 +60,7 @@ public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider { try { serviceId = InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException e) { - serviceId = org.apache.commons.lang3.RandomStringUtils.randomAlphabetic(10); + serviceId = StringUtils.randomAlphabetic(10); } } log.info("Current Service ID: {}", serviceId); diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java index e62ee850fa..e3177ab0b6 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaConsumerStatsService.java @@ -29,8 +29,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.queue.discovery.PartitionService; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplate.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplate.java index d4946581e6..29aadcafd6 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplate.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaProducerTemplate.java @@ -23,7 +23,7 @@ import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.clients.producer.ProducerRecord; import org.apache.kafka.common.header.Header; import org.apache.kafka.common.header.internals.RecordHeader; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueCallback; diff --git a/common/stats/pom.xml b/common/stats/pom.xml index 7648eb911a..d316980e97 100644 --- a/common/stats/pom.xml +++ b/common/stats/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT common org.thingsboard.common @@ -86,6 +86,10 @@ awaitility test + + org.thingsboard.common + data + diff --git a/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java b/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java index e71ac22d00..dff0212e99 100644 --- a/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java +++ b/common/stats/src/main/java/org/thingsboard/server/common/stats/DefaultStatsFactory.java @@ -19,7 +19,7 @@ import io.micrometer.core.instrument.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import javax.annotation.PostConstruct; import java.util.concurrent.atomic.AtomicInteger; diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index 6248f11a6c..70b2470dc4 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapAdaptorUtils.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapAdaptorUtils.java index a23ca82234..1043cecc41 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapAdaptorUtils.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapAdaptorUtils.java @@ -16,7 +16,7 @@ package org.thingsboard.server.transport.coap.adaptors; import org.eclipse.californium.core.coap.Request; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.gen.transport.TransportProtos; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java index 3d4a4ee053..3568624289 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java @@ -27,7 +27,7 @@ import org.eclipse.californium.core.coap.MediaTypeRegistry; import org.eclipse.californium.core.coap.Request; import org.eclipse.californium.core.coap.Response; import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.adaptor.JsonConverter; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java index 1bd1ffb827..13c783675e 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/ProtoCoapAdaptor.java @@ -26,7 +26,7 @@ import org.eclipse.californium.core.coap.MediaTypeRegistry; import org.eclipse.californium.core.coap.Request; import org.eclipse.californium.core.coap.Response; import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.adaptor.JsonConverter; diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java index 1b3c936130..f8336025d4 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DefaultCoapClientContext.java @@ -23,6 +23,7 @@ import org.eclipse.californium.core.observe.ObserveRelation; import org.eclipse.californium.core.server.resources.CoapExchange; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.context.annotation.Lazy; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.thingsboard.server.coapserver.CoapServerContext; import org.thingsboard.server.common.data.DataConstants; @@ -45,6 +46,8 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.rpc.RpcStatus; import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.common.msg.session.SessionMsgType; +import org.thingsboard.server.common.transport.DeviceDeletedEvent; +import org.thingsboard.server.common.transport.DeviceUpdatedEvent; import org.thingsboard.server.common.transport.SessionMsgListener; import org.thingsboard.server.common.transport.TransportDeviceProfileCache; import org.thingsboard.server.common.transport.TransportService; @@ -52,6 +55,7 @@ import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.adaptor.AdaptorException; import org.thingsboard.server.common.transport.auth.SessionInfoCreator; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; +import org.thingsboard.server.common.transport.DeviceProfileUpdatedEvent; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.transport.coap.CoapTransportContext; @@ -97,6 +101,45 @@ public class DefaultCoapClientContext implements CoapClientContext { this.partitionService = partitionService; } + @EventListener(DeviceProfileUpdatedEvent.class) + public void onApplicationEvent(DeviceProfileUpdatedEvent event) { + var deviceProfile = event.getDeviceProfile(); + clients.values().stream().filter(state -> state.getSession() == null).forEach(state -> { + state.lock(); + try { + if (deviceProfile.getId().equals(state.getProfileId())) { + initStateAdaptor(deviceProfile, state); + } + } catch (AdaptorException e) { + log.trace("[{}] Failed to update client state due to: ", state.getDeviceId(), e); + } finally { + state.unlock(); + } + }); + } + + @EventListener(DeviceUpdatedEvent.class) + public void onApplicationEvent(DeviceUpdatedEvent event) { + var device = event.getDevice(); + var state = clients.get(device.getId()); + if (state == null) { + return; + } + state.lock(); + try { + if (state.getSession() == null) { + clients.remove(device.getId()); + } + } finally { + state.unlock(); + } + } + + @EventListener(DeviceDeletedEvent.class) + public void onApplicationEvent(DeviceDeletedEvent event) { + clients.remove(event.getDeviceId()); + } + @Override public boolean registerAttributeObservation(TbCoapClientState clientState, String token, CoapExchange exchange) { return registerFeatureObservation(clientState, token, exchange, FeatureType.ATTRIBUTES); diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index a55dea2f2b..c0bb5d9001 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java index de9619cb9f..17bc257ccb 100644 --- a/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java +++ b/common/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java @@ -28,7 +28,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index 29e74a7931..d9f92d685d 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java index 63221f36ff..966404cce2 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/bootstrap/secure/LwM2MBootstrapConfig.java @@ -37,6 +37,8 @@ import java.util.List; @Data public class LwM2MBootstrapConfig implements Serializable { + private static final long serialVersionUID = -4729088085817468640L; + List serverConfiguration; /** -bootstrapServer, lwm2mServer diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java index ca001e3b83..43c40280a8 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java @@ -18,7 +18,7 @@ package org.thingsboard.server.transport.lwm2m.utils; import com.fasterxml.jackson.core.type.TypeReference; import com.google.gson.JsonElement; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.eclipse.leshan.core.attributes.Attribute; import org.eclipse.leshan.core.attributes.AttributeSet; import org.eclipse.leshan.core.model.LwM2mModel; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java index c4057be1fe..cd2bb87fc4 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2mValueConverterImpl.java @@ -23,7 +23,7 @@ import org.eclipse.leshan.core.node.ObjectLink; import org.eclipse.leshan.core.node.codec.CodecException; import org.eclipse.leshan.core.node.codec.LwM2mValueConverter; import org.eclipse.leshan.core.util.Hex; -import org.eclipse.leshan.core.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import java.math.BigInteger; import java.text.DateFormat; diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index 2542e5914e..9906b39ba7 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java index 8408c76a16..98aaca7f06 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java @@ -24,7 +24,7 @@ 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.StringUtils; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.common.transport.TransportService; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index f37715efc8..ad07c9f9a4 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -41,7 +41,7 @@ import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java index 75c76c0cdb..b8e120f301 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java @@ -27,7 +27,7 @@ import io.netty.handler.codec.mqtt.MqttPublishMessage; import io.netty.handler.codec.mqtt.MqttPublishVariableHeader; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.transport.adaptor.AdaptorException; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java index 175bf17c02..17ae0d75b5 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java @@ -25,7 +25,7 @@ import io.netty.handler.codec.mqtt.MqttMessage; import io.netty.handler.codec.mqtt.MqttPublishMessage; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.device.profile.MqttTopics; import org.thingsboard.server.common.data.ota.OtaPackageType; import org.thingsboard.server.common.transport.adaptor.AdaptorException; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java index d91935086b..fad67470ba 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java @@ -35,7 +35,7 @@ import io.netty.handler.codec.mqtt.MqttPublishMessage; import lombok.extern.slf4j.Slf4j; import org.springframework.util.CollectionUtils; import org.springframework.util.ConcurrentReferenceHashMap; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportServiceCallback; diff --git a/common/transport/pom.xml b/common/transport/pom.xml index be0da470c3..125139c523 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/snmp/pom.xml b/common/transport/snmp/pom.xml index 7bcf4dc137..c9611333f8 100644 --- a/common/transport/snmp/pom.xml +++ b/common/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.common - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT transport diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index 308768231e..932629bb12 100644 --- a/common/transport/transport-api/pom.xml +++ b/common/transport/transport-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/DeviceDeletedEvent.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/DeviceDeletedEvent.java new file mode 100644 index 0000000000..9a7cdbefdd --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/DeviceDeletedEvent.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2022 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; + +import lombok.Getter; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.queue.discovery.event.TbApplicationEvent; + +public final class DeviceDeletedEvent extends TbApplicationEvent { + + private static final long serialVersionUID = -7453664970966733857L; + @Getter + private final DeviceId deviceId; + + public DeviceDeletedEvent(DeviceId deviceId) { + super(new Object()); + this.deviceId = deviceId; + } +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/DeviceProfileUpdatedEvent.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/DeviceProfileUpdatedEvent.java new file mode 100644 index 0000000000..ca95a636d3 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/DeviceProfileUpdatedEvent.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2022 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; + +import lombok.Getter; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.queue.discovery.event.TbApplicationEvent; + +public final class DeviceProfileUpdatedEvent extends TbApplicationEvent { + + @Getter + private final DeviceProfile deviceProfile; + + public DeviceProfileUpdatedEvent(DeviceProfile deviceProfile) { + super(new Object()); + this.deviceProfile = deviceProfile; + } +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java index 636b9554a5..3ba33d58da 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java @@ -23,7 +23,7 @@ import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import com.google.gson.JsonSyntaxException; import org.apache.commons.lang3.math.NumberUtils; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java index 137ceff337..6fe0f29cfc 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/adaptor/ProtoConverter.java @@ -25,7 +25,7 @@ import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.util.JsonFormat; import lombok.extern.slf4j.Slf4j; import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.gen.transport.TransportApiProtos; diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java index e3170797bd..92084db5af 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultTransportRateLimitService.java @@ -18,7 +18,7 @@ package org.thingsboard.server.common.transport.limits; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.DeviceId; diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index a63e4a01a2..8b532ae92e 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -44,7 +44,6 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; @@ -58,6 +57,8 @@ import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import org.thingsboard.server.common.stats.MessagesStats; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.stats.StatsType; +import org.thingsboard.server.common.transport.DeviceDeletedEvent; +import org.thingsboard.server.common.transport.DeviceProfileUpdatedEvent; import org.thingsboard.server.common.transport.DeviceUpdatedEvent; import org.thingsboard.server.common.transport.SessionMsgListener; import org.thingsboard.server.common.transport.TransportDeviceProfileCache; @@ -925,10 +926,7 @@ public class DefaultTransportService implements TransportService { } } else if (EntityType.DEVICE.equals(entityType)) { Optional deviceOpt = dataDecodingEncodingService.decode(msg.getData().toByteArray()); - deviceOpt.ifPresent(device -> { - onDeviceUpdate(device); - eventPublisher.publishEvent(new DeviceUpdatedEvent(device)); - }); + deviceOpt.ifPresent(this::onDeviceUpdate); } } else if (toSessionMsg.hasEntityDeleteMsg()) { TransportProtos.EntityDeleteMsg msg = toSessionMsg.getEntityDeleteMsg(); @@ -994,6 +992,8 @@ public class DefaultTransportService implements TransportService { transportCallbackExecutor.submit(() -> md.getListener().onDeviceProfileUpdate(newSessionInfo, deviceProfile)); } }); + + eventPublisher.publishEvent(new DeviceProfileUpdatedEvent(deviceProfile)); } private void onDeviceUpdate(Device device) { @@ -1027,6 +1027,8 @@ public class DefaultTransportService implements TransportService { transportCallbackExecutor.submit(() -> md.getListener().onDeviceUpdate(newSessionInfo, device, Optional.ofNullable(newDeviceProfile))); } }); + + eventPublisher.publishEvent(new DeviceUpdatedEvent(device)); } private void onDeviceDeleted(DeviceId deviceId) { @@ -1038,6 +1040,8 @@ public class DefaultTransportService implements TransportService { }); } }); + + eventPublisher.publishEvent(new DeviceDeletedEvent(deviceId)); } protected UUID toSessionId(TransportProtos.SessionInfoProto sessionInfo) { diff --git a/common/util/pom.xml b/common/util/pom.xml index cd45ff96d7..5717362ff0 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT common org.thingsboard.common diff --git a/common/version-control/pom.xml b/common/version-control/pom.xml index ac472c8e3c..7929a49a3b 100644 --- a/common/version-control/pom.xml +++ b/common/version-control/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT common org.thingsboard.common diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java index d3615f7228..64f4e90cb8 100644 --- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java +++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/DefaultGitRepositoryService.java @@ -18,7 +18,7 @@ package org.thingsboard.server.service.sync.vc; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; -import org.apache.commons.lang3.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.eclipse.jgit.api.errors.GitAPIException; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; diff --git a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java index 7d1710a87b..d820659e77 100644 --- a/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java +++ b/common/version-control/src/main/java/org/thingsboard/server/service/sync/vc/GitRepository.java @@ -20,7 +20,7 @@ import com.google.common.collect.Ordering; import com.google.common.collect.Streams; import lombok.Data; import lombok.Getter; -import org.apache.commons.lang3.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.apache.sshd.common.util.security.SecurityUtils; import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.Git; diff --git a/dao/pom.xml b/dao/pom.xml index ae98928787..f911a7e043 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT thingsboard dao diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index 714e131025..f83a66fce9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -24,6 +24,7 @@ import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.alarm.Alarm; @@ -142,6 +143,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } @Override + @Transactional public AlarmOperationResult deleteAlarm(TenantId tenantId, AlarmId alarmId) { try { log.debug("Deleting Alarm Id: {}", alarmId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index 31c40e0d96..3ffcc999c3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -20,10 +20,9 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; -import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.util.Pair; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; @@ -150,6 +149,7 @@ public class BaseAssetService extends AbstractCachedEntityService { @Transient protected String strKey; + @Transient + protected Long aggValuesLastTs; + @Transient + protected Long aggValuesCount; + + public AbstractTsKvEntity() { + } + + public AbstractTsKvEntity(Long aggValuesLastTs) { + this.aggValuesLastTs = aggValuesLastTs; + } + public abstract boolean isNotEmpty(); protected static boolean isAllNull(Object... args) { @@ -105,7 +118,12 @@ public abstract class AbstractTsKvEntity implements ToData { } else if (jsonValue != null) { kvEntry = new JsonDataEntry(strKey, jsonValue); } - return new BasicTsKvEntry(ts, kvEntry); + + if (aggValuesCount == null) { + return new BasicTsKvEntry(ts, kvEntry); + } else { + return new AggTsKvEntry(ts, kvEntry, aggValuesCount); + } } } \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java index ccaea5524e..1e39760a12 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardEntity.java @@ -24,9 +24,9 @@ import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; -import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.ShortCustomerInfo; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseSqlEntity; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java index de5dd6a22c..d7a7e6e055 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/DashboardInfoEntity.java @@ -21,9 +21,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.ShortCustomerInfo; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.model.BaseSqlEntity; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2RegistrationEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2RegistrationEntity.java index 68b5baa9bf..8aed530950 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2RegistrationEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2RegistrationEntity.java @@ -16,11 +16,11 @@ package org.thingsboard.server.dao.model.sql; import com.fasterxml.jackson.databind.JsonNode; -import io.micrometer.core.instrument.util.StringUtils; import lombok.Data; import lombok.EqualsAndHashCode; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.OAuth2ParamsId; import org.thingsboard.server.common.data.id.OAuth2RegistrationId; import org.thingsboard.server.common.data.oauth2.MapperType; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/timescale/ts/TimescaleTsKvEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/timescale/ts/TimescaleTsKvEntity.java index 05c264a48b..ba5ad92f4f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/timescale/ts/TimescaleTsKvEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/timescale/ts/TimescaleTsKvEntity.java @@ -17,7 +17,7 @@ package org.thingsboard.server.dao.model.sqlts.timescale.ts; import lombok.Data; import lombok.EqualsAndHashCode; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; import javax.persistence.ColumnResult; @@ -62,6 +62,7 @@ import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.F @ColumnResult(name = "doubleCountValue", type = Long.class), @ColumnResult(name = "strValue", type = String.class), @ColumnResult(name = "aggType", type = String.class), + @ColumnResult(name = "maxAggTs", type = Long.class), } ), }), @@ -78,6 +79,7 @@ import static org.thingsboard.server.dao.sqlts.timescale.AggregationRepository.F @ColumnResult(name = "longValueCount", type = Long.class), @ColumnResult(name = "doubleValueCount", type = Long.class), @ColumnResult(name = "jsonValueCount", type = Long.class), + @ColumnResult(name = "maxAggTs", type = Long.class), } ) }), @@ -114,7 +116,8 @@ public final class TimescaleTsKvEntity extends AbstractTsKvEntity { public TimescaleTsKvEntity() { } - public TimescaleTsKvEntity(Long tsBucket, Long interval, Long longValue, Double doubleValue, Long longCountValue, Long doubleCountValue, String strValue, String aggType) { + public TimescaleTsKvEntity(Long tsBucket, Long interval, Long longValue, Double doubleValue, Long longCountValue, Long doubleCountValue, String strValue, String aggType, Long aggValuesLastTs) { + super(aggValuesLastTs); if (!StringUtils.isEmpty(strValue)) { this.strValue = strValue; } @@ -135,6 +138,7 @@ public final class TimescaleTsKvEntity extends AbstractTsKvEntity { } else { this.doubleValue = 0.0; } + this.aggValuesCount = totalCount; break; case SUM: if (doubleCountValue > 0) { @@ -157,7 +161,8 @@ public final class TimescaleTsKvEntity extends AbstractTsKvEntity { } } - public TimescaleTsKvEntity(Long tsBucket, Long interval, Long booleanValueCount, Long strValueCount, Long longValueCount, Long doubleValueCount, Long jsonValueCount) { + public TimescaleTsKvEntity(Long tsBucket, Long interval, Long booleanValueCount, Long strValueCount, Long longValueCount, Long doubleValueCount, Long jsonValueCount, Long aggValuesLastTs) { + super(aggValuesLastTs); if (!isAllNull(tsBucket, interval, booleanValueCount, strValueCount, longValueCount, doubleValueCount, jsonValueCount)) { this.ts = tsBucket + interval / 2; if (booleanValueCount != 0) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvEntity.java index 1c0277f8b6..15fd30c742 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sqlts/ts/TsKvEntity.java @@ -16,12 +16,15 @@ package org.thingsboard.server.dao.model.sqlts.ts; import lombok.Data; +import lombok.EqualsAndHashCode; import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; import javax.persistence.Entity; import javax.persistence.IdClass; import javax.persistence.Table; +import javax.persistence.Transient; +@EqualsAndHashCode(callSuper = true) @Data @Entity @Table(name = "ts_kv") @@ -31,11 +34,13 @@ public final class TsKvEntity extends AbstractTsKvEntity { public TsKvEntity() { } - public TsKvEntity(String strValue) { + public TsKvEntity(String strValue, Long aggValuesLastTs) { + super(aggValuesLastTs); this.strValue = strValue; } - public TsKvEntity(Long longValue, Double doubleValue, Long longCountValue, Long doubleCountValue, String aggType) { + public TsKvEntity(Long longValue, Double doubleValue, Long longCountValue, Long doubleCountValue, String aggType, Long aggValuesLastTs) { + super(aggValuesLastTs); if (!isAllNull(longValue, doubleValue, longCountValue, doubleCountValue)) { switch (aggType) { case AVG: @@ -52,6 +57,7 @@ public final class TsKvEntity extends AbstractTsKvEntity { } else { this.doubleValue = 0.0; } + this.aggValuesCount = totalCount; break; case SUM: if (doubleCountValue > 0) { @@ -74,7 +80,8 @@ public final class TsKvEntity extends AbstractTsKvEntity { } } - public TsKvEntity(Long booleanValueCount, Long strValueCount, Long longValueCount, Long doubleValueCount, Long jsonValueCount) { + public TsKvEntity(Long booleanValueCount, Long strValueCount, Long longValueCount, Long doubleValueCount, Long jsonValueCount, Long aggValuesLastTs) { + super(aggValuesLastTs); if (!isAllNull(booleanValueCount, strValueCount, longValueCount, doubleValueCount)) { if (booleanValueCount != 0) { this.longValue = booleanValueCount; diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java index fd4b29817b..b1f5d4e397 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java @@ -19,8 +19,8 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.oauth2.MapperType; import org.thingsboard.server.common.data.oauth2.OAuth2BasicMapperConfig; diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index f60920a31d..6f6a942162 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -28,8 +28,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; import org.springframework.transaction.support.TransactionSynchronizationManager; -import org.springframework.util.StringUtils; import org.thingsboard.server.cache.TbTransactionalCache; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index b511ab3da2..420739e37c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -131,7 +131,6 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } @Override - @Transactional public RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData) { Validator.validateId(ruleChainMetaData.getRuleChainId(), "Incorrect rule chain id."); RuleChain ruleChain = findRuleChainById(tenantId, ruleChainMetaData.getRuleChainId()); @@ -748,6 +747,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } @Override + @Transactional public void deleteRuleNodes(TenantId tenantId, RuleChainId ruleChainId) { List nodeRelations = getRuleChainToNodeRelations(tenantId, ruleChainId); for (EntityRelation relation : nodeRelations) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java index 81297b3594..9bf729ad25 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java @@ -17,9 +17,9 @@ package org.thingsboard.server.dao.service; import com.fasterxml.jackson.databind.JsonNode; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.TenantEntityDao; import org.thingsboard.server.dao.TenantEntityWithDataDao; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AdminSettingsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AdminSettingsDataValidator.java index 8033688787..82cb8084fd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AdminSettingsDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AdminSettingsDataValidator.java @@ -16,9 +16,9 @@ package org.thingsboard.server.dao.service.validator; import lombok.AllArgsConstructor; -import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.AdminSettings; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmDataValidator.java index a95f7a43aa..f9097cfcc0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmDataValidator.java @@ -17,7 +17,7 @@ package org.thingsboard.server.dao.service.validator; import lombok.AllArgsConstructor; import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.exception.DataValidationException; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java index f61cf69845..b7685a0476 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetDataValidator.java @@ -18,9 +18,9 @@ package org.thingsboard.server.dao.service.validator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ClientRegistrationTemplateDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ClientRegistrationTemplateDataValidator.java index b5d0247e28..2023ec353f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ClientRegistrationTemplateDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ClientRegistrationTemplateDataValidator.java @@ -16,7 +16,7 @@ package org.thingsboard.server.dao.service.validator; import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; import org.thingsboard.server.dao.exception.DataValidationException; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ComponentDescriptorDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ComponentDescriptorDataValidator.java index f7eceec4d5..0d2caed6d4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ComponentDescriptorDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ComponentDescriptorDataValidator.java @@ -15,8 +15,8 @@ */ package org.thingsboard.server.dao.service.validator; -import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.dao.exception.DataValidationException; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CustomerDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CustomerDataValidator.java index 9cc8d6a0de..83ff93e079 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CustomerDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CustomerDataValidator.java @@ -15,12 +15,12 @@ */ package org.thingsboard.server.dao.service.validator; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.customer.CustomerDao; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java index 23b960253c..a3a4ef50fd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java @@ -15,12 +15,12 @@ */ package org.thingsboard.server.dao.service.validator; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.dashboard.DashboardDao; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java index d30180a892..a9d8e27f80 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceDataValidator.java @@ -18,10 +18,10 @@ package org.thingsboard.server.dao.service.validator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.device.data.DeviceTransportConfiguration; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/EdgeDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/EdgeDataValidator.java index b88724f7af..3f0d3659ce 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/EdgeDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/EdgeDataValidator.java @@ -17,8 +17,8 @@ package org.thingsboard.server.dao.service.validator; import lombok.AllArgsConstructor; import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; @@ -49,13 +49,13 @@ public class EdgeDataValidator extends DataValidator { @Override protected void validateDataImpl(TenantId tenantId, Edge edge) { - if (org.springframework.util.StringUtils.isEmpty(edge.getType())) { + if (StringUtils.isEmpty(edge.getType())) { throw new DataValidationException("Edge type should be specified!"); } - if (org.springframework.util.StringUtils.isEmpty(edge.getName())) { + if (StringUtils.isEmpty(edge.getName())) { throw new DataValidationException("Edge name should be specified!"); } - if (org.springframework.util.StringUtils.isEmpty(edge.getSecret())) { + if (StringUtils.isEmpty(edge.getSecret())) { throw new DataValidationException("Edge secret should be specified!"); } if (StringUtils.isEmpty(edge.getRoutingKey())) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/EntityViewDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/EntityViewDataValidator.java index 7532dda195..e8220b71ba 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/EntityViewDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/EntityViewDataValidator.java @@ -16,10 +16,10 @@ package org.thingsboard.server.dao.service.validator; import lombok.AllArgsConstructor; -import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityView; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.customer.CustomerDao; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/EventDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/EventDataValidator.java index 8ad541ebac..93a0e6416e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/EventDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/EventDataValidator.java @@ -15,8 +15,8 @@ */ package org.thingsboard.server.dao.service.validator; -import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.event.Event; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.exception.DataValidationException; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java index 49de43dba3..03f1ad59e1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ResourceDataValidator.java @@ -15,10 +15,10 @@ */ package org.thingsboard.server.dao.service.validator; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java index 1581a61b80..e6ae7d04a8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java @@ -15,11 +15,11 @@ */ package org.thingsboard.server.dao.service.validator; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainType; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantDataValidator.java index 65770aefdb..cea8bc9fb7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantDataValidator.java @@ -15,9 +15,9 @@ */ package org.thingsboard.server.dao.service.validator; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.exception.DataValidationException; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java index 8484ecfa59..0368e38173 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java @@ -15,10 +15,10 @@ */ package org.thingsboard.server.dao.service.validator; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.queue.ProcessingStrategy; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserCredentialsDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserCredentialsDataValidator.java index 5920ad552f..b15f0d2daf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserCredentialsDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserCredentialsDataValidator.java @@ -15,10 +15,10 @@ */ package org.thingsboard.server.dao.service.validator; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.UserCredentials; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java index 21e2143f1a..4df37ddc00 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java @@ -15,12 +15,12 @@ */ package org.thingsboard.server.dao.service.validator; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetTypeDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetTypeDataValidator.java index 25c6b5db86..aad1d1bc4f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetTypeDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetTypeDataValidator.java @@ -16,9 +16,8 @@ package org.thingsboard.server.dao.service.validator; import lombok.AllArgsConstructor; -import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; -import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; @@ -26,7 +25,6 @@ import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.service.DataValidator; -import org.thingsboard.server.dao.tenant.TenantDao; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.widget.WidgetTypeDao; import org.thingsboard.server.dao.widget.WidgetsBundleDao; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetsBundleDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetsBundleDataValidator.java index b950a12826..14faf3e80c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetsBundleDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/WidgetsBundleDataValidator.java @@ -16,8 +16,8 @@ package org.thingsboard.server.dao.service.validator; import lombok.AllArgsConstructor; -import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.exception.DataValidationException; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java index 2e3fe80448..62516232d2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceDao.java @@ -23,13 +23,13 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.DeviceIdInfo; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.ota.OtaPackageType; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java index 07a680493f..6f4cd8587a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/device/JpaDeviceProfileDao.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.sql.device; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; @@ -24,6 +23,7 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileInfo; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java index 36f834b8bc..2f05f66cdf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/edge/JpaBaseEdgeEventDao.java @@ -18,11 +18,11 @@ package org.thingsboard.server.dao.sql.edge; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.id.EdgeEventId; import org.thingsboard.server.common.data.id.EdgeId; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java index 504bc91911..a9a3e25db2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/event/JpaBaseEventDao.java @@ -24,13 +24,13 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.event.RuleChainDebugEventFilter; -import org.thingsboard.server.common.data.event.RuleNodeDebugEventFilter; import org.thingsboard.server.common.data.event.ErrorEventFilter; import org.thingsboard.server.common.data.event.Event; import org.thingsboard.server.common.data.event.EventFilter; import org.thingsboard.server.common.data.event.EventType; import org.thingsboard.server.common.data.event.LifeCycleEventFilter; +import org.thingsboard.server.common.data.event.RuleChainDebugEventFilter; +import org.thingsboard.server.common.data.event.RuleNodeDebugEventFilter; import org.thingsboard.server.common.data.event.StatisticsEventFilter; import org.thingsboard.server.common.data.id.EventId; import org.thingsboard.server.common.data.page.PageData; @@ -38,7 +38,6 @@ import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.event.EventDao; -import org.thingsboard.server.dao.model.sql.AssetInfoEntity; import org.thingsboard.server.dao.model.sql.EventEntity; import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java index 7d660519eb..ff156eb51c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java @@ -18,8 +18,8 @@ package org.thingsboard.server.dao.sql.query; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java index a1be8884ff..69cb4a2d86 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java @@ -16,11 +16,11 @@ package org.thingsboard.server.dao.sql.query; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Repository; import org.springframework.transaction.support.TransactionTemplate; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java index dde5f35f28..bc7940f300 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultEntityQueryRepository.java @@ -17,12 +17,12 @@ package org.thingsboard.server.dao.sql.query; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Repository; import org.springframework.transaction.support.TransactionTemplate; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityDataAdapter.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityDataAdapter.java index 49640c1640..e2d691f32b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityDataAdapter.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityDataAdapter.java @@ -54,8 +54,8 @@ public class EntityDataAdapter { EntityType entityType = EntityType.valueOf((String) row.get("entity_type")); EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, id); Map> latest = new HashMap<>(); - Map timeseries = new HashMap<>(); - EntityData entityData = new EntityData(entityId, latest, timeseries); + //Maybe avoid empty hashmaps? + EntityData entityData = new EntityData(entityId, latest, new HashMap<>(), new HashMap<>()); for (EntityKeyMapping mapping : selectionMapping) { if (!mapping.isIgnore()) { EntityKey entityKey = mapping.getEntityKey(); @@ -81,7 +81,10 @@ public class EntityDataAdapter { if (value != null) { String strVal = value.toString(); // check number - if (strVal.length() > 0 && NumberUtils.isParsable(strVal)) { + if (NumberUtils.isParsable(strVal)) { + if (strVal.startsWith("0") && !strVal.startsWith("0.")) { + return strVal; + } try { long longVal = Long.parseLong(strVal); return Long.toString(longVal); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java index 1499b7150f..649d3e7f2c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -16,9 +16,9 @@ package org.thingsboard.server.dao.sql.query; import lombok.Data; -import org.apache.commons.lang3.StringUtils; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.ComplexFilterPredicate; import org.thingsboard.server.common.data.query.EntityCountQuery; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java index 69f16d4bbc..22c4661fcd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDao.java @@ -17,8 +17,6 @@ package org.thingsboard.server.dao.sqlts; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; -import com.google.common.util.concurrent.SettableFuture; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; @@ -26,9 +24,9 @@ import org.springframework.data.domain.Sort; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.Aggregation; -import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.DaoUtil; @@ -47,10 +45,9 @@ import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.UUID; -import java.util.concurrent.CompletableFuture; import java.util.function.Function; -import java.util.stream.Collectors; +@SuppressWarnings("UnstableApiUsage") @Slf4j public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSqlTimeseriesDao implements TimeseriesDao { @@ -81,7 +78,7 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq Comparator.comparing((Function) AbstractTsKvEntity::getEntityId) .thenComparing(AbstractTsKvEntity::getKey) .thenComparing(AbstractTsKvEntity::getTs) - ); + ); } @PreDestroy @@ -114,32 +111,32 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq } @Override - public ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, List queries) { + public ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, List queries) { return processFindAllAsync(tenantId, entityId, queries); } @Override - public ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) { + public ListenableFuture findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) { if (query.getAggregation() == Aggregation.NONE) { - return findAllAsyncWithLimit(entityId, query); + return Futures.immediateFuture(findAllAsyncWithLimit(entityId, query)); } else { - List>> futures = new ArrayList<>(); - long endPeriod = query.getEndTs(); + List>> futures = new ArrayList<>(); long startPeriod = query.getStartTs(); + long endPeriod = Math.max(query.getStartTs() + 1, query.getEndTs()); long step = query.getInterval(); - while (startPeriod <= endPeriod) { + while (startPeriod < endPeriod) { long startTs = startPeriod; - long endTs = Math.min(startPeriod + step, endPeriod + 1); + long endTs = Math.min(startPeriod + step, endPeriod); long ts = startTs + (endTs - startTs) / 2; - ListenableFuture> aggregateTsKvEntry = findAndAggregateAsync(entityId, query.getKey(), startTs, endTs, ts, query.getAggregation()); + ListenableFuture> aggregateTsKvEntry = findAndAggregateAsync(entityId, query.getKey(), startTs, endTs, ts, query.getAggregation()); futures.add(aggregateTsKvEntry); startPeriod = endTs; } - return getTskvEntriesFuture(Futures.allAsList(futures)); + return getReadTsKvQueryResultFuture(query, Futures.allAsList(futures)); } } - private ListenableFuture> findAllAsyncWithLimit(EntityId entityId, ReadTsKvQuery query) { + private ReadTsKvQueryResult findAllAsyncWithLimit(EntityId entityId, ReadTsKvQuery query) { Integer keyId = getOrSaveKeyId(query.getKey()); List tsKvEntities = tsKvRepository.findAllWithLimit( entityId.getId(), @@ -147,125 +144,52 @@ public abstract class AbstractChunkedAggregationTimeseriesDao extends AbstractSq query.getStartTs(), query.getEndTs(), PageRequest.of(0, query.getLimit(), - Sort.by(new Sort.Order(Sort.Direction.fromString(query.getOrder()), "ts").nullsNative()))); + Sort.by(new Sort.Order(Sort.Direction.fromString(query.getOrder()), "ts").nullsNative()))); tsKvEntities.forEach(tsKvEntity -> tsKvEntity.setStrKey(query.getKey())); - return Futures.immediateFuture(DaoUtil.convertDataList(tsKvEntities)); + List tsKvEntries = DaoUtil.convertDataList(tsKvEntities); + long lastTs = tsKvEntries.stream().map(TsKvEntry::getTs).max(Long::compare).orElse(query.getStartTs()); + return new ReadTsKvQueryResult(query.getId(), tsKvEntries, lastTs); } - ListenableFuture> findAndAggregateAsync(EntityId entityId, String key, long startTs, long endTs, long ts, Aggregation aggregation) { - List> entitiesFutures = new ArrayList<>(); - switchAggregation(entityId, key, startTs, endTs, aggregation, entitiesFutures); - return Futures.transform(setFutures(entitiesFutures), entity -> { + ListenableFuture> findAndAggregateAsync(EntityId entityId, String key, long startTs, long endTs, long ts, Aggregation aggregation) { + return service.submit(() -> { + TsKvEntity entity = switchAggregation(entityId, key, startTs, endTs, aggregation); if (entity != null && entity.isNotEmpty()) { entity.setEntityId(entityId.getId()); entity.setStrKey(key); entity.setTs(ts); - return Optional.of(DaoUtil.getData(entity)); + return Optional.of(entity); } else { return Optional.empty(); } - }, MoreExecutors.directExecutor()); + }); } - protected void switchAggregation(EntityId entityId, String key, long startTs, long endTs, Aggregation aggregation, List> entitiesFutures) { + protected TsKvEntity switchAggregation(EntityId entityId, String key, long startTs, long endTs, Aggregation aggregation) { + var keyId = getOrSaveKeyId(key); switch (aggregation) { case AVG: - findAvg(entityId, key, startTs, endTs, entitiesFutures); - break; + return tsKvRepository.findAvg(entityId.getId(), keyId, startTs, endTs); case MAX: - findMax(entityId, key, startTs, endTs, entitiesFutures); - break; + var max = tsKvRepository.findNumericMax(entityId.getId(), keyId, startTs, endTs); + if (max.isNotEmpty()) { + return max; + } else { + return tsKvRepository.findStringMax(entityId.getId(), keyId, startTs, endTs); + } case MIN: - findMin(entityId, key, startTs, endTs, entitiesFutures); - break; + var min = tsKvRepository.findNumericMin(entityId.getId(), keyId, startTs, endTs); + if (min.isNotEmpty()) { + return min; + } else { + return tsKvRepository.findStringMin(entityId.getId(), keyId, startTs, endTs); + } case SUM: - findSum(entityId, key, startTs, endTs, entitiesFutures); - break; + return tsKvRepository.findSum(entityId.getId(), keyId, startTs, endTs); case COUNT: - findCount(entityId, key, startTs, endTs, entitiesFutures); - break; + return tsKvRepository.findCount(entityId.getId(), keyId, startTs, endTs); default: throw new IllegalArgumentException("Not supported aggregation type: " + aggregation); } } - - protected void findCount(EntityId entityId, String key, long startTs, long endTs, List> entitiesFutures) { - Integer keyId = getOrSaveKeyId(key); - entitiesFutures.add(tsKvRepository.findCount( - entityId.getId(), - keyId, - startTs, - endTs)); - } - - protected void findSum(EntityId entityId, String key, long startTs, long endTs, List> entitiesFutures) { - Integer keyId = getOrSaveKeyId(key); - entitiesFutures.add(tsKvRepository.findSum( - entityId.getId(), - keyId, - startTs, - endTs)); - } - - protected void findMin(EntityId entityId, String key, long startTs, long endTs, List> entitiesFutures) { - Integer keyId = getOrSaveKeyId(key); - entitiesFutures.add(tsKvRepository.findStringMin( - entityId.getId(), - keyId, - startTs, - endTs)); - entitiesFutures.add(tsKvRepository.findNumericMin( - entityId.getId(), - keyId, - startTs, - endTs)); - } - - protected void findMax(EntityId entityId, String key, long startTs, long endTs, List> entitiesFutures) { - Integer keyId = getOrSaveKeyId(key); - entitiesFutures.add(tsKvRepository.findStringMax( - entityId.getId(), - keyId, - startTs, - endTs)); - entitiesFutures.add(tsKvRepository.findNumericMax( - entityId.getId(), - keyId, - startTs, - endTs)); - } - - protected void findAvg(EntityId entityId, String key, long startTs, long endTs, List> entitiesFutures) { - Integer keyId = getOrSaveKeyId(key); - entitiesFutures.add(tsKvRepository.findAvg( - entityId.getId(), - keyId, - startTs, - endTs)); - } - - protected SettableFuture setFutures(List> entitiesFutures) { - SettableFuture listenableFuture = SettableFuture.create(); - CompletableFuture> entities = - CompletableFuture.allOf(entitiesFutures.toArray(new CompletableFuture[entitiesFutures.size()])) - .thenApply(v -> entitiesFutures.stream() - .map(CompletableFuture::join) - .collect(Collectors.toList())); - - entities.whenComplete((tsKvEntities, throwable) -> { - if (throwable != null) { - listenableFuture.setException(throwable); - } else { - TsKvEntity result = null; - for (TsKvEntity entity : tsKvEntities) { - if (entity.isNotEmpty()) { - result = entity; - break; - } - } - listenableFuture.set(result); - } - }); - return listenableFuture; - } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java index aa0d9b1c47..3de21e41bf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AbstractSqlTimeseriesDao.java @@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Value; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.sql.ScheduledLogExecutorComponent; @@ -38,6 +39,7 @@ import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +@SuppressWarnings("UnstableApiUsage") @Slf4j public abstract class AbstractSqlTimeseriesDao extends BaseAbstractSqlTimeseriesDao implements AggregationTimeseriesDao { @@ -86,22 +88,19 @@ public abstract class AbstractSqlTimeseriesDao extends BaseAbstractSqlTimeseries } } - protected ListenableFuture> processFindAllAsync(TenantId tenantId, EntityId entityId, List queries) { - List>> futures = queries + protected ListenableFuture> processFindAllAsync(TenantId tenantId, EntityId entityId, List queries) { + List> futures = queries .stream() .map(query -> findAllAsync(tenantId, entityId, query)) .collect(Collectors.toList()); return Futures.transform(Futures.allAsList(futures), new Function<>() { @Nullable @Override - public List apply(@Nullable List> results) { + public List apply(@Nullable List results) { if (results == null || results.isEmpty()) { return null; } - return results.stream() - .filter(Objects::nonNull) - .flatMap(List::stream) - .collect(Collectors.toList()); + return results.stream().filter(Objects::nonNull).collect(Collectors.toList()); } }, service); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AggregationTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AggregationTimeseriesDao.java index 31270bacc7..994555aaa5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/AggregationTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/AggregationTimeseriesDao.java @@ -19,11 +19,12 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import java.util.List; public interface AggregationTimeseriesDao { - ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query); + ListenableFuture findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query); } \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/BaseAbstractSqlTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/BaseAbstractSqlTimeseriesDao.java index 82e54168bc..1946198505 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/BaseAbstractSqlTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/BaseAbstractSqlTimeseriesDao.java @@ -22,14 +22,20 @@ import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataIntegrityViolationException; +import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; import org.thingsboard.server.dao.model.sqlts.dictionary.TsKvDictionary; import org.thingsboard.server.dao.model.sqlts.dictionary.TsKvDictionaryCompositeKey; +import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity; import org.thingsboard.server.dao.sql.JpaAbstractDaoListeningExecutorService; import org.thingsboard.server.dao.sqlts.dictionary.TsKvDictionaryRepository; import javax.annotation.Nullable; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -80,18 +86,20 @@ public abstract class BaseAbstractSqlTimeseriesDao extends JpaAbstractDaoListeni return keyId; } - protected ListenableFuture> getTskvEntriesFuture(ListenableFuture>> future) { - return Futures.transform(future, new Function>, List>() { + protected ListenableFuture getReadTsKvQueryResultFuture(ReadTsKvQuery query, ListenableFuture>> future) { + return Futures.transform(future, new Function<>() { @Nullable @Override - public List apply(@Nullable List> results) { + public ReadTsKvQueryResult apply(@Nullable List> results) { if (results == null || results.isEmpty()) { return null; } - return results.stream() - .filter(Optional::isPresent) - .map(Optional::get) - .collect(Collectors.toList()); + List data = results.stream().filter(Optional::isPresent).map(Optional::get).collect(Collectors.toList()); + var lastTs = data.stream().map(AbstractTsKvEntity::getAggValuesLastTs).filter(Objects::nonNull).max(Long::compare); + if (lastTs.isEmpty()) { + lastTs = data.stream().map(AbstractTsKvEntity::getTs).filter(Objects::nonNull).max(Long::compare); + } + return new ReadTsKvQueryResult(query.getId(), DaoUtil.convertDataList(data), lastTs.orElse(query.getStartTs())); } }, service); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java index db30adead2..50975f21de 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; @@ -190,7 +191,8 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme long endTs = query.getStartTs() - 1; ReadTsKvQuery findNewLatestQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, endTs - startTs, 1, Aggregation.NONE, DESC_ORDER); - return aggregationTimeseriesDao.findAllAsync(tenantId, entityId, findNewLatestQuery); + return Futures.transform(aggregationTimeseriesDao.findAllAsync(tenantId, entityId, findNewLatestQuery), + ReadTsKvQueryResult::getData, MoreExecutors.directExecutor()); } protected ListenableFuture getFindLatestFuture(EntityId entityId, String key) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java index 9cf796b0aa..d37234dfde 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/AggregationRepository.java @@ -36,54 +36,79 @@ public class AggregationRepository { public static final String FIND_SUM = "findSum"; public static final String FIND_COUNT = "findCount"; - public static final String FROM_WHERE_CLAUSE = "FROM ts_kv tskv WHERE tskv.entity_id = cast(:entityId AS uuid) AND tskv.key= cast(:entityKey AS int) AND tskv.ts > :startTs AND tskv.ts <= :endTs GROUP BY tskv.entity_id, tskv.key, tsBucket ORDER BY tskv.entity_id, tskv.key, tsBucket"; - - public static final String FIND_AVG_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, SUM(COALESCE(tskv.long_v, 0)) AS longValue, SUM(COALESCE(tskv.dbl_v, 0.0)) AS doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, null AS strValue, 'AVG' AS aggType "; - - public static final String FIND_MAX_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, MAX(COALESCE(tskv.long_v, -9223372036854775807)) AS longValue, MAX(COALESCE(tskv.dbl_v, -1.79769E+308)) as doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, MAX(tskv.str_v) AS strValue, 'MAX' AS aggType "; - - public static final String FIND_MIN_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, MIN(COALESCE(tskv.long_v, 9223372036854775807)) AS longValue, MIN(COALESCE(tskv.dbl_v, 1.79769E+308)) as doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, MIN(tskv.str_v) AS strValue, 'MIN' AS aggType "; - - public static final String FIND_SUM_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, SUM(COALESCE(tskv.long_v, 0)) AS longValue, SUM(COALESCE(tskv.dbl_v, 0.0)) AS doubleValue, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, null AS strValue, null AS jsonValue, 'SUM' AS aggType "; - - public static final String FIND_COUNT_QUERY = "SELECT time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, SUM(CASE WHEN tskv.bool_v IS NULL THEN 0 ELSE 1 END) AS booleanValueCount, SUM(CASE WHEN tskv.str_v IS NULL THEN 0 ELSE 1 END) AS strValueCount, SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longValueCount, SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleValueCount, SUM(CASE WHEN tskv.json_v IS NULL THEN 0 ELSE 1 END) AS jsonValueCount "; + public static final String FROM_WHERE_CLAUSE = "FROM ts_kv tskv WHERE " + + "tskv.entity_id = cast(:entityId AS uuid) " + + "AND tskv.key= cast(:entityKey AS int) " + + "AND tskv.ts >= :startTs AND tskv.ts < :endTs " + + "GROUP BY tskv.entity_id, tskv.key, tsBucket " + + "ORDER BY tskv.entity_id, tskv.key, tsBucket"; + + public static final String FIND_AVG_QUERY = "SELECT " + + "time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, " + + "SUM(COALESCE(tskv.long_v, 0)) AS longValue, " + + "SUM(COALESCE(tskv.dbl_v, 0.0)) AS doubleValue, " + + "SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, " + + "SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, " + + "null AS strValue, 'AVG' AS aggType, MAX(tskv.ts) AS maxAggTs "; + + public static final String FIND_MAX_QUERY = "SELECT " + + "time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, " + + "MAX(COALESCE(tskv.long_v, -9223372036854775807)) AS longValue, " + + "MAX(COALESCE(tskv.dbl_v, -1.79769E+308)) as doubleValue, " + + "SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, " + + "SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, " + + "MAX(tskv.str_v) AS strValue, 'MAX' AS aggType, MAX(tskv.ts) AS maxAggTs "; + + public static final String FIND_MIN_QUERY = "SELECT " + + "time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, " + + "MIN(COALESCE(tskv.long_v, 9223372036854775807)) AS longValue, " + + "MIN(COALESCE(tskv.dbl_v, 1.79769E+308)) as doubleValue, " + + "SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, " + + "SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, " + + "MIN(tskv.str_v) AS strValue, 'MIN' AS aggType, MAX(tskv.ts) AS maxAggTs "; + + public static final String FIND_SUM_QUERY = "SELECT " + + "time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, " + + "SUM(COALESCE(tskv.long_v, 0)) AS longValue, SUM(COALESCE(tskv.dbl_v, 0.0)) AS doubleValue, " + + "SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longCountValue, " + + "SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleCountValue, " + + "null AS strValue, null AS jsonValue, 'SUM' AS aggType, MAX(tskv.ts) AS maxAggTs "; + + public static final String FIND_COUNT_QUERY = "SELECT " + + "time_bucket(:timeBucket, tskv.ts, :startTs) AS tsBucket, :timeBucket AS interval, " + + "SUM(CASE WHEN tskv.bool_v IS NULL THEN 0 ELSE 1 END) AS booleanValueCount, " + + "SUM(CASE WHEN tskv.str_v IS NULL THEN 0 ELSE 1 END) AS strValueCount, " + + "SUM(CASE WHEN tskv.long_v IS NULL THEN 0 ELSE 1 END) AS longValueCount, " + + "SUM(CASE WHEN tskv.dbl_v IS NULL THEN 0 ELSE 1 END) AS doubleValueCount, " + + "SUM(CASE WHEN tskv.json_v IS NULL THEN 0 ELSE 1 END) AS jsonValueCount, " + + "MAX(tskv.ts) AS maxAggTs "; @PersistenceContext private EntityManager entityManager; - @Async - public CompletableFuture> findAvg(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) { - @SuppressWarnings("unchecked") - List resultList = getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_AVG); - return CompletableFuture.supplyAsync(() -> resultList); + @SuppressWarnings("unchecked") + public List findAvg(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) { + return getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_AVG); } - @Async - public CompletableFuture> findMax(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) { - @SuppressWarnings("unchecked") - List resultList = getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_MAX); - return CompletableFuture.supplyAsync(() -> resultList); + @SuppressWarnings("unchecked") + public List findMax(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) { + return getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_MAX); } - @Async - public CompletableFuture> findMin(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) { - @SuppressWarnings("unchecked") - List resultList = getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_MIN); - return CompletableFuture.supplyAsync(() -> resultList); + @SuppressWarnings("unchecked") + public List findMin(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) { + return getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_MIN); } - @Async - public CompletableFuture> findSum(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) { - @SuppressWarnings("unchecked") - List resultList = getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_SUM); - return CompletableFuture.supplyAsync(() -> resultList); + @SuppressWarnings("unchecked") + public List findSum(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) { + return getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_SUM); } - @Async - public CompletableFuture> findCount(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) { - @SuppressWarnings("unchecked") - List resultList = getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_COUNT); - return CompletableFuture.supplyAsync(() -> resultList); + @SuppressWarnings("unchecked") + public List findCount(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs) { + return getResultList(entityId, entityKey, timeBucket, startTs, endTs, FIND_COUNT); } private List getResultList(UUID entityId, int entityKey, long timeBucket, long startTs, long endTs, String query) { @@ -96,5 +121,4 @@ public class AggregationRepository { .getResultList(); } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java index 9c03a9e3dd..6e9f9e61b8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/timescale/TimescaleTimeseriesDao.java @@ -30,11 +30,13 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.AbstractTsKvEntity; import org.thingsboard.server.dao.model.sqlts.timescale.ts.TimescaleTsKvEntity; +import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueParams; import org.thingsboard.server.dao.sql.TbSqlBlockingQueueWrapper; import org.thingsboard.server.dao.sqlts.AbstractSqlTimeseriesDao; @@ -101,13 +103,13 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements } @Override - public ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, List queries) { + public ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, List queries) { return processFindAllAsync(tenantId, entityId, queries); } @Override public ListenableFuture save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl) { - int dataPointDays = getDataPointDays(tsKvEntry, computeTtl(ttl)); + int dataPointDays = getDataPointDays(tsKvEntry, computeTtl(ttl)); String strKey = tsKvEntry.getKey(); Integer keyId = getOrSaveKeyId(strKey); TimescaleTsKvEntity entity = new TimescaleTsKvEntity(); @@ -148,15 +150,15 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements } @Override - public ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) { + public ListenableFuture findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) { if (query.getAggregation() == Aggregation.NONE) { - return findAllAsyncWithLimit(entityId, query); + return Futures.immediateFuture(findAllAsyncWithLimit(entityId, query)); } else { long startTs = query.getStartTs(); - long endTs = query.getEndTs(); + long endTs = Math.max(query.getStartTs() + 1, query.getEndTs()); long timeBucket = query.getInterval(); - ListenableFuture>> future = findAllAndAggregateAsync(entityId, query.getKey(), startTs, endTs, timeBucket, query.getAggregation()); - return getTskvEntriesFuture(future); + List> data = findAllAndAggregateAsync(entityId, query.getKey(), startTs, endTs, timeBucket, query.getAggregation()); + return getReadTsKvQueryResultFuture(query, Futures.immediateFuture(data)); } } @@ -165,7 +167,7 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements super.cleanup(systemTtl); } - private ListenableFuture> findAllAsyncWithLimit(EntityId entityId, ReadTsKvQuery query) { + private ReadTsKvQueryResult findAllAsyncWithLimit(EntityId entityId, ReadTsKvQuery query) { String strKey = query.getKey(); Integer keyId = getOrSaveKeyId(strKey); List timescaleTsKvEntities = tsKvRepository.findAllWithLimit( @@ -174,105 +176,60 @@ public class TimescaleTimeseriesDao extends AbstractSqlTimeseriesDao implements query.getStartTs(), query.getEndTs(), PageRequest.of(0, query.getLimit(), - Sort.by(new Sort.Order(Sort.Direction.fromString(query.getOrder()), "ts").nullsNative())));; + Sort.by(new Sort.Order(Sort.Direction.fromString(query.getOrder()), "ts").nullsNative()))); + ; timescaleTsKvEntities.forEach(tsKvEntity -> tsKvEntity.setStrKey(strKey)); - return Futures.immediateFuture(DaoUtil.convertDataList(timescaleTsKvEntities)); + var tsKvEntries = DaoUtil.convertDataList(timescaleTsKvEntities); + long lastTs = tsKvEntries.stream().map(TsKvEntry::getTs).max(Long::compare).orElse(query.getStartTs()); + return new ReadTsKvQueryResult(query.getId(), tsKvEntries, lastTs); } - private ListenableFuture>> findAllAndAggregateAsync(EntityId entityId, String key, long startTs, long endTs, long timeBucket, Aggregation aggregation) { - CompletableFuture> listCompletableFuture = switchAggregation(key, startTs, endTs, timeBucket, aggregation, entityId.getId()); - SettableFuture> listenableFuture = SettableFuture.create(); - listCompletableFuture.whenComplete((timescaleTsKvEntities, throwable) -> { - if (throwable != null) { - listenableFuture.setException(throwable); - } else { - listenableFuture.set(timescaleTsKvEntities); - } - }); - return Futures.transform(listenableFuture, timescaleTsKvEntities -> { - if (!CollectionUtils.isEmpty(timescaleTsKvEntities)) { - List> result = new ArrayList<>(); - timescaleTsKvEntities.forEach(entity -> { - if (entity != null && entity.isNotEmpty()) { - entity.setEntityId(entityId.getId()); - entity.setStrKey(key); - result.add(Optional.of(DaoUtil.getData(entity))); - } else { - result.add(Optional.empty()); - } - }); - return result; - } else { - return Collections.emptyList(); - } - }, MoreExecutors.directExecutor()); + private List> findAllAndAggregateAsync(EntityId entityId, String key, long startTs, long endTs, long timeBucket, Aggregation aggregation) { + long interval = endTs - startTs; + long remainingPart = interval % timeBucket; + List timescaleTsKvEntities; + if (remainingPart == 0) { + timescaleTsKvEntities = switchAggregation(key, startTs, endTs, timeBucket, aggregation, entityId.getId()); + } else { + interval = interval - remainingPart; + timescaleTsKvEntities = new ArrayList<>(); + timescaleTsKvEntities.addAll(switchAggregation(key, startTs, startTs + interval, timeBucket, aggregation, entityId.getId())); + timescaleTsKvEntities.addAll(switchAggregation(key, startTs + interval, endTs, remainingPart, aggregation, entityId.getId())); + } + + if (!CollectionUtils.isEmpty(timescaleTsKvEntities)) { + List> result = new ArrayList<>(); + timescaleTsKvEntities.forEach(entity -> { + if (entity != null && entity.isNotEmpty()) { + entity.setEntityId(entityId.getId()); + entity.setStrKey(key); + result.add(Optional.of(entity)); + } else { + result.add(Optional.empty()); + } + }); + return result; + } else { + return Collections.emptyList(); + } } - private CompletableFuture> switchAggregation(String key, long startTs, long endTs, long timeBucket, Aggregation aggregation, UUID entityId) { + private List switchAggregation(String key, long startTs, long endTs, long timeBucket, Aggregation aggregation, UUID entityId) { + Integer keyId = getOrSaveKeyId(key); switch (aggregation) { case AVG: - return findAvg(key, startTs, endTs, timeBucket, entityId); + return aggregationRepository.findAvg(entityId, keyId, timeBucket, startTs, endTs); case MAX: - return findMax(key, startTs, endTs, timeBucket, entityId); + return aggregationRepository.findMax(entityId, keyId, timeBucket, startTs, endTs); case MIN: - return findMin(key, startTs, endTs, timeBucket, entityId); + return aggregationRepository.findMin(entityId, keyId, timeBucket, startTs, endTs); case SUM: - return findSum(key, startTs, endTs, timeBucket, entityId); + return aggregationRepository.findSum(entityId, keyId, timeBucket, startTs, endTs); case COUNT: - return findCount(key, startTs, endTs, timeBucket, entityId); + return aggregationRepository.findCount(entityId, keyId, timeBucket, startTs, endTs); default: throw new IllegalArgumentException("Not supported aggregation type: " + aggregation); } } - private CompletableFuture> findCount(String key, long startTs, long endTs, long timeBucket, UUID entityId) { - Integer keyId = getOrSaveKeyId(key); - return aggregationRepository.findCount( - entityId, - keyId, - timeBucket, - startTs, - endTs); - } - - private CompletableFuture> findSum(String key, long startTs, long endTs, long timeBucket, UUID entityId) { - Integer keyId = getOrSaveKeyId(key); - return aggregationRepository.findSum( - entityId, - keyId, - timeBucket, - startTs, - endTs); - } - - private CompletableFuture> findMin(String key, long startTs, long endTs, long timeBucket, UUID entityId) { - Integer keyId = getOrSaveKeyId(key); - return aggregationRepository.findMin( - entityId, - keyId, - timeBucket, - startTs, - endTs); - } - - private CompletableFuture> findMax(String key, long startTs, long endTs, long timeBucket, UUID entityId) { - Integer keyId = getOrSaveKeyId(key); - return aggregationRepository.findMax( - entityId, - keyId, - timeBucket, - startTs, - endTs); - } - - private CompletableFuture> findAvg(String key, long startTs, long endTs, long timeBucket, UUID entityId) { - Integer keyId = getOrSaveKeyId(key); - return aggregationRepository.findAvg( - entityId, - keyId, - timeBucket, - startTs, - endTs); - } - } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/TsKvRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/TsKvRepository.java index c603d9d92b..1d9817a5bd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/TsKvRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/ts/TsKvRepository.java @@ -20,14 +20,12 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import org.springframework.scheduling.annotation.Async; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.dao.model.sqlts.ts.TsKvCompositeKey; import org.thingsboard.server.dao.model.sqlts.ts.TsKvEntity; import java.util.List; import java.util.UUID; -import java.util.concurrent.CompletableFuture; public interface TsKvRepository extends JpaRepository { @@ -48,82 +46,75 @@ public interface TsKvRepository extends JpaRepository= :startTs AND tskv.ts < :endTs") - CompletableFuture findStringMax(@Param("entityId") UUID entityId, + TsKvEntity findStringMax(@Param("entityId") UUID entityId, @Param("entityKey") int entityKey, @Param("startTs") long startTs, @Param("endTs") long endTs); - @Async @Query("SELECT new TsKvEntity(MAX(COALESCE(tskv.longValue, -9223372036854775807)), " + "MAX(COALESCE(tskv.doubleValue, -1.79769E+308)), " + "SUM(CASE WHEN tskv.longValue IS NULL THEN 0 ELSE 1 END), " + "SUM(CASE WHEN tskv.doubleValue IS NULL THEN 0 ELSE 1 END), " + - "'MAX') FROM TsKvEntity tskv " + + "'MAX', MAX(tskv.ts)) FROM TsKvEntity tskv " + "WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs") - CompletableFuture findNumericMax(@Param("entityId") UUID entityId, + TsKvEntity findNumericMax(@Param("entityId") UUID entityId, @Param("entityKey") int entityKey, @Param("startTs") long startTs, @Param("endTs") long endTs); - @Async - @Query("SELECT new TsKvEntity(MIN(tskv.strValue)) FROM TsKvEntity tskv " + + @Query("SELECT new TsKvEntity(MIN(tskv.strValue), MAX(tskv.ts)) FROM TsKvEntity tskv " + "WHERE tskv.strValue IS NOT NULL " + "AND tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs") - CompletableFuture findStringMin(@Param("entityId") UUID entityId, + TsKvEntity findStringMin(@Param("entityId") UUID entityId, @Param("entityKey") int entityKey, @Param("startTs") long startTs, @Param("endTs") long endTs); - @Async @Query("SELECT new TsKvEntity(MIN(COALESCE(tskv.longValue, 9223372036854775807)), " + "MIN(COALESCE(tskv.doubleValue, 1.79769E+308)), " + "SUM(CASE WHEN tskv.longValue IS NULL THEN 0 ELSE 1 END), " + "SUM(CASE WHEN tskv.doubleValue IS NULL THEN 0 ELSE 1 END), " + - "'MIN') FROM TsKvEntity tskv " + + "'MIN', MAX(tskv.ts)) FROM TsKvEntity tskv " + "WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs") - CompletableFuture findNumericMin( + TsKvEntity findNumericMin( @Param("entityId") UUID entityId, @Param("entityKey") int entityKey, @Param("startTs") long startTs, @Param("endTs") long endTs); - @Async @Query("SELECT new TsKvEntity(SUM(CASE WHEN tskv.booleanValue IS NULL THEN 0 ELSE 1 END), " + "SUM(CASE WHEN tskv.strValue IS NULL THEN 0 ELSE 1 END), " + "SUM(CASE WHEN tskv.longValue IS NULL THEN 0 ELSE 1 END), " + "SUM(CASE WHEN tskv.doubleValue IS NULL THEN 0 ELSE 1 END), " + - "SUM(CASE WHEN tskv.jsonValue IS NULL THEN 0 ELSE 1 END)) FROM TsKvEntity tskv " + + "SUM(CASE WHEN tskv.jsonValue IS NULL THEN 0 ELSE 1 END), MAX(tskv.ts)) FROM TsKvEntity tskv " + "WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs") - CompletableFuture findCount(@Param("entityId") UUID entityId, + TsKvEntity findCount(@Param("entityId") UUID entityId, @Param("entityKey") int entityKey, @Param("startTs") long startTs, @Param("endTs") long endTs); - @Async @Query("SELECT new TsKvEntity(SUM(COALESCE(tskv.longValue, 0)), " + "SUM(COALESCE(tskv.doubleValue, 0.0)), " + "SUM(CASE WHEN tskv.longValue IS NULL THEN 0 ELSE 1 END), " + "SUM(CASE WHEN tskv.doubleValue IS NULL THEN 0 ELSE 1 END), " + - "'AVG') FROM TsKvEntity tskv " + + "'AVG', MAX(tskv.ts)) FROM TsKvEntity tskv " + "WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs") - CompletableFuture findAvg(@Param("entityId") UUID entityId, + TsKvEntity findAvg(@Param("entityId") UUID entityId, @Param("entityKey") int entityKey, @Param("startTs") long startTs, @Param("endTs") long endTs); - @Async @Query("SELECT new TsKvEntity(SUM(COALESCE(tskv.longValue, 0)), " + "SUM(COALESCE(tskv.doubleValue, 0.0)), " + "SUM(CASE WHEN tskv.longValue IS NULL THEN 0 ELSE 1 END), " + "SUM(CASE WHEN tskv.doubleValue IS NULL THEN 0 ELSE 1 END), " + - "'SUM') FROM TsKvEntity tskv " + + "'SUM', MAX(tskv.ts)) FROM TsKvEntity tskv " + "WHERE tskv.entityId = :entityId AND tskv.key = :entityKey AND tskv.ts >= :startTs AND tskv.ts < :endTs") - CompletableFuture findSum(@Param("entityId") UUID entityId, + TsKvEntity findSum(@Param("entityId") UUID entityId, @Param("entityKey") int entityKey, @Param("startTs") long startTs, @Param("endTs") long endTs); diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java index 8a2d3354d4..a1ffde92d5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantProfileServiceImpl.java @@ -19,8 +19,6 @@ import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.TenantProfile; diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 866150e679..25489f3e1a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -170,6 +170,11 @@ public class TenantServiceImpl extends AbstractCachedEntityService, Optional> { +public class AggregatePartitionsFunction implements com.google.common.util.concurrent.AsyncFunction, Optional> { private static final int LONG_CNT_POS = 0; private static final int DOUBLE_CNT_POS = 1; private static final int BOOL_CNT_POS = 2; private static final int STR_CNT_POS = 3; private static final int JSON_CNT_POS = 4; - private static final int LONG_POS = 5; - private static final int DOUBLE_POS = 6; - private static final int BOOL_POS = 7; - private static final int STR_POS = 8; - private static final int JSON_POS = 9; + private static final int MAX_TS_POS = 5; + private static final int LONG_POS = 6; + private static final int DOUBLE_POS = 7; + private static final int BOOL_POS = 8; + private static final int STR_POS = 9; + private static final int JSON_POS = 10; + private final Aggregation aggregation; private final String key; @@ -66,29 +70,29 @@ public class AggregatePartitionsFunction implements com.google.common.util.concu } @Override - public ListenableFuture> apply(@Nullable List rsList) { - log.trace("[{}][{}][{}] Going to aggregate data", key, ts, aggregation); - if (rsList == null || rsList.isEmpty()) { - return Futures.immediateFuture(Optional.empty()); - } - return Futures.transform( - Futures.allAsList( - rsList.stream().map(rs -> rs.allRows(this.executor)) - .collect(Collectors.toList())), - rowsList -> { - try { - AggregationResult aggResult = new AggregationResult(); - for (List rs : rowsList) { - for (Row row : rs) { - processResultSetRow(row, aggResult); + public ListenableFuture> apply(@Nullable List rsList) { + log.trace("[{}][{}][{}] Going to aggregate data", key, ts, aggregation); + if (rsList == null || rsList.isEmpty()) { + return Futures.immediateFuture(Optional.empty()); + } + return Futures.transform( + Futures.allAsList( + rsList.stream().map(rs -> rs.allRows(this.executor)) + .collect(Collectors.toList())), + rowsList -> { + try { + AggregationResult aggResult = new AggregationResult(); + for (List rs : rowsList) { + for (Row row : rs) { + processResultSetRow(row, aggResult); + } + } + return processAggregationResult(aggResult); + } catch (Exception e) { + log.error("[{}][{}][{}] Failed to aggregate data", key, ts, aggregation, e); + return Optional.empty(); } - } - return processAggregationResult(aggResult); - } catch (Exception e) { - log.error("[{}][{}][{}] Failed to aggregate data", key, ts, aggregation, e); - return Optional.empty(); - } - }, this.executor); + }, this.executor); } private void processResultSetRow(Row row, AggregationResult aggResult) { @@ -105,6 +109,7 @@ public class AggregatePartitionsFunction implements com.google.common.util.concu long boolCount = row.getLong(BOOL_CNT_POS); long strCount = row.getLong(STR_CNT_POS); long jsonCount = row.getLong(JSON_CNT_POS); + long aggValuesLastTs = row.getLong(MAX_TS_POS); if (longCount > 0 || doubleCount > 0) { if (longCount > 0) { @@ -134,6 +139,8 @@ public class AggregatePartitionsFunction implements com.google.common.util.concu return; } + aggResult.aggValuesLastTs = Math.max(aggResult.aggValuesLastTs, aggValuesLastTs); + if (aggregation == Aggregation.COUNT) { aggResult.count += curCount; } else if (aggregation == Aggregation.AVG || aggregation == Aggregation.SUM) { @@ -231,34 +238,37 @@ public class AggregatePartitionsFunction implements com.google.common.util.concu } } - private Optional processAggregationResult(AggregationResult aggResult) { + private Optional processAggregationResult(AggregationResult aggResult) { Optional result; if (aggResult.dataType == null) { result = Optional.empty(); } else if (aggregation == Aggregation.COUNT) { result = Optional.of(new BasicTsKvEntry(ts, new LongDataEntry(key, aggResult.count))); } else if (aggregation == Aggregation.AVG || aggregation == Aggregation.SUM) { - result = processAvgOrSumResult(aggResult); + result = processAvgOrSumResult(aggregation, aggResult); } else if (aggregation == Aggregation.MIN || aggregation == Aggregation.MAX) { result = processMinOrMaxResult(aggResult); } else { result = Optional.empty(); } - if (!result.isPresent()) { + if (result.isEmpty()) { log.trace("[{}][{}][{}] Aggregated data is empty.", key, ts, aggregation); } - return result; + return result.map(tsKvEntry -> new TsKvEntryAggWrapper(tsKvEntry, aggResult.aggValuesLastTs)); } - private Optional processAvgOrSumResult(AggregationResult aggResult) { + private Optional processAvgOrSumResult(Aggregation aggregation, AggregationResult aggResult) { if (aggResult.count == 0 || (aggResult.dataType == DataType.DOUBLE && aggResult.dValue == null) || (aggResult.dataType == DataType.LONG && aggResult.lValue == null)) { return Optional.empty(); } else if (aggResult.dataType == DataType.DOUBLE || aggResult.dataType == DataType.LONG) { if (aggregation == Aggregation.AVG || aggResult.hasDouble) { double sum = Optional.ofNullable(aggResult.dValue).orElse(0.0d) + Optional.ofNullable(aggResult.lValue).orElse(0L); - return Optional.of(new BasicTsKvEntry(ts, new DoubleDataEntry(key, aggregation == Aggregation.SUM ? sum : (sum / aggResult.count)))); + DoubleDataEntry doubleDataEntry = new DoubleDataEntry(key, aggregation == Aggregation.SUM ? sum : (sum / aggResult.count)); + TsKvEntry result = aggregation == Aggregation.AVG ? new AggTsKvEntry(ts, doubleDataEntry, aggResult.count) : new BasicTsKvEntry(ts, doubleDataEntry); + return Optional.of(result); } else { - return Optional.of(new BasicTsKvEntry(ts, new LongDataEntry(key, aggregation == Aggregation.SUM ? aggResult.lValue : (aggResult.lValue / aggResult.count)))); + LongDataEntry longDataEntry = new LongDataEntry(key, aggregation == Aggregation.SUM ? aggResult.lValue : (aggResult.lValue / aggResult.count)); + return Optional.of(new BasicTsKvEntry(ts, longDataEntry)); } } return Optional.empty(); @@ -291,5 +301,6 @@ public class AggregatePartitionsFunction implements com.google.common.util.concu Long lValue = null; long count = 0; boolean hasDouble = false; + long aggValuesLastTs = 0; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index a74f51ce9e..b4cb27cb53 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.kv.BaseDeleteTsKvQuery; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; import org.thingsboard.server.dao.entityview.EntityViewService; @@ -45,13 +46,15 @@ import org.thingsboard.server.dao.service.Validator; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; -import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.thingsboard.server.common.data.StringUtils.isBlank; /** * @author Andrew Shvayka */ +@SuppressWarnings("UnstableApiUsage") @Service @Slf4j public class BaseTimeseriesService implements TimeseriesService { @@ -59,7 +62,7 @@ public class BaseTimeseriesService implements TimeseriesService { private static final int INSERTS_PER_ENTRY = 3; private static final int INSERTS_PER_ENTRY_WITHOUT_LATEST = 2; private static final int DELETES_PER_ENTRY = INSERTS_PER_ENTRY; - public static final Function, Integer> SUM_ALL_INTEGERS = new Function, Integer>() { + public static final Function, Integer> SUM_ALL_INTEGERS = new Function<>() { @Override public @Nullable Integer apply(@Nullable List input) { int result = 0; @@ -87,7 +90,7 @@ public class BaseTimeseriesService implements TimeseriesService { private EntityViewService entityViewService; @Override - public ListenableFuture> findAll(TenantId tenantId, EntityId entityId, List queries) { + public ListenableFuture> findAllByQueries(TenantId tenantId, EntityId entityId, List queries) { validate(entityId); queries.forEach(this::validate); if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW)) { @@ -103,6 +106,17 @@ public class BaseTimeseriesService implements TimeseriesService { return timeseriesDao.findAllAsync(tenantId, entityId, queries); } + @Override + public ListenableFuture> findAll(TenantId tenantId, EntityId entityId, List queries) { + return Futures.transform(findAllByQueries(tenantId, entityId, queries), + result -> { + if (result != null && !result.isEmpty()) { + return result.stream().map(ReadTsKvQueryResult::getData).flatMap(Collection::stream).collect(Collectors.toList()); + } + return Collections.emptyList(); + }, MoreExecutors.directExecutor()); + } + @Override public ListenableFuture> findLatest(TenantId tenantId, EntityId entityId, Collection keys) { validate(entityId); @@ -244,7 +258,7 @@ public class BaseTimeseriesService implements TimeseriesService { public ListenableFuture> removeAllLatest(TenantId tenantId, EntityId entityId) { validate(entityId); return Futures.transformAsync(this.findAllLatest(tenantId, entityId), latest -> { - if (!latest.isEmpty()) { + if (latest != null && !latest.isEmpty()) { Collection keys = latest.stream().map(TsKvEntry::getKey).collect(Collectors.toList()); return Futures.transform(this.removeLatest(tenantId, entityId, keys), res -> keys, MoreExecutors.directExecutor()); } else { diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java index 3b5e45f206..367ad942a7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java @@ -42,7 +42,9 @@ import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.kv.TsKvEntryAggWrapper; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.nosql.TbResultSet; import org.thingsboard.server.dao.nosql.TbResultSetFuture; @@ -71,6 +73,7 @@ import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal; /** * @author Andrew Shvayka */ +@SuppressWarnings("UnstableApiUsage") @Component @Slf4j @NoSqlTsDao @@ -139,20 +142,10 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD } @Override - public ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, List queries) { - List>> futures = queries.stream().map(query -> findAllAsync(tenantId, entityId, query)).collect(Collectors.toList()); - return Futures.transform(Futures.allAsList(futures), new Function<>() { - @Nullable - @Override - public List apply(@Nullable List> results) { - if (results == null || results.isEmpty()) { - return null; - } - return results.stream() - .flatMap(List::stream) - .collect(Collectors.toList()); - } - }, readResultsProcessingExecutor); + public ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, List queries) { + List> futures = queries.stream() + .map(query -> findAllAsync(tenantId, entityId, query)).collect(Collectors.toList()); + return Futures.allAsList(futures); } @Override @@ -270,28 +263,42 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD } @Override - public ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) { + public ListenableFuture findAllAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) { if (query.getAggregation() == Aggregation.NONE) { return findAllAsyncWithLimit(tenantId, entityId, query); } else { long startPeriod = query.getStartTs(); - long endPeriod = query.getEndTs(); + long endPeriod = Math.max(query.getStartTs() + 1, query.getEndTs()); long step = Math.max(query.getInterval(), MIN_AGGREGATION_STEP_MS); - List>> futures = new ArrayList<>(); - while (startPeriod <= endPeriod) { + List>> futures = new ArrayList<>(); + while (startPeriod < endPeriod) { long startTs = startPeriod; - long endTs = Math.min(startPeriod + step, endPeriod + 1); + long endTs = Math.min(startPeriod + step, endPeriod); long ts = endTs - startTs; ReadTsKvQuery subQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, ts, 1, query.getAggregation(), query.getOrder()); futures.add(findAndAggregateAsync(tenantId, entityId, subQuery, toPartitionTs(startTs), toPartitionTs(endTs))); startPeriod = endTs; } - ListenableFuture>> future = Futures.allAsList(futures); + ListenableFuture>> future = Futures.allAsList(futures); return Futures.transform(future, new Function<>() { @Nullable @Override - public List apply(@Nullable List> input) { - return input == null ? Collections.emptyList() : input.stream().filter(v -> v.isPresent()).map(v -> v.get()).collect(Collectors.toList()); + public ReadTsKvQueryResult apply(@Nullable List> input) { + if (input == null) { + return new ReadTsKvQueryResult(query.getId(), Collections.emptyList(), query.getStartTs()); + } else { + long maxTs = query.getStartTs(); + List data = new ArrayList<>(); + for (var opt : input) { + if (opt.isPresent()) { + TsKvEntryAggWrapper tsKvEntryAggWrapper = opt.get(); + maxTs = Math.max(maxTs, tsKvEntryAggWrapper.getLastEntryTs()); + data.add(tsKvEntryAggWrapper.getEntry()); + } + } + return new ReadTsKvQueryResult(query.getId(), data, maxTs); + } + } }, readResultsProcessingExecutor); } @@ -302,13 +309,13 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD //Cleanup by TTL is native for Cassandra } - private ListenableFuture> findAllAsyncWithLimit(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) { + private ListenableFuture findAllAsyncWithLimit(TenantId tenantId, EntityId entityId, ReadTsKvQuery query) { long minPartition = toPartitionTs(query.getStartTs()); long maxPartition = toPartitionTs(query.getEndTs()); final ListenableFuture> partitionsListFuture = getPartitionsFuture(tenantId, query, entityId, minPartition, maxPartition); final SimpleListenableFuture> resultFuture = new SimpleListenableFuture<>(); - Futures.addCallback(partitionsListFuture, new FutureCallback>() { + Futures.addCallback(partitionsListFuture, new FutureCallback<>() { @Override public void onSuccess(@Nullable List partitions) { TsKvQueryCursor cursor = new TsKvQueryCursor(entityId.getEntityType().name(), entityId.getId(), query, partitions); @@ -321,7 +328,13 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD } }, readResultsProcessingExecutor); - return resultFuture; + return Futures.transform(resultFuture, tsKvEntries -> { + long lastTs = query.getStartTs(); + if (tsKvEntries != null) { + lastTs = tsKvEntries.stream().map(TsKvEntry::getTs).max(Long::compare).orElse(query.getStartTs()); + } + return new ReadTsKvQueryResult(query.getId(), tsKvEntries, lastTs); + }, MoreExecutors.directExecutor()); } private long toPartitionTs(long ts) { @@ -379,7 +392,7 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD } } - private ListenableFuture> findAndAggregateAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query, long minPartition, long maxPartition) { + private ListenableFuture> findAndAggregateAsync(TenantId tenantId, EntityId entityId, ReadTsKvQuery query, long minPartition, long maxPartition) { final Aggregation aggregation = query.getAggregation(); final String key = query.getKey(); final long startTs = query.getStartTs(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java index 124af03a1a..2b3d62712f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java @@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.kv.Aggregation; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult; import org.thingsboard.server.dao.model.ModelConstants; @@ -145,9 +146,10 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes long endTs = query.getStartTs() - 1; ReadTsKvQuery findNewLatestQuery = new BaseReadTsKvQuery(query.getKey(), startTs, endTs, endTs - startTs, 1, Aggregation.NONE, DESC_ORDER); - ListenableFuture> future = aggregationTimeseriesDao.findAllAsync(tenantId, entityId, findNewLatestQuery); + ListenableFuture future = aggregationTimeseriesDao.findAllAsync(tenantId, entityId, findNewLatestQuery); - return Futures.transformAsync(future, entryList -> { + return Futures.transformAsync(future, result -> { + var entryList = result.getData(); if (entryList.size() == 1) { TsKvEntry entry = entryList.get(0); return Futures.transform(saveLatest(tenantId, entityId, entryList.get(0)), v -> new TsKvLatestRemovingResult(entry), MoreExecutors.directExecutor()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java index c075a51434..5fd26d400a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesDao.java @@ -20,16 +20,18 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.DeleteTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import java.util.List; +import java.util.Map; /** * @author Andrew Shvayka */ public interface TimeseriesDao { - ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, List queries); + ListenableFuture> findAllAsync(TenantId tenantId, EntityId entityId, List queries); ListenableFuture save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl); diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index 7c8a8ca27b..fc0de8a4fa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -21,13 +21,14 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.ListenableFuture; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.authentication.DisabledException; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; @@ -111,7 +112,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic if (user.getId() == null) { UserCredentials userCredentials = new UserCredentials(); userCredentials.setEnabled(false); - userCredentials.setActivateToken(RandomStringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH)); + userCredentials.setActivateToken(StringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH)); userCredentials.setUserId(new UserId(savedUser.getUuidId())); saveUserCredentialsAndPasswordHistory(user.getTenantId(), userCredentials); } @@ -177,7 +178,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic if (!userCredentials.isEnabled()) { throw new DisabledException(String.format("User credentials not enabled [%s]", email)); } - userCredentials.setResetToken(RandomStringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH)); + userCredentials.setResetToken(StringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH)); return saveUserCredentials(tenantId, userCredentials); } @@ -187,7 +188,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic if (!userCredentials.isEnabled()) { throw new IncorrectParameterException("Unable to reset password for inactive user"); } - userCredentials.setResetToken(RandomStringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH)); + userCredentials.setResetToken(StringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH)); return saveUserCredentials(tenantId, userCredentials); } @@ -201,6 +202,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic } @Override + @Transactional public void deleteUser(TenantId tenantId, UserId userId) { log.trace("Executing deleteUser [{}]", userId); validateId(userId, INCORRECT_USER_ID + userId); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 22f36a2bfb..e93221508e 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -722,38 +722,6 @@ CREATE TABLE IF NOT EXISTS rpc ( status varchar(255) NOT NULL ); -CREATE OR REPLACE PROCEDURE cleanup_events_by_ttl( - IN regular_events_start_ts bigint, - IN regular_events_end_ts bigint, - IN debug_events_start_ts bigint, - IN debug_events_end_ts bigint, - INOUT deleted bigint) - LANGUAGE plpgsql AS -$$ -DECLARE - ttl_deleted_count bigint DEFAULT 0; - debug_ttl_deleted_count bigint DEFAULT 0; -BEGIN - IF regular_events_start_ts > 0 AND regular_events_end_ts > 0 THEN - EXECUTE format( - 'WITH deleted AS (DELETE FROM event WHERE id in (SELECT id from event WHERE ts > %L::bigint AND ts < %L::bigint AND ' || - '(event_type != %L::varchar AND event_type != %L::varchar)) RETURNING *) ' || - 'SELECT count(*) FROM deleted', regular_events_start_ts, regular_events_end_ts, - 'DEBUG_RULE_NODE', 'DEBUG_RULE_CHAIN') into ttl_deleted_count; - END IF; - IF debug_events_start_ts > 0 AND debug_events_end_ts > 0 THEN - EXECUTE format( - 'WITH deleted AS (DELETE FROM event WHERE id in (SELECT id from event WHERE ts > %L::bigint AND ts < %L::bigint AND ' || - '(event_type = %L::varchar OR event_type = %L::varchar)) RETURNING *) ' || - 'SELECT count(*) FROM deleted', debug_events_start_ts, debug_events_end_ts, - 'DEBUG_RULE_NODE', 'DEBUG_RULE_CHAIN') into debug_ttl_deleted_count; - END IF; - RAISE NOTICE 'Events removed by ttl: %', ttl_deleted_count; - RAISE NOTICE 'Debug Events removed by ttl: %', debug_ttl_deleted_count; - deleted := ttl_deleted_count + debug_ttl_deleted_count; -END -$$; - CREATE OR REPLACE FUNCTION to_uuid(IN entity_id varchar, OUT uuid_id uuid) AS $$ BEGIN diff --git a/dao/src/test/java/org/thingsboard/server/dao/TimescaleDaoServiceTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/TimescaleDaoServiceTestSuite.java new file mode 100644 index 0000000000..6c29aea9ee --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/TimescaleDaoServiceTestSuite.java @@ -0,0 +1,28 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao; + +import org.junit.extensions.cpsuite.ClasspathSuite; +import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters; +import org.junit.runner.RunWith; + +@RunWith(ClasspathSuite.class) +@ClassnameFilters({ + "org.thingsboard.server.dao.service.*.nosql.*ServiceTimescaleTest", +}) +public class TimescaleDaoServiceTestSuite { + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/TimescaleSqlInitializer.java b/dao/src/test/java/org/thingsboard/server/dao/TimescaleSqlInitializer.java new file mode 100644 index 0000000000..991f959937 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/TimescaleSqlInitializer.java @@ -0,0 +1,65 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao; + +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.net.URL; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +@Slf4j +public class TimescaleSqlInitializer { + + private static final List sqlFiles = List.of( + "sql/schema-timescale.sql", + "sql/schema-entities.sql", + "sql/schema-entities-idx.sql", + "sql/schema-entities-idx-psql-addon.sql", + "sql/system-data.sql", + "sql/system-test-psql.sql"); + private static final String dropAllTablesSqlFile = "sql/timescale/drop-all-tables.sql"; + + public static void initDb(Connection conn) { + cleanUpDb(conn); + log.info("initialize Timescale DB..."); + try { + for (String sqlFile : sqlFiles) { + URL sqlFileUrl = Resources.getResource(sqlFile); + String sql = Resources.toString(sqlFileUrl, Charsets.UTF_8); + conn.createStatement().execute(sql); + } + } catch (IOException | SQLException e) { + throw new RuntimeException("Unable to init the Timescale database. Reason: " + e.getMessage(), e); + } + log.info("Timescale DB is initialized!"); + } + + private static void cleanUpDb(Connection conn) { + log.info("clean up Timescale DB..."); + try { + URL dropAllTableSqlFileUrl = Resources.getResource(dropAllTablesSqlFile); + String dropAllTablesSql = Resources.toString(dropAllTableSqlFileUrl, Charsets.UTF_8); + conn.createStatement().execute(dropAllTablesSql); + } catch (IOException | SQLException e) { + throw new RuntimeException("Unable to clean up the Timescale database. Reason: " + e.getMessage(), e); + } + } +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index 475f84a29a..674d00d5f6 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -18,7 +18,6 @@ package org.thingsboard.server.dao.service; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -33,14 +32,13 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.EventInfo; import org.thingsboard.server.common.data.OtaPackage; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.edge.Edge; -import org.thingsboard.server.common.data.event.Event; import org.thingsboard.server.common.data.event.RuleNodeDebugEvent; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; @@ -265,8 +263,8 @@ public abstract class AbstractServiceTest { edge.setTenantId(tenantId); edge.setName(name); edge.setType(type); - edge.setSecret(RandomStringUtils.randomAlphanumeric(20)); - edge.setRoutingKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setSecret(StringUtils.randomAlphanumeric(20)); + edge.setRoutingKey(StringUtils.randomAlphanumeric(20)); return edge; } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java index a4bffa9d65..0a6cd0f33c 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java @@ -16,13 +16,13 @@ package org.thingsboard.server.dao.service; import com.datastax.oss.driver.api.core.uuid.Uuids; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; @@ -257,7 +257,7 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { for (int i=0;i<143;i++) { Asset asset = new Asset(); asset.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1+suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); asset.setName(name); @@ -269,7 +269,7 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { for (int i=0;i<175;i++) { Asset asset = new Asset(); asset.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2+suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); asset.setName(name); @@ -335,7 +335,7 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { for (int i=0;i<143;i++) { Asset asset = new Asset(); asset.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1+suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); asset.setName(name); @@ -348,7 +348,7 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { for (int i=0;i<175;i++) { Asset asset = new Asset(); asset.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2+suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); asset.setName(name); @@ -470,7 +470,7 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { for (int i=0;i<175;i++) { Asset asset = new Asset(); asset.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1+suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); asset.setName(name); @@ -483,7 +483,7 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { for (int i=0;i<143;i++) { Asset asset = new Asset(); asset.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2+suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); asset.setName(name); @@ -558,7 +558,7 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { for (int i=0;i<175;i++) { Asset asset = new Asset(); asset.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1+suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); asset.setName(name); @@ -572,7 +572,7 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { for (int i=0;i<143;i++) { Asset asset = new Asset(); asset.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2+suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); asset.setName(name); @@ -634,8 +634,8 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { @Test public void testCleanCacheIfAssetRenamed() { - String assetNameBeforeRename = RandomStringUtils.randomAlphanumeric(15); - String assetNameAfterRename = RandomStringUtils.randomAlphanumeric(15); + String assetNameBeforeRename = StringUtils.randomAlphanumeric(15); + String assetNameAfterRename = StringUtils.randomAlphanumeric(15); Asset asset = new Asset(); asset.setTenantId(tenantId); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseCustomerServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseCustomerServiceTest.java index 4dd3169579..f494b330e2 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseCustomerServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseCustomerServiceTest.java @@ -20,13 +20,13 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -189,7 +189,7 @@ public abstract class BaseCustomerServiceTest extends AbstractServiceTest { for (int i = 0; i < 143; i++) { Customer customer = new Customer(); customer.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); + String suffix = StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); String title = title1 + suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); customer.setTitle(title); @@ -202,7 +202,7 @@ public abstract class BaseCustomerServiceTest extends AbstractServiceTest { for (int i = 0; i < 175; i++) { Customer customer = new Customer(); customer.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); + String suffix = StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); String title = title2 + suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); customer.setTitle(title); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java index 770b215adf..cd40ea8761 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.dao.service; import com.datastax.oss.driver.api.core.uuid.Uuids; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -24,6 +23,7 @@ import org.junit.Test; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; @@ -272,7 +272,7 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { for (int i=0;i<123;i++) { Dashboard dashboard = new Dashboard(); dashboard.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*17)); + String suffix = StringUtils.randomAlphanumeric((int)(Math.random()*17)); String title = title1+suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); dashboard.setTitle(title); @@ -283,7 +283,7 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { for (int i=0;i<193;i++) { Dashboard dashboard = new Dashboard(); dashboard.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15)); + String suffix = StringUtils.randomAlphanumeric((int)(Math.random()*15)); String title = title2+suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); dashboard.setTitle(title); @@ -416,8 +416,8 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { edge.setType("default"); edge.setName("Test different edge"); edge.setType("default"); - edge.setSecret(RandomStringUtils.randomAlphanumeric(20)); - edge.setRoutingKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setSecret(StringUtils.randomAlphanumeric(20)); + edge.setRoutingKey(StringUtils.randomAlphanumeric(20)); edge = edgeService.saveEdge(edge); try { dashboardService.assignDashboardToEdge(tenantId, dashboard.getId(), edge.getId()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java index 3bbe6f22ab..111ec63188 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.service; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -26,13 +25,13 @@ import org.springframework.cache.CacheManager; import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.server.common.data.CacheConstants; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceCredentialsId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.dao.device.DeviceCredentialsDao; import org.thingsboard.server.dao.device.DeviceCredentialsService; -import org.thingsboard.server.dao.device.DeviceDao; import org.thingsboard.server.dao.device.DeviceService; import java.util.UUID; @@ -44,8 +43,8 @@ import static org.mockito.Mockito.when; public abstract class BaseDeviceCredentialsCacheTest extends AbstractServiceTest { - private static final String CREDENTIALS_ID_1 = RandomStringUtils.randomAlphanumeric(20); - private static final String CREDENTIALS_ID_2 = RandomStringUtils.randomAlphanumeric(20); + private static final String CREDENTIALS_ID_1 = StringUtils.randomAlphanumeric(20); + private static final String CREDENTIALS_ID_2 = StringUtils.randomAlphanumeric(20); @Autowired private DeviceCredentialsService deviceCredentialsService; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java index b4c0ff77dd..40d508f84b 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.dao.service; import com.datastax.oss.driver.api.core.uuid.Uuids; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -29,11 +28,12 @@ import org.thingsboard.server.common.data.DeviceInfo; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.OtaPackage; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; -import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.DeviceCredentials; @@ -424,7 +424,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { for (int i = 0; i < 143; i++) { Device device = new Device(); device.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); device.setName(name); @@ -436,7 +436,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { for (int i = 0; i < 175; i++) { Device device = new Device(); device.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); device.setName(name); @@ -502,7 +502,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { for (int i = 0; i < 143; i++) { Device device = new Device(); device.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); device.setName(name); @@ -515,7 +515,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { for (int i = 0; i < 175; i++) { Device device = new Device(); device.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); device.setName(name); @@ -637,7 +637,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { for (int i = 0; i < 175; i++) { Device device = new Device(); device.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); device.setName(name); @@ -650,7 +650,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { for (int i = 0; i < 143; i++) { Device device = new Device(); device.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); device.setName(name); @@ -725,7 +725,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { for (int i = 0; i < 175; i++) { Device device = new Device(); device.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); device.setName(name); @@ -739,7 +739,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { for (int i = 0; i < 143; i++) { Device device = new Device(); device.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); device.setName(name); @@ -801,8 +801,8 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { @Test public void testCleanCacheIfDeviceRenamed() { - String deviceNameBeforeRename = RandomStringUtils.randomAlphanumeric(15); - String deviceNameAfterRename = RandomStringUtils.randomAlphanumeric(15); + String deviceNameBeforeRename = StringUtils.randomAlphanumeric(15); + String deviceNameAfterRename = StringUtils.randomAlphanumeric(15); Device device = new Device(); device.setTenantId(tenantId); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java index 093bd386a7..6b3ceba23f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java @@ -17,7 +17,6 @@ package org.thingsboard.server.dao.service; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.node.ObjectNode; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -25,6 +24,7 @@ import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; @@ -236,7 +236,7 @@ public abstract class BaseEdgeServiceTest extends AbstractServiceTest { String title1 = "Edge title 1"; List edgesTitle1 = new ArrayList<>(); for (int i = 0; i < 143; i++) { - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); Edge edge = constructEdge(name, "default"); @@ -245,7 +245,7 @@ public abstract class BaseEdgeServiceTest extends AbstractServiceTest { String title2 = "Edge title 2"; List edgesTitle2 = new ArrayList<>(); for (int i = 0; i < 175; i++) { - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); Edge edge = constructEdge(name, "default"); @@ -308,7 +308,7 @@ public abstract class BaseEdgeServiceTest extends AbstractServiceTest { String type1 = "typeA"; List edgesType1 = new ArrayList<>(); for (int i = 0; i < 143; i++) { - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); Edge edge = constructEdge(name, type1); @@ -318,7 +318,7 @@ public abstract class BaseEdgeServiceTest extends AbstractServiceTest { String type2 = "typeB"; List edgesType2 = new ArrayList<>(); for (int i = 0; i < 175; i++) { - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); Edge edge = constructEdge(name, type2); @@ -434,7 +434,7 @@ public abstract class BaseEdgeServiceTest extends AbstractServiceTest { String title1 = "Edge title 1"; List edgesTitle1 = new ArrayList<>(); for (int i = 0; i < 175; i++) { - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); Edge edge = constructEdge(name, "default"); @@ -444,7 +444,7 @@ public abstract class BaseEdgeServiceTest extends AbstractServiceTest { String title2 = "Edge title 2"; List edgesTitle2 = new ArrayList<>(); for (int i = 0; i < 143; i++) { - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); Edge edge = constructEdge(name, "default"); @@ -516,7 +516,7 @@ public abstract class BaseEdgeServiceTest extends AbstractServiceTest { String type1 = "typeC"; List edgesType1 = new ArrayList<>(); for (int i = 0; i < 175; i++) { - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title1 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); Edge edge = constructEdge(name, type1); @@ -527,7 +527,7 @@ public abstract class BaseEdgeServiceTest extends AbstractServiceTest { String type2 = "typeD"; List edgesType2 = new ArrayList<>(); for (int i = 0; i < 143; i++) { - String suffix = RandomStringUtils.randomAlphanumeric(15); + String suffix = StringUtils.randomAlphanumeric(15); String name = title2 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); Edge edge = constructEdge(name, type2); @@ -592,8 +592,8 @@ public abstract class BaseEdgeServiceTest extends AbstractServiceTest { @Test public void testCleanCacheIfEdgeRenamed() { - String edgeNameBeforeRename = RandomStringUtils.randomAlphanumeric(15); - String edgeNameAfterRename = RandomStringUtils.randomAlphanumeric(15); + String edgeNameBeforeRename = StringUtils.randomAlphanumeric(15); + String edgeNameAfterRename = StringUtils.randomAlphanumeric(15); Edge edge = constructEdge(tenantId, edgeNameBeforeRename, "default"); edgeService.saveEdge(edge); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java index 808e499239..5cfd4667de 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEntityServiceTest.java @@ -19,9 +19,7 @@ import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomUtils; -import org.apache.commons.lang3.StringUtils; import org.hamcrest.Matchers; import org.junit.After; import org.junit.Assert; @@ -33,6 +31,7 @@ import org.springframework.jdbc.core.ResultSetExtractor; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.edge.Edge; @@ -312,8 +311,8 @@ public abstract class BaseEntityServiceTest extends AbstractServiceTest { edge.setName("Edge" + i); edge.setType(type); edge.setLabel("EdgeLabel" + i); - edge.setSecret(RandomStringUtils.randomAlphanumeric(20)); - edge.setRoutingKey(RandomStringUtils.randomAlphanumeric(20)); + edge.setSecret(StringUtils.randomAlphanumeric(20)); + edge.setRoutingKey(StringUtils.randomAlphanumeric(20)); return edge; } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java index 592a541f81..5a7a5e33bb 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java @@ -16,12 +16,12 @@ package org.thingsboard.server.dao.service; import com.google.common.collect.Lists; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.oauth2.MapperType; import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; @@ -654,7 +654,7 @@ public abstract class BaseOAuth2ServiceTest extends AbstractServiceTest { private OAuth2MobileInfo validMobileInfo(String pkgName, String appSecret) { return OAuth2MobileInfo.builder().pkgName(pkgName) - .appSecret(appSecret != null ? appSecret : RandomStringUtils.randomAlphanumeric(24)) + .appSecret(appSecret != null ? appSecret : StringUtils.randomAlphanumeric(24)) .build(); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java index ade4ce8c28..f4ddb45eed 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.dao.service; import com.datastax.oss.driver.api.core.uuid.Uuids; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -28,6 +27,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.OtaPackageInfo; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -38,7 +38,6 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.exception.DataValidationException; -import javax.validation.ValidationException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; @@ -669,7 +668,7 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { OtaPackageInfo firmwareInfo = new OtaPackageInfo(); firmwareInfo.setDeviceProfileId(deviceProfileId); firmwareInfo.setType(FIRMWARE); - firmwareInfo.setTitle(RandomStringUtils.random(257)); + firmwareInfo.setTitle(StringUtils.random(257)); firmwareInfo.setVersion(VERSION); firmwareInfo.setUrl(URL); firmwareInfo.setTenantId(tenantId); @@ -689,7 +688,7 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmwareInfo.setTenantId(tenantId); firmwareInfo.setTitle(TITLE); - firmwareInfo.setVersion(RandomStringUtils.random(257)); + firmwareInfo.setVersion(StringUtils.random(257)); thrown.expectMessage("length of version must be equal or less than 255"); otaPackageService.saveOtaPackageInfo(firmwareInfo, true); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java index 92a3a6af3a..92dabab6c6 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java @@ -17,11 +17,11 @@ package org.thingsboard.server.dao.service; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.RuleChainId; @@ -175,7 +175,7 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { for (int i = 0; i < 123; i++) { RuleChain ruleChain = new RuleChain(); ruleChain.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int) (Math.random() * 17)); + String suffix = StringUtils.randomAlphanumeric((int) (Math.random() * 17)); String name = name1 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); ruleChain.setName(name); @@ -186,7 +186,7 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { for (int i = 0; i < 193; i++) { RuleChain ruleChain = new RuleChain(); ruleChain.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int) (Math.random() * 15)); + String suffix = StringUtils.randomAlphanumeric((int) (Math.random() * 15)); String name = name2 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); ruleChain.setName(name); @@ -502,7 +502,7 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { for (int i = 0; i < 123; i++) { RuleChain ruleChain = new RuleChain(); ruleChain.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int) (Math.random() * 17)); + String suffix = StringUtils.randomAlphanumeric((int) (Math.random() * 17)); String name = name1 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); ruleChain.setName(name); @@ -516,7 +516,7 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { for (int i = 0; i < 193; i++) { RuleChain ruleChain = new RuleChain(); ruleChain.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int) (Math.random() * 15)); + String suffix = StringUtils.randomAlphanumeric((int) (Math.random() * 15)); String name = name2 + suffix; name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase(); ruleChain.setName(name); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java index 4a48c56157..cad0fcf596 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java @@ -15,7 +15,6 @@ */ package org.thingsboard.server.dao.service; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; @@ -34,6 +33,7 @@ import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.Tenant; @@ -192,7 +192,7 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest { List tenantsTitle1 = new ArrayList<>(); for (int i = 0; i < 134; i++) { Tenant tenant = new Tenant(); - String suffix = RandomStringUtils.randomAlphanumeric((int) (Math.random() * 15)); + String suffix = StringUtils.randomAlphanumeric((int) (Math.random() * 15)); String title = title1 + suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); tenant.setTitle(title); @@ -202,7 +202,7 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest { List tenantsTitle2 = new ArrayList<>(); for (int i = 0; i < 127; i++) { Tenant tenant = new Tenant(); - String suffix = RandomStringUtils.randomAlphanumeric((int) (Math.random() * 15)); + String suffix = StringUtils.randomAlphanumeric((int) (Math.random() * 15)); String title = title2 + suffix; title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase(); tenant.setTitle(title); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java index 532a12dca2..3d8b3c0cf8 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java @@ -15,12 +15,12 @@ */ package org.thingsboard.server.dao.service; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.CustomerId; @@ -248,7 +248,7 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { User user = new User(); user.setAuthority(Authority.TENANT_ADMIN); user.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); + String suffix = StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); String email = email1 + suffix + "@thingsboard.org"; email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase(); user.setEmail(email); @@ -262,7 +262,7 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { User user = new User(); user.setAuthority(Authority.TENANT_ADMIN); user.setTenantId(tenantId); - String suffix = RandomStringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); + String suffix = StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); String email = email2 + suffix + "@thingsboard.org"; email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase(); user.setEmail(email); @@ -400,7 +400,7 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { user.setAuthority(Authority.CUSTOMER_USER); user.setTenantId(tenantId); user.setCustomerId(customerId); - String suffix = RandomStringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); + String suffix = StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); String email = email1 + suffix + "@thingsboard.org"; email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase(); user.setEmail(email); @@ -415,7 +415,7 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { user.setAuthority(Authority.CUSTOMER_USER); user.setTenantId(tenantId); user.setCustomerId(customerId); - String suffix = RandomStringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); + String suffix = StringUtils.randomAlphanumeric((int) (5 + Math.random() * 10)); String email = email2 + suffix + "@thingsboard.org"; email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase(); user.setEmail(email); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DaoTimescaleTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DaoTimescaleTest.java new file mode 100644 index 0000000000..237f539cf4 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DaoTimescaleTest.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service; + +import org.springframework.test.context.TestPropertySource; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +@TestPropertySource(locations = {"classpath:application-test.properties", "classpath:timescale-test.properties"}) +public @interface DaoTimescaleTest { +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java index 5339863044..4e2fd17846 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java @@ -213,17 +213,17 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { } saveEntries(deviceId, TS + 100L + 1L); - List queries = List.of(new BaseReadTsKvQuery(LONG_KEY, TS, TS + 100, 101, 1, Aggregation.COUNT, DESC_ORDER)); + List queries = List.of(new BaseReadTsKvQuery(LONG_KEY, TS, TS + 100, 100, 1, Aggregation.COUNT, DESC_ORDER)); List entries = tsService.findAll(tenantId, deviceId, queries).get(); Assert.assertEquals(1, entries.size()); - Assert.assertEquals(toTsEntry(TS + 50, new LongDataEntry(LONG_KEY, 11L)), entries.get(0)); + Assert.assertEquals(toTsEntry(TS + 50, new LongDataEntry(LONG_KEY, 10L)), entries.get(0)); EntityView entityView = saveAndCreateEntityView(deviceId, List.of(LONG_KEY)); entries = tsService.findAll(tenantId, entityView.getId(), queries).get(); Assert.assertEquals(1, entries.size()); - Assert.assertEquals(toTsEntry(TS + 50, new LongDataEntry(LONG_KEY, 11L)), entries.get(0)); + Assert.assertEquals(toTsEntry(TS + 50, new LongDataEntry(LONG_KEY, 10L)), entries.get(0)); } @Test @@ -240,14 +240,14 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { List entries = tsService.findAll(tenantId, deviceId, queries).get(); Assert.assertEquals(2, entries.size()); Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0)); - Assert.assertEquals(toTsEntry(TS + 75000, new LongDataEntry(LONG_KEY, 5L)), entries.get(1)); + Assert.assertEquals(toTsEntry(TS + 75000 - 1, new LongDataEntry(LONG_KEY, 5L)), entries.get(1)); EntityView entityView = saveAndCreateEntityView(deviceId, List.of(LONG_KEY)); entries = tsService.findAll(tenantId, entityView.getId(), queries).get(); Assert.assertEquals(2, entries.size()); Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0)); - Assert.assertEquals(toTsEntry(TS + 75000, new LongDataEntry(LONG_KEY, 5L)), entries.get(1)); + Assert.assertEquals(toTsEntry(TS + 75000 - 1, new LongDataEntry(LONG_KEY, 5L)), entries.get(1)); } @Test @@ -264,14 +264,14 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { List entries = tsService.findAll(tenantId, deviceId, queries).get(); Assert.assertEquals(2, entries.size()); Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0)); - Assert.assertEquals(toTsEntry(TS + 65000, new LongDataEntry(LONG_KEY, 4L)), entries.get(1)); + Assert.assertEquals(toTsEntry(TS + 65000, new LongDataEntry(LONG_KEY, 3L)), entries.get(1)); EntityView entityView = saveAndCreateEntityView(deviceId, List.of(LONG_KEY)); entries = tsService.findAll(tenantId, entityView.getId(), queries).get(); Assert.assertEquals(2, entries.size()); Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0)); - Assert.assertEquals(toTsEntry(TS + 65000, new LongDataEntry(LONG_KEY, 4L)), entries.get(1)); + Assert.assertEquals(toTsEntry(TS + 65000, new LongDataEntry(LONG_KEY, 3L)), entries.get(1)); } @Test @@ -286,14 +286,14 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { List entries = tsService.findAll(tenantId, deviceId, queries).get(); Assert.assertEquals(2, entries.size()); Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0)); - Assert.assertEquals(toTsEntry(TS + 75000, new LongDataEntry(LONG_KEY, 5L)), entries.get(1)); + Assert.assertEquals(toTsEntry(TS + 75000 - 1, new LongDataEntry(LONG_KEY, 4L)), entries.get(1)); EntityView entityView = saveAndCreateEntityView(deviceId, List.of(LONG_KEY)); entries = tsService.findAll(tenantId, entityView.getId(), queries).get(); Assert.assertEquals(2, entries.size()); Assert.assertEquals(toTsEntry(TS + 25000, new LongDataEntry(LONG_KEY, 5L)), entries.get(0)); - Assert.assertEquals(toTsEntry(TS + 75000, new LongDataEntry(LONG_KEY, 5L)), entries.get(1)); + Assert.assertEquals(toTsEntry(TS + 75000 - 1, new LongDataEntry(LONG_KEY, 4L)), entries.get(1)); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceTimescaleTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceTimescaleTest.java new file mode 100644 index 0000000000..c36934cf15 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceTimescaleTest.java @@ -0,0 +1,23 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.service.timeseries.nosql; + +import org.thingsboard.server.dao.service.DaoTimescaleTest; +import org.thingsboard.server.dao.service.timeseries.BaseTimeseriesServiceTest; + +@DaoTimescaleTest +public class TimeseriesServiceTimescaleTest extends BaseTimeseriesServiceTest { +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/query/EntityDataAdapterTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/query/EntityDataAdapterTest.java index 8965358e63..6d052db1d1 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/query/EntityDataAdapterTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/query/EntityDataAdapterTest.java @@ -25,5 +25,6 @@ public class EntityDataAdapterTest { public void testConvertValue() { assertThat(EntityDataAdapter.convertValue("500")).isEqualTo("500"); assertThat(EntityDataAdapter.convertValue("500D")).isEqualTo("500D"); //do not convert to Double !!! + assertThat(EntityDataAdapter.convertValue("0101010521130565")).isEqualTo("0101010521130565"); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDaoTest.java index db216f7f6e..9c4b1cbbea 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sqlts/AbstractChunkedAggregationTimeseriesDaoTest.java @@ -19,8 +19,11 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import org.junit.Before; import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult; import org.thingsboard.server.common.data.kv.TsKvEntry; import java.util.Optional; @@ -43,25 +46,25 @@ public class AbstractChunkedAggregationTimeseriesDaoTest { final int LIMIT = 1; final String TEMP = "temp"; final String DESC = "DESC"; - AbstractChunkedAggregationTimeseriesDao tsDao; + private AbstractChunkedAggregationTimeseriesDao tsDao; @Before public void setUp() throws Exception { tsDao = spy(AbstractChunkedAggregationTimeseriesDao.class); - ListenableFuture> optionalListenableFuture = Futures.immediateFuture(Optional.of(mock(TsKvEntry.class))); - willReturn(optionalListenableFuture).given(tsDao).findAndAggregateAsync(any(), anyString(), anyLong(), anyLong(), anyLong(), any()); - willReturn(Futures.immediateFuture(mock(TsKvEntry.class))).given(tsDao).getTskvEntriesFuture(any()); + Optional optionalListenableFuture = Optional.of(mock(TsKvEntry.class)); + willReturn(Futures.immediateFuture(optionalListenableFuture)).given(tsDao).findAndAggregateAsync(any(), anyString(), anyLong(), anyLong(), anyLong(), any()); + willReturn(Futures.immediateFuture(mock(ReadTsKvQueryResult.class))).given(tsDao).getReadTsKvQueryResultFuture(any(), any()); } @Test public void givenIntervalNotMultiplePeriod_whenAggregateCount_thenLastIntervalShorterThanOthersAndEqualsEndTs() { ReadTsKvQuery query = new BaseReadTsKvQuery(TEMP, 1, 3000, 2000, LIMIT, COUNT, DESC); ReadTsKvQuery subQueryFirst = new BaseReadTsKvQuery(TEMP, 1, 2001, 1001, LIMIT, COUNT, DESC); - ReadTsKvQuery subQuerySecond = new BaseReadTsKvQuery(TEMP, 2001, 3001, 2501, LIMIT, COUNT, DESC); + ReadTsKvQuery subQuerySecond = new BaseReadTsKvQuery(TEMP, 2001, 3000, 2501, LIMIT, COUNT, DESC); tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query); verify(tsDao, times(2)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any()); verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQueryFirst.getKey(), 1, 2001, getTsForReadTsKvQuery(1, 2001), COUNT); - verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQuerySecond.getKey(), 2001, 3000 + 1, getTsForReadTsKvQuery(2001, 3001), COUNT); + verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQuerySecond.getKey(), 2001, 3000, getTsForReadTsKvQuery(2001, 3000), COUNT); } @Test @@ -71,19 +74,17 @@ public class AbstractChunkedAggregationTimeseriesDaoTest { willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query); assertThat(tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query)).isNotNull(); verify(tsDao, times(1)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any()); - verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQueryFirst.getKey(), 1, 3000 + 1, getTsForReadTsKvQuery(1, 3001), COUNT); + verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQueryFirst.getKey(), 1, 3000, getTsForReadTsKvQuery(1, 3000), COUNT); } @Test public void givenIntervalNotMultiplePeriod_whenAggregateCount_thenIntervalEqualsPeriodMinusOne() { ReadTsKvQuery query = new BaseReadTsKvQuery(TEMP, 1, 3000, 2999, LIMIT, COUNT, DESC); ReadTsKvQuery subQueryFirst = new BaseReadTsKvQuery(TEMP, 1, 3000, 1500, LIMIT, COUNT, DESC); - ReadTsKvQuery subQuerySecond = new BaseReadTsKvQuery(TEMP, 3000, 3001, 3000, LIMIT, COUNT, DESC); willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query); tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query); - verify(tsDao, times(2)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any()); + verify(tsDao, times(1)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any()); verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQueryFirst.getKey(), 1, 3000, getTsForReadTsKvQuery(1, 3000), COUNT); - verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQuerySecond.getKey(), 3000, 3001, getTsForReadTsKvQuery(3000, 3001), COUNT); } @@ -94,7 +95,7 @@ public class AbstractChunkedAggregationTimeseriesDaoTest { willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query); tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query); verify(tsDao, times(1)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any()); - verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQueryFirst.getKey(), 1, 3001, getTsForReadTsKvQuery(1, 3001), COUNT); + verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQueryFirst.getKey(), 1, 3000, getTsForReadTsKvQuery(1, 3000), COUNT); } @Test @@ -134,7 +135,7 @@ public class AbstractChunkedAggregationTimeseriesDaoTest { willCallRealMethod().given(tsDao).findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query); tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query); verify(tsDao, times(1)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any()); - verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQueryFirst.getKey(), 1, 3001, getTsForReadTsKvQuery(1, 3001), COUNT); + verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, subQueryFirst.getKey(), 1, 3000, getTsForReadTsKvQuery(1, 3000), COUNT); } @Test @@ -144,8 +145,7 @@ public class AbstractChunkedAggregationTimeseriesDaoTest { tsDao.findAllAsync(SYS_TENANT_ID, SYS_TENANT_ID, query); verify(tsDao, times(1000)).findAndAggregateAsync(any(), any(), anyLong(), anyLong(), anyLong(), any()); for (long i = 1; i <= 3000; i += 3) { - ReadTsKvQuery querySub = new BaseReadTsKvQuery(TEMP, i, i + 3, i + (i + 3 - i) / 2, LIMIT, COUNT, DESC); - verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, querySub.getKey(), i, i + 3, getTsForReadTsKvQuery(i, i + 3), COUNT); + verify(tsDao, times(1)).findAndAggregateAsync(SYS_TENANT_ID, TEMP, i, Math.min(i + 3, 3000), getTsForReadTsKvQuery(i, i + 3), COUNT); } } diff --git a/dao/src/test/resources/timescale-test.properties b/dao/src/test/resources/timescale-test.properties new file mode 100644 index 0000000000..2c5552cb75 --- /dev/null +++ b/dao/src/test/resources/timescale-test.properties @@ -0,0 +1,18 @@ +database.ts.type=timescale +database.ts_latest.type=timescale + +sql.ts_inserts_executor_type=fixed +sql.ts_inserts_fixed_thread_pool_size=10 + +spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true +spring.jpa.properties.hibernate.order_by.default_null_ordering=last +spring.jpa.properties.hibernate.jdbc.log.warnings=false + +spring.jpa.show-sql=false + +spring.jpa.hibernate.ddl-auto=none +spring.datasource.username=postgres +spring.datasource.password=postgres +spring.datasource.url=jdbc:tc:timescaledb:latest-pg12:///thingsboard?TC_DAEMON=true&TC_TMPFS=/testtmpfs:rw&?TC_INITFUNCTION=org.thingsboard.server.dao.TimescaleSqlInitializer::initDb +spring.datasource.driverClassName=org.testcontainers.jdbc.ContainerDatabaseDriver +spring.datasource.hikari.maximumPoolSize = 50 diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index da4e900015..a1c745e163 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java index bfe1c913eb..07529baead 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/AbstractContainerTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.msa; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableMap; @@ -24,20 +23,16 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; -import org.apache.cassandra.cql3.Json; -import org.apache.commons.lang3.RandomStringUtils; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.conn.ssl.TrustStrategy; -import org.apache.http.conn.ssl.X509HostnameVerifier; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.ssl.SSLContextBuilder; import org.apache.http.ssl.SSLContexts; -import org.json.simple.JSONObject; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.rules.TestRule; @@ -47,20 +42,14 @@ import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.thingsboard.rest.client.RestClient; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.msa.mapper.WsTelemetryResponse; - -import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSocket; import java.net.URI; -import java.security.cert.X509Certificate; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Random; @Slf4j @@ -125,7 +114,7 @@ public abstract class AbstractContainerTest { protected Device createDevice(String name) { Device device = new Device(); - device.setName(name + RandomStringUtils.randomAlphanumeric(7)); + device.setName(name + StringUtils.randomAlphanumeric(7)); device.setType("DEFAULT"); return restClient.saveDevice(device); } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java index eedf070fac..979fbb187d 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ThingsBoardDbInstaller.java @@ -16,9 +16,9 @@ package org.thingsboard.server.msa; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.rules.ExternalResource; import org.testcontainers.utility.Base58; +import org.thingsboard.server.common.data.StringUtils; import java.io.File; import java.util.ArrayList; @@ -215,7 +215,7 @@ public class ThingsBoardDbInstaller extends ExternalResource { File tbLogsDir = new File(targetDir); tbLogsDir.mkdirs(); - String logsContainerName = "tb-logs-container-" + RandomStringUtils.randomAlphanumeric(10); + String logsContainerName = "tb-logs-container-" + StringUtils.randomAlphanumeric(10); dockerCompose.withCommand("run -d --rm --name " + logsContainerName + " -v " + volumeName + ":/root alpine tail -f /dev/null"); dockerCompose.invokeDocker(); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java index 19a53cb1ce..ce36f08a64 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java @@ -26,8 +26,8 @@ import io.netty.buffer.Unpooled; import io.netty.handler.codec.mqtt.MqttQoS; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.RandomStringUtils; -import org.junit.*; +import org.junit.Assert; +import org.junit.Test; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; @@ -36,6 +36,7 @@ import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttHandler; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.rule.NodeConnectionInfo; @@ -50,8 +51,15 @@ import org.thingsboard.server.msa.mapper.WsTelemetryResponse; import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.*; -import java.util.concurrent.*; +import java.util.Arrays; +import java.util.Objects; +import java.util.Optional; +import java.util.Random; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; @Slf4j public class MqttClientTest extends AbstractContainerTest { @@ -150,7 +158,7 @@ public class MqttClientTest extends AbstractContainerTest { // Add a new client attribute JsonObject clientAttributes = new JsonObject(); - String clientAttributeValue = RandomStringUtils.randomAlphanumeric(8); + String clientAttributeValue = StringUtils.randomAlphanumeric(8); clientAttributes.addProperty("clientAttr", clientAttributeValue); mqttClient.publish("v1/devices/me/attributes", Unpooled.wrappedBuffer(clientAttributes.toString().getBytes())).get(); @@ -166,7 +174,7 @@ public class MqttClientTest extends AbstractContainerTest { // Add a new shared attribute JsonObject sharedAttributes = new JsonObject(); - String sharedAttributeValue = RandomStringUtils.randomAlphanumeric(8); + String sharedAttributeValue = StringUtils.randomAlphanumeric(8); sharedAttributes.addProperty("sharedAttr", sharedAttributeValue); ResponseEntity sharedAttributesResponse = restClient.getRestTemplate() .postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE", @@ -215,7 +223,7 @@ public class MqttClientTest extends AbstractContainerTest { // Add a new shared attribute JsonObject sharedAttributes = new JsonObject(); - String sharedAttributeValue = RandomStringUtils.randomAlphanumeric(8); + String sharedAttributeValue = StringUtils.randomAlphanumeric(8); sharedAttributes.addProperty(sharedAttributeName, sharedAttributeValue); ResponseEntity sharedAttributesResponse = restClient.getRestTemplate() .postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE", @@ -229,7 +237,7 @@ public class MqttClientTest extends AbstractContainerTest { // Update the shared attribute value JsonObject updatedSharedAttributes = new JsonObject(); - String updatedSharedAttributeValue = RandomStringUtils.randomAlphanumeric(8); + String updatedSharedAttributeValue = StringUtils.randomAlphanumeric(8); updatedSharedAttributes.addProperty(sharedAttributeName, updatedSharedAttributeValue); ResponseEntity updatedSharedAttributesResponse = restClient.getRestTemplate() .postForEntity(HTTPS_URL + "/api/plugins/telemetry/DEVICE/{deviceId}/SHARED_SCOPE", diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java index 46aa4acd1d..25a8946431 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttGatewayClientTest.java @@ -28,7 +28,6 @@ import io.netty.buffer.Unpooled; import io.netty.handler.codec.mqtt.MqttQoS; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -39,6 +38,7 @@ import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttHandler; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -226,7 +226,7 @@ public class MqttGatewayClientTest extends AbstractContainerTest { WsClient wsClient = subscribeToWebSocket(createdDevice.getId(), "CLIENT_SCOPE", CmdsType.ATTR_SUB_CMDS); // Add a new client attribute JsonObject clientAttributes = new JsonObject(); - String clientAttributeValue = RandomStringUtils.randomAlphanumeric(8); + String clientAttributeValue = StringUtils.randomAlphanumeric(8); clientAttributes.addProperty("clientAttr", clientAttributeValue); JsonObject gatewayClientAttributes = new JsonObject(); @@ -245,7 +245,7 @@ public class MqttGatewayClientTest extends AbstractContainerTest { // Add a new shared attribute JsonObject sharedAttributes = new JsonObject(); - String sharedAttributeValue = RandomStringUtils.randomAlphanumeric(8); + String sharedAttributeValue = StringUtils.randomAlphanumeric(8); sharedAttributes.addProperty("sharedAttr", sharedAttributeValue); // Subscribe for attribute update event @@ -281,7 +281,7 @@ public class MqttGatewayClientTest extends AbstractContainerTest { // Add a new shared attribute JsonObject sharedAttributes = new JsonObject(); - String sharedAttributeValue = RandomStringUtils.randomAlphanumeric(8); + String sharedAttributeValue = StringUtils.randomAlphanumeric(8); sharedAttributes.addProperty(sharedAttributeName, sharedAttributeValue); JsonObject gatewaySharedAttributeValue = new JsonObject(); @@ -300,7 +300,7 @@ public class MqttGatewayClientTest extends AbstractContainerTest { // Update the shared attribute value JsonObject updatedSharedAttributes = new JsonObject(); - String updatedSharedAttributeValue = RandomStringUtils.randomAlphanumeric(8); + String updatedSharedAttributeValue = StringUtils.randomAlphanumeric(8); updatedSharedAttributes.addProperty(sharedAttributeName, updatedSharedAttributeValue); JsonObject gatewayUpdatedSharedAttributeValue = new JsonObject(); diff --git a/msa/js-executor/docker/Dockerfile b/msa/js-executor/docker/Dockerfile index 8746e1db54..93e5ab21ff 100644 --- a/msa/js-executor/docker/Dockerfile +++ b/msa/js-executor/docker/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/node:16.15.1-bullseye-slim +FROM thingsboard/node:16.17.0-bullseye-slim ENV NODE_ENV production ENV DOCKER_MODE true diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json index f220fd4ff5..dea0eb29c1 100644 --- a/msa/js-executor/package.json +++ b/msa/js-executor/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-js-executor", "private": true, - "version": "3.4.1", + "version": "3.4.2", "description": "ThingsBoard JavaScript Executor Microservice", "main": "server.ts", "bin": "server.js", diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index b13be7048e..95a4e0c724 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/pom.xml b/msa/pom.xml index 4de30d5fdf..e527566683 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT thingsboard msa diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index 785748957e..965d0ab05a 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index 1226f5a5ee..30358d97cb 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml index 0fc536f512..3b5a91dd13 100644 --- a/msa/transport/coap/pom.xml +++ b/msa/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml index 00f3b19917..6fbb13f32f 100644 --- a/msa/transport/http/pom.xml +++ b/msa/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/lwm2m/pom.xml b/msa/transport/lwm2m/pom.xml index 746df2afc4..fa8d99e8b5 100644 --- a/msa/transport/lwm2m/pom.xml +++ b/msa/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml index da786c014f..c63fd43668 100644 --- a/msa/transport/mqtt/pom.xml +++ b/msa/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml index efc9093263..38f08cffaf 100644 --- a/msa/transport/pom.xml +++ b/msa/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/snmp/pom.xml b/msa/transport/snmp/pom.xml index e2ec3d811d..8edd6dfeaa 100644 --- a/msa/transport/snmp/pom.xml +++ b/msa/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.msa transport - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT org.thingsboard.msa.transport diff --git a/msa/vc-executor-docker/pom.xml b/msa/vc-executor-docker/pom.xml index c4be5c06da..38c6979c83 100644 --- a/msa/vc-executor-docker/pom.xml +++ b/msa/vc-executor-docker/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/vc-executor/pom.xml b/msa/vc-executor/pom.xml index bc315e8d34..2a4d07011e 100644 --- a/msa/vc-executor/pom.xml +++ b/msa/vc-executor/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/web-ui/docker/Dockerfile b/msa/web-ui/docker/Dockerfile index 7b047381ae..af6b2d5de6 100644 --- a/msa/web-ui/docker/Dockerfile +++ b/msa/web-ui/docker/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/node:16.15.1-bullseye-slim +FROM thingsboard/node:16.17.0-bullseye-slim ENV NODE_ENV production ENV DOCKER_MODE true diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json index 797ac1c9fd..4529d51510 100644 --- a/msa/web-ui/package.json +++ b/msa/web-ui/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-web-ui", "private": true, - "version": "3.4.1", + "version": "3.4.2", "description": "ThingsBoard Web UI Microservice", "main": "server.ts", "bin": "server.js", diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml index 9d9f3f8e08..872170798f 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT msa org.thingsboard.msa diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index 472ce55fc7..5b4c8506c6 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -19,11 +19,11 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT thingsboard netty-mqtt - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT jar Netty MQTT Client diff --git a/pom.xml b/pom.xml index abc2a46fbf..b7ecea21fe 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT pom Thingsboard @@ -89,7 +89,7 @@ 1.6.3 0.7 1.18.2 - 1.67 + 1.69 2.0.1 42.2.20 org/thingsboard/server/gen/**/*, @@ -133,7 +133,7 @@ 1.3.0 1.2.7 - 1.16.0 + 1.17.3 1.12 3.0.0 6.1.0.202203080745-r diff --git a/rest-client/pom.xml b/rest-client/pom.xml index 8b1d4b9641..4b9efe6177 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT thingsboard rest-client diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index f2f446e534..c1402b93ad 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -34,7 +34,7 @@ import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.support.HttpRequestWrapper; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import org.thingsboard.common.util.ThingsBoardExecutors; diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml index 584bce461c..438c2d3157 100644 --- a/rule-engine/pom.xml +++ b/rule-engine/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT thingsboard rule-engine diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index f3ce720866..99170b585c 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNode.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNode.java index 8780833a5e..1516b6957c 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNode.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNode.java @@ -29,7 +29,7 @@ public interface TbNode { void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException; - void destroy(); + default void destroy() {} default void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) {} diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java index bc38db7fd6..940a5453b6 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/util/TbNodeUtils.java @@ -19,9 +19,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 1bfb82428b..5326d44fd2 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNode.java index 0cd637c01c..efff637e57 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractCustomerActionNode.java @@ -92,6 +92,9 @@ public abstract class TbAbstractCustomerActionNode processEntityRelationAction(TbContext ctx, TbMsg msg, String relationType) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java index 73e03892c9..fd69303501 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCopyAttributesToEntityViewNode.java @@ -158,7 +158,4 @@ public class TbCopyAttributesToEntityViewNode implements TbNode { return CollectionsUtil.contains(keys, attrKey); } - @Override - public void destroy() { - } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java index 22bed5b4e4..2d34972bfa 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java @@ -98,7 +98,4 @@ public class TbMsgCountNode implements TbNode { ctx.tellSelf(tickMsg, curDelay); } - @Override - public void destroy() { - } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java index a03d853c09..06889bd6de 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java @@ -25,7 +25,7 @@ import com.amazonaws.services.sqs.model.SendMessageRequest; import com.amazonaws.services.sqs.model.SendMessageResult; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/CertPemCredentials.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/CertPemCredentials.java index 3a38a286f2..528840d972 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/CertPemCredentials.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/credentials/CertPemCredentials.java @@ -28,7 +28,7 @@ import org.bouncycastle.openssl.PEMKeyPair; import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; import org.bouncycastle.util.encoders.Hex; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import javax.crypto.Cipher; import javax.crypto.EncryptedPrivateKeyInfo; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java index 80b319e087..32ec34ef39 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java @@ -19,7 +19,7 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.common.util.TbStopWatch; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.ScriptEngine; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java index eb30a236af..28dc64c068 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/AbstractTbMsgPushNode.java @@ -71,10 +71,6 @@ public abstract class AbstractTbMsgPushNode dataMap = dataToMap(msg); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java index c4bc9641a4..4832a0af35 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java @@ -103,8 +103,4 @@ public class TbCheckRelationNode implements TbNode { } } - @Override - public void destroy() { - - } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java index fd292be43e..aeddcd18d3 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java @@ -52,8 +52,4 @@ public class TbMsgTypeFilterNode implements TbNode { ctx.tellNext(msg, config.getMessageTypes().contains(msg.getType()) ? "True" : "False"); } - @Override - public void destroy() { - - } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java index 696212c9cb..62e44bb50e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java @@ -117,8 +117,4 @@ public class TbMsgTypeSwitchNode implements TbNode { ctx.tellNext(msg, relationType); } - @Override - public void destroy() { - - } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java index 8b893c0396..10ccc77dc1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java @@ -51,8 +51,4 @@ public class TbOriginatorTypeFilterNode implements TbNode { ctx.tellNext(msg, config.getOriginatorTypes().contains(originatorType) ? "True" : "False"); } - @Override - public void destroy() { - - } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java index d44c555f8c..e5c06c3878 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java @@ -90,8 +90,4 @@ public class TbOriginatorTypeSwitchNode implements TbNode { ctx.tellNext(msg, relationType); } - @Override - public void destroy() { - - } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbAckNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbAckNode.java index 801a31fbb6..64938092f6 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbAckNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbAckNode.java @@ -51,7 +51,4 @@ public class TbAckNode implements TbNode { ctx.tellSuccess(msg); } - @Override - public void destroy() { - } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java index 3fa2b5d64e..ececf96303 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java @@ -54,7 +54,4 @@ public class TbCheckpointNode implements TbNode { ctx.enqueueForTellNext(msg, queueName, TbRelationTypes.SUCCESS, () -> ctx.ack(msg), error -> ctx.tellFailure(msg, error)); } - @Override - public void destroy() { - } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java index eb2e6bb249..fc4a1edfc9 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java @@ -63,7 +63,4 @@ public class TbRuleChainInputNode implements TbNode { ctx.input(msg, ruleChainId); } - @Override - public void destroy() { - } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainOutputNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainOutputNode.java index 5b014d8bc4..bc701dd56a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainOutputNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainOutputNode.java @@ -51,7 +51,4 @@ public class TbRuleChainOutputNode implements TbNode { ctx.output(msg, ctx.getSelf().getName()); } - @Override - public void destroy() { - } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/AbstractGeofencingNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/AbstractGeofencingNode.java index 40bbd6f506..af35e88b81 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/AbstractGeofencingNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/AbstractGeofencingNode.java @@ -148,9 +148,4 @@ public abstract class AbstractGeofencingNode findEntityIdAsync(TbContext ctx, TbMsg msg); private void safePutAttributes(TbContext ctx, TbMsg msg, T entityId) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java index 75d1869e6d..490a42a72c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java @@ -63,10 +63,6 @@ public abstract class TbAbstractGetEntityDetailsNode ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor()); } - @Override - public void destroy() { - } - protected abstract C loadGetEntityDetailsNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException; protected abstract ListenableFuture getDetails(TbContext ctx, TbMsg msg); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbEntityGetAttrNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbEntityGetAttrNode.java index b1eaea0bbd..28fd920264 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbEntityGetAttrNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbEntityGetAttrNode.java @@ -100,11 +100,6 @@ public abstract class TbEntityGetAttrNode implements TbNode ctx.tellSuccess(msg); } - @Override - public void destroy() { - - } - protected abstract ListenableFuture findEntityAsync(TbContext ctx, EntityId originator); public void setConfig(TbGetEntityAttrNodeConfiguration config) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsConfiguration.java index be8e356b59..b0e9bf39e5 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsConfiguration.java @@ -25,6 +25,7 @@ import java.util.Map; public class TbGetOriginatorFieldsConfiguration implements NodeConfiguration { private Map fieldsMapping; + private boolean ignoreNullStrings; @Override public TbGetOriginatorFieldsConfiguration defaultConfiguration() { @@ -33,6 +34,7 @@ public class TbGetOriginatorFieldsConfiguration implements NodeConfiguration { config.getFieldsMapping().forEach((field, metaKey) -> { - String val = data.getFieldValue(field); + String val = data.getFieldValue(field, ignoreNullStrings); if (val != null) { msg.getMetaData().putValue(metaKey, val); } @@ -80,8 +82,4 @@ public class TbGetOriginatorFieldsNode implements TbNode { } } - @Override - public void destroy() { - - } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java index 320502005b..77298cba77 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java @@ -25,7 +25,7 @@ import com.google.gson.JsonParseException; import lombok.Data; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.rule.engine.api.RuleNode; @@ -98,7 +98,7 @@ public class TbGetTelemetryNode implements TbNode { } Aggregation parseAggregationConfig(String aggName) { - if (StringUtils.isEmpty(aggName)) { + if (StringUtils.isEmpty(aggName) || !fetchMode.equals(FETCH_MODE_ALL)) { return Aggregation.NONE; } return Aggregation.valueOf(aggName); @@ -110,11 +110,9 @@ public class TbGetTelemetryNode implements TbNode { ctx.tellFailure(msg, new IllegalStateException("Telemetry is not selected!")); } else { try { - if (config.isUseMetadataIntervalPatterns()) { - checkMetadataKeyPatterns(msg); - } + Interval interval = getInterval(msg); List keys = TbNodeUtils.processPatterns(tsKeyNames, msg); - ListenableFuture> list = ctx.getTimeseriesService().findAll(ctx.getTenantId(), msg.getOriginator(), buildQueries(msg, keys)); + ListenableFuture> list = ctx.getTimeseriesService().findAll(ctx.getTenantId(), msg.getOriginator(), buildQueries(interval, keys)); DonAsynchron.withCallback(list, data -> { process(data, msg, keys); ctx.tellSuccess(ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), msg.getData())); @@ -125,12 +123,7 @@ public class TbGetTelemetryNode implements TbNode { } } - @Override - public void destroy() { - } - - private List buildQueries(TbMsg msg, List keys) { - final Interval interval = getInterval(msg); + private List buildQueries(Interval interval, List keys) { final long aggIntervalStep = Aggregation.NONE.equals(aggregation) ? 1 : // exact how it validates on BaseTimeseriesService.validate() // see CassandraBaseTimeseriesDao.findAllAsync() @@ -210,71 +203,45 @@ public class TbGetTelemetryNode implements TbNode { } private Interval getInterval(TbMsg msg) { - Interval interval = new Interval(); if (config.isUseMetadataIntervalPatterns()) { - if (isParsable(msg, config.getStartIntervalPattern())) { - interval.setStartTs(Long.parseLong(TbNodeUtils.processPattern(config.getStartIntervalPattern(), msg))); - } - if (isParsable(msg, config.getEndIntervalPattern())) { - interval.setEndTs(Long.parseLong(TbNodeUtils.processPattern(config.getEndIntervalPattern(), msg))); - } + return getIntervalFromPatterns(msg); } else { + Interval interval = new Interval(); long ts = System.currentTimeMillis(); interval.setStartTs(ts - TimeUnit.valueOf(config.getStartIntervalTimeUnit()).toMillis(config.getStartInterval())); interval.setEndTs(ts - TimeUnit.valueOf(config.getEndIntervalTimeUnit()).toMillis(config.getEndInterval())); + return interval; } - return interval; - } - - private boolean isParsable(TbMsg msg, String pattern) { - return NumberUtils.isParsable(TbNodeUtils.processPattern(pattern, msg)); } - private void checkMetadataKeyPatterns(TbMsg msg) { - isUndefined(msg, config.getStartIntervalPattern(), config.getEndIntervalPattern()); - isInvalid(msg, config.getStartIntervalPattern(), config.getEndIntervalPattern()); + private Interval getIntervalFromPatterns(TbMsg msg) { + Interval interval = new Interval(); + interval.setStartTs(checkPattern(msg, config.getStartIntervalPattern())); + interval.setEndTs(checkPattern(msg, config.getEndIntervalPattern())); + return interval; } - private void isUndefined(TbMsg msg, String startIntervalPattern, String endIntervalPattern) { - if (getMetadataValue(msg, startIntervalPattern) == null && getMetadataValue(msg, endIntervalPattern) == null) { - throw new IllegalArgumentException("Message metadata values: '" + - replaceRegex(startIntervalPattern) + "' and '" + - replaceRegex(endIntervalPattern) + "' are undefined"); - } else { - if (getMetadataValue(msg, startIntervalPattern) == null) { - throw new IllegalArgumentException("Message metadata value: '" + - replaceRegex(startIntervalPattern) + "' is undefined"); - } - if (getMetadataValue(msg, endIntervalPattern) == null) { - throw new IllegalArgumentException("Message metadata value: '" + - replaceRegex(endIntervalPattern) + "' is undefined"); - } + private long checkPattern(TbMsg msg, String pattern) { + String value = getValuePattern(msg, pattern); + if (value == null) { + throw new IllegalArgumentException("Message value: '" + + replaceRegex(pattern) + "' is undefined"); } - } - - private void isInvalid(TbMsg msg, String startIntervalPattern, String endIntervalPattern) { - if (getInterval(msg).getStartTs() == null && getInterval(msg).getEndTs() == null) { - throw new IllegalArgumentException("Message metadata values: '" + - replaceRegex(startIntervalPattern) + "' and '" + - replaceRegex(endIntervalPattern) + "' have invalid format"); - } else { - if (getInterval(msg).getStartTs() == null) { - throw new IllegalArgumentException("Message metadata value: '" + - replaceRegex(startIntervalPattern) + "' has invalid format"); - } - if (getInterval(msg).getEndTs() == null) { - throw new IllegalArgumentException("Message metadata value: '" + - replaceRegex(endIntervalPattern) + "' has invalid format"); - } + boolean parsable = NumberUtils.isParsable(value); + if (!parsable) { + throw new IllegalArgumentException("Message value: '" + + replaceRegex(pattern) + "' has invalid format"); } + return Long.parseLong(value); } - private String getMetadataValue(TbMsg msg, String pattern) { - return msg.getMetaData().getValue(replaceRegex(pattern)); + private String getValuePattern(TbMsg msg, String pattern) { + String value = TbNodeUtils.processPattern(pattern, msg); + return value.equals(pattern) ? null : value; } private String replaceRegex(String pattern) { - return pattern.replaceAll("[${}]", ""); + return pattern.replaceAll("[$\\[{}\\]]", ""); } private int validateLimit(int limit) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index 1600f2271c..6dd632aa97 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -20,7 +20,7 @@ import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.ssl.SslContext; import io.netty.util.concurrent.Future; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClientConfig; import org.thingsboard.mqtt.MqttConnectResult; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java index 1218518a12..80c75e08ba 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java @@ -18,7 +18,7 @@ package org.thingsboard.rule.engine.mqtt.azure; import io.netty.handler.codec.mqtt.MqttVersion; import io.netty.handler.ssl.SslContext; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.common.util.AzureIotHubUtil; import org.thingsboard.mqtt.MqttClientConfig; import org.thingsboard.rule.engine.api.RuleNode; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java index 831fd3c08c..1f90b4e6c1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/AlarmState.java @@ -20,7 +20,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.ListenableFuture; import lombok.Data; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.rule.engine.action.TbAlarmResult; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java index f0bbdfb66b..5ab5adc7fc 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/DeviceState.java @@ -17,7 +17,7 @@ package org.thingsboard.rule.engine.profile; import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.profile.state.PersistedAlarmState; import org.thingsboard.rule.engine.profile.state.PersistedDeviceState; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java index 0b3822352e..5ba55fe2d5 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java @@ -22,7 +22,7 @@ import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.MessageProperties; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java index e035d415ed..28c937c9ce 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbHttpClient.java @@ -35,7 +35,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.http.client.HttpComponentsAsyncClientHttpRequestFactory; import org.springframework.http.client.Netty4ClientHttpRequestFactory; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.springframework.util.concurrent.ListenableFuture; import org.springframework.util.concurrent.ListenableFutureCallback; import org.springframework.web.client.AsyncRestTemplate; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java index 36497df564..ce9b5050f0 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java @@ -16,7 +16,7 @@ package org.thingsboard.rule.engine.rpc; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; @@ -70,8 +70,4 @@ public class TbSendRPCReplyNode implements TbNode { } } - @Override - public void destroy() { - } - } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java index 45b45e91f9..80d379f79d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java @@ -21,7 +21,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcRequest; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -127,10 +127,6 @@ public class TbSendRPCRequestNode implements TbNode { } } - @Override - public void destroy() { - } - private String wrap(String name, String body) { JsonObject json = new JsonObject(); json.addProperty(name, body); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRpcReplyNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRpcReplyNodeConfiguration.java index a736759f1a..1a014eb323 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRpcReplyNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRpcReplyNodeConfiguration.java @@ -16,7 +16,7 @@ package org.thingsboard.rule.engine.rpc; import lombok.Data; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.server.common.data.DataConstants; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java index 56e1ca8577..9f1c0d4e20 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java @@ -17,7 +17,7 @@ package org.thingsboard.rule.engine.telemetry; import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; @@ -75,8 +75,4 @@ public class TbMsgAttributesNode implements TbNode { ); } - @Override - public void destroy() { - } - } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java index 22d9d1ac07..4a44d9f7b1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java @@ -17,7 +17,7 @@ package org.thingsboard.rule.engine.telemetry; import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; -import org.springframework.util.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationBeginNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationBeginNode.java index ad846b6a29..21f1a9d82f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationBeginNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationBeginNode.java @@ -49,8 +49,4 @@ public class TbSynchronizationBeginNode implements TbNode { ctx.tellSuccess(msg); } - @Override - public void destroy() { - - } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationEndNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationEndNode.java index f346fcf497..5cb81399c6 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationEndNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transaction/TbSynchronizationEndNode.java @@ -48,7 +48,4 @@ public class TbSynchronizationEndNode implements TbNode { ctx.tellSuccess(msg); } - @Override - public void destroy() { - } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java index a59980849d..b48f666184 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java @@ -108,8 +108,4 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode { } - @Override - public void destroy() { - - } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java index bb086af15a..9c97f6f85b 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java @@ -15,8 +15,12 @@ */ package org.thingsboard.rule.engine.metadata; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Before; import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.TbContext; +import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.server.common.data.kv.Aggregation; import static org.hamcrest.CoreMatchers.is; @@ -24,14 +28,25 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.willCallRealMethod; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; public class TbGetTelemetryNodeTest { TbGetTelemetryNode node; + TbGetTelemetryNodeConfiguration config; + TbNodeConfiguration nodeConfiguration; + TbContext ctx; @Before public void setUp() throws Exception { - node = mock(TbGetTelemetryNode.class); + ctx = mock(TbContext.class); + node = spy(new TbGetTelemetryNode()); + config = new TbGetTelemetryNodeConfiguration(); + config.setFetchMode("ALL"); + ObjectMapper mapper = JacksonUtil.OBJECT_MAPPER; + nodeConfiguration = new TbNodeConfiguration(mapper.valueToTree(config)); + node.init(ctx, nodeConfiguration); + willCallRealMethod().given(node).parseAggregationConfig(any()); } diff --git a/tools/pom.xml b/tools/pom.xml index 1b1f99d44e..850485ed42 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT thingsboard tools diff --git a/tools/src/main/java/org/thingsboard/client/tools/migrator/DictionaryParser.java b/tools/src/main/java/org/thingsboard/client/tools/migrator/DictionaryParser.java index b6a1be84b1..2feaf7cd77 100644 --- a/tools/src/main/java/org/thingsboard/client/tools/migrator/DictionaryParser.java +++ b/tools/src/main/java/org/thingsboard/client/tools/migrator/DictionaryParser.java @@ -17,7 +17,7 @@ package org.thingsboard.client.tools.migrator; import org.apache.commons.io.FileUtils; import org.apache.commons.io.LineIterator; -import org.apache.commons.lang3.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import java.io.File; import java.io.IOException; diff --git a/tools/src/main/java/org/thingsboard/client/tools/migrator/PgCaMigrator.java b/tools/src/main/java/org/thingsboard/client/tools/migrator/PgCaMigrator.java index 695356ed2d..77fc0837ed 100644 --- a/tools/src/main/java/org/thingsboard/client/tools/migrator/PgCaMigrator.java +++ b/tools/src/main/java/org/thingsboard/client/tools/migrator/PgCaMigrator.java @@ -19,7 +19,7 @@ import com.google.common.collect.Lists; import org.apache.cassandra.io.sstable.CQLSSTableWriter; import org.apache.commons.io.FileUtils; import org.apache.commons.io.LineIterator; -import org.apache.commons.lang3.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import java.io.File; diff --git a/tools/src/main/java/org/thingsboard/client/tools/migrator/RelatedEntitiesParser.java b/tools/src/main/java/org/thingsboard/client/tools/migrator/RelatedEntitiesParser.java index b0f786a2d3..c84020b6b5 100644 --- a/tools/src/main/java/org/thingsboard/client/tools/migrator/RelatedEntitiesParser.java +++ b/tools/src/main/java/org/thingsboard/client/tools/migrator/RelatedEntitiesParser.java @@ -17,7 +17,7 @@ package org.thingsboard.client.tools.migrator; import org.apache.commons.io.FileUtils; import org.apache.commons.io.LineIterator; -import org.apache.commons.lang3.StringUtils; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.EntityType; import java.io.File; diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index 4235f1b1f5..dd151dc920 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index 11f4e91141..ffed38e79a 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -115,7 +115,7 @@ transport: key_password: "${COAP_DTLS_PEM_KEY_PASSWORD:server_key_password}" # Keystore server credentials keystore: - # Type of the key store + # Type of the key store (JKS or PKCS12) 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}" diff --git a/transport/http/pom.xml b/transport/http/pom.xml index 69ab0495c8..15a9305c76 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index 79efbd0a3d..01fe269739 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -39,7 +39,7 @@ server: key_password: "${SSL_PEM_KEY_PASSWORD:server_key_password}" # Keystore server credentials keystore: - # Type of the key store + # Type of the key store (JKS or PKCS12) type: "${SSL_KEY_STORE_TYPE:PKCS12}" # Path to the key store that holds the SSL certificate store_file: "${SSL_KEY_STORE:classpath:keystore/keystore.p12}" diff --git a/transport/lwm2m/pom.xml b/transport/lwm2m/pom.xml index 29fd3c6fb0..b2d7809f4d 100644 --- a/transport/lwm2m/pom.xml +++ b/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 0f28100385..33a8496941 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -129,7 +129,7 @@ transport: key_password: "${LWM2M_SERVER_PEM_KEY_PASSWORD:server_key_password}" # Keystore server credentials keystore: - # Type of the key store + # Type of the key store (JKS or PKCS12) 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}" @@ -165,7 +165,7 @@ transport: key_password: "${LWM2M_BS_PEM_KEY_PASSWORD:server_key_password}" # Keystore server credentials keystore: - # Type of the key store + # Type of the key store (JKS or PKCS12) 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}" @@ -188,7 +188,7 @@ transport: cert_file: "${LWM2M_TRUST_PEM_CERT:lwm2mtruststorechain.pem}" # Keystore with trust certificates keystore: - # Type of the key store + # Type of the key store (JKS or PKCS12) type: "${LWM2M_TRUST_KEY_STORE_TYPE:JKS}" # Path to the key store that holds the X509 certificates store_file: "${LWM2M_TRUST_KEY_STORE:lwm2mtruststorechain.jks}" diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index 77b4d2ad33..4115434335 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index 68a1f71f64..e45f5c29fc 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -125,7 +125,7 @@ transport: key_password: "${MQTT_SSL_PEM_KEY_PASSWORD:server_key_password}" # Keystore server credentials keystore: - # Type of the key store + # Type of the key store (JKS or PKCS12) 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}" diff --git a/transport/pom.xml b/transport/pom.xml index c114c031d2..d46c850c41 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT thingsboard transport diff --git a/transport/snmp/pom.xml b/transport/snmp/pom.xml index 7fefe9f49b..57cfd4d8f0 100644 --- a/transport/snmp/pom.xml +++ b/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT transport diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 44917dc47f..ea410747b2 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -1,6 +1,6 @@ { "name": "thingsboard", - "version": "3.4.1", + "version": "3.4.2", "scripts": { "ng": "ng", "start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --configuration development --host 0.0.0.0 --open", diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml index 5bf5867398..dade6f8c28 100644 --- a/ui-ngx/pom.xml +++ b/ui-ngx/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 3.4.1-SNAPSHOT + 3.4.2-SNAPSHOT thingsboard org.thingsboard diff --git a/ui-ngx/src/app/core/api/data-aggregator.ts b/ui-ngx/src/app/core/api/data-aggregator.ts index 7485a4bc87..6e77a0a161 100644 --- a/ui-ngx/src/app/core/api/data-aggregator.ts +++ b/ui-ngx/src/app/core/api/data-aggregator.ts @@ -14,7 +14,10 @@ /// limitations under the License. /// -import { SubscriptionData, SubscriptionDataHolder } from '@app/shared/models/telemetry/telemetry.models'; +import { + AggKey, + IndexedSubscriptionData, +} from '@app/shared/models/telemetry/telemetry.models'; import { AggregationType, calculateIntervalComparisonEndTime, @@ -25,10 +28,10 @@ import { SubscriptionTimewindow } from '@shared/models/time/time.models'; import { UtilsService } from '@core/services/utils.service'; -import { deepClone, isNumber, isNumeric } from '@core/utils'; +import { deepClone, isDefinedAndNotNull, isNumber, isNumeric } from '@core/utils'; import Timeout = NodeJS.Timeout; -export declare type onAggregatedData = (data: SubscriptionData, detectChanges: boolean) => void; +export declare type onAggregatedData = (data: IndexedSubscriptionData, detectChanges: boolean) => void; interface AggData { count: number; @@ -67,12 +70,12 @@ class AggDataMap { } class AggregationMap { - aggMap: {[key: string]: AggDataMap} = {}; + aggMap: {[id: number]: AggDataMap} = {}; detectRangeChanged(): boolean { let changed = false; - for (const key of Object.keys(this.aggMap)) { - const aggDataMap = this.aggMap[key]; + for (const id of Object.keys(this.aggMap)) { + const aggDataMap = this.aggMap[id]; if (aggDataMap.rangeChanged) { changed = true; aggDataMap.rangeChanged = false; @@ -82,8 +85,8 @@ class AggregationMap { } clearRangeChangedFlags() { - for (const key of Object.keys(this.aggMap)) { - this.aggMap[key].rangeChanged = false; + for (const id of Object.keys(this.aggMap)) { + this.aggMap[id].rangeChanged = false; } } } @@ -93,7 +96,7 @@ declare type AggFunction = (aggData: AggData, value?: any) => void; const avg: AggFunction = (aggData: AggData, value?: any) => { aggData.count++; if (isNumber(value)) { - aggData.sum += value; + aggData.sum = aggData.aggValue * (aggData.count - 1) + value; aggData.aggValue = aggData.sum / aggData.count; } else { aggData.aggValue = value; @@ -135,9 +138,25 @@ const none: AggFunction = (aggData: AggData, value?: any) => { export class DataAggregator { - private dataBuffer: SubscriptionData = {}; - private data: SubscriptionData; - private readonly lastPrevKvPairData: {[key: string]: [number, any]}; + constructor(private onDataCb: onAggregatedData, + private tsKeys: AggKey[], + private isLatestDataAgg: boolean, + private subsTw: SubscriptionTimewindow, + private utils: UtilsService, + private ignoreDataUpdateOnIntervalTick: boolean) { + this.tsKeys.forEach((key) => { + if (!this.dataBuffer[key.id]) { + this.dataBuffer[key.id] = []; + } + }); + if (this.subsTw.aggregation.stateData) { + this.lastPrevKvPairData = {}; + } + } + + private dataBuffer: IndexedSubscriptionData = []; + private data: IndexedSubscriptionData; + private readonly lastPrevKvPairData: {[id: number]: [number, any]}; private aggregationMap: AggregationMap; @@ -145,9 +164,7 @@ export class DataAggregator { private resetPending = false; private updatedData = false; - private noAggregation = this.subsTw.aggregation.type === AggregationType.NONE; - private aggregationTimeout = Math.max(this.subsTw.aggregation.interval, 1000); - private readonly aggFunction: AggFunction; + private aggregationTimeout = this.isLatestDataAgg ? 1000 : Math.max(this.subsTw.aggregation.interval, 1000); private intervalTimeoutHandle: Timeout; private intervalScheduledTime: number; @@ -156,38 +173,29 @@ export class DataAggregator { private endTs: number; private elapsed: number; - constructor(private onDataCb: onAggregatedData, - private tsKeyNames: string[], - private subsTw: SubscriptionTimewindow, - private utils: UtilsService, - private ignoreDataUpdateOnIntervalTick: boolean) { - this.tsKeyNames.forEach((key) => { - this.dataBuffer[key] = []; - }); - if (this.subsTw.aggregation.stateData) { - this.lastPrevKvPairData = {}; + private static convertValue(val: string, noAggregation: boolean): any { + if (val && isNumeric(val) && (!noAggregation || noAggregation && Number(val).toString() === val)) { + return Number(val); } - switch (this.subsTw.aggregation.type) { + return val; + } + + private static getAggFunction(aggType: AggregationType): AggFunction { + switch (aggType) { case AggregationType.MIN: - this.aggFunction = min; - break; + return min; case AggregationType.MAX: - this.aggFunction = max; - break; + return max; case AggregationType.AVG: - this.aggFunction = avg; - break; + return avg; case AggregationType.SUM: - this.aggFunction = sum; - break; + return sum; case AggregationType.COUNT: - this.aggFunction = count; - break; + return count; case AggregationType.NONE: - this.aggFunction = none; - break; + return none; default: - this.aggFunction = avg; + return avg; } } @@ -206,7 +214,7 @@ export class DataAggregator { this.intervalScheduledTime = this.utils.currentPerfTime(); this.calculateStartEndTs(); this.elapsed = 0; - this.aggregationTimeout = Math.max(this.subsTw.aggregation.interval, 1000); + this.aggregationTimeout = this.isLatestDataAgg ? 1000 : Math.max(this.subsTw.aggregation.interval, 1000); this.resetPending = true; this.updatedData = false; this.intervalTimeoutHandle = setTimeout(this.onInterval.bind(this), this.aggregationTimeout); @@ -220,7 +228,7 @@ export class DataAggregator { this.aggregationMap = null; } - public onData(data: SubscriptionDataHolder, update: boolean, history: boolean, detectChanges: boolean) { + public onData(data: IndexedSubscriptionData, update: boolean, history: boolean, detectChanges: boolean) { this.updatedData = true; if (!this.dataReceived || this.resetPending) { let updateIntervalScheduledTime = true; @@ -235,9 +243,9 @@ export class DataAggregator { } if (update) { this.aggregationMap = new AggregationMap(); - this.updateAggregatedData(data.data); + this.updateAggregatedData(data); } else { - this.aggregationMap = this.processAggregatedData(data.data); + this.aggregationMap = this.processAggregatedData(data); } if (updateIntervalScheduledTime) { this.intervalScheduledTime = this.utils.currentPerfTime(); @@ -245,7 +253,7 @@ export class DataAggregator { this.aggregationMap.clearRangeChangedFlags(); this.onInterval(history, detectChanges); } else { - this.updateAggregatedData(data.data); + this.updateAggregatedData(data); if (history) { this.intervalScheduledTime = this.utils.currentPerfTime(); this.onInterval(history, detectChanges); @@ -283,9 +291,9 @@ export class DataAggregator { } const intervalTimeout = rangeChanged ? this.aggregationTimeout - this.elapsed : this.aggregationTimeout; if (!history) { - const delta = Math.floor(this.elapsed / this.subsTw.aggregation.interval); + const delta = Math.floor(this.elapsed / this.aggregationTimeout); if (delta || !this.data || rangeChanged) { - const tickTs = delta * this.subsTw.aggregation.interval; + const tickTs = delta * this.aggregationTimeout; if (this.subsTw.quickInterval) { const startEndTime = calculateIntervalStartEndTime(this.subsTw.quickInterval, this.subsTw.timezone); this.startTs = startEndTime[0] + this.subsTw.tsOffset; @@ -295,7 +303,7 @@ export class DataAggregator { this.endTs += tickTs; } this.data = this.updateData(); - this.elapsed = this.elapsed - delta * this.subsTw.aggregation.interval; + this.elapsed = this.elapsed - delta * this.aggregationTimeout; } } else { this.data = this.updateData(); @@ -309,39 +317,45 @@ export class DataAggregator { } } - private updateData(): SubscriptionData { - this.tsKeyNames.forEach((key) => { - this.dataBuffer[key] = []; + private updateData(): IndexedSubscriptionData { + this.dataBuffer = []; + this.tsKeys.forEach((key) => { + if (!this.dataBuffer[key.id]) { + this.dataBuffer[key.id] = []; + } }); - for (const key of Object.keys(this.aggregationMap.aggMap)) { - const aggKeyData = this.aggregationMap.aggMap[key]; - let keyData = this.dataBuffer[key]; + for (const idStr of Object.keys(this.aggregationMap.aggMap)) { + const id = Number(idStr); + const aggKeyData = this.aggregationMap.aggMap[id]; + const aggKey = this.aggKeyById(id); + const noAggregation = aggKey.agg === AggregationType.NONE; + let keyData = this.dataBuffer[id]; aggKeyData.forEach((aggData, aggTimestamp) => { if (aggTimestamp < this.startTs) { if (this.subsTw.aggregation.stateData && - (!this.lastPrevKvPairData[key] || this.lastPrevKvPairData[key][0] < aggTimestamp)) { - this.lastPrevKvPairData[key] = [aggTimestamp, aggData.aggValue]; + (!this.lastPrevKvPairData[id] || this.lastPrevKvPairData[id][0] < aggTimestamp)) { + this.lastPrevKvPairData[id] = [aggTimestamp, aggData.aggValue]; } aggKeyData.delete(aggTimestamp); this.updatedData = true; - } else if (aggTimestamp < this.endTs || this.noAggregation) { + } else if (aggTimestamp < this.endTs || noAggregation) { const kvPair: [number, any] = [aggTimestamp, aggData.aggValue]; keyData.push(kvPair); } }); keyData.sort((set1, set2) => set1[0] - set2[0]); if (this.subsTw.aggregation.stateData) { - this.updateStateBounds(keyData, deepClone(this.lastPrevKvPairData[key])); + this.updateStateBounds(keyData, deepClone(this.lastPrevKvPairData[id])); } if (keyData.length > this.subsTw.aggregation.limit) { keyData = keyData.slice(keyData.length - this.subsTw.aggregation.limit); } - this.dataBuffer[key] = keyData; + this.dataBuffer[id] = keyData; } return this.dataBuffer; } - private updateStateBounds(keyData: [number, any][], lastPrevKvPair: [number, any]) { + private updateStateBounds(keyData: [number, any, number?][], lastPrevKvPair: [number, any]) { if (lastPrevKvPair) { lastPrevKvPair[0] = this.startTs; } @@ -369,66 +383,71 @@ export class DataAggregator { } } - private processAggregatedData(data: SubscriptionData): AggregationMap { - const isCount = this.subsTw.aggregation.type === AggregationType.COUNT; + private processAggregatedData(data: IndexedSubscriptionData): AggregationMap { const aggregationMap = new AggregationMap(); - for (const key of Object.keys(data)) { - let aggKeyData = aggregationMap.aggMap[key]; + for (const idStr of Object.keys(data)) { + const id = Number(idStr); + const aggKey = this.aggKeyById(id); + const aggType = aggKey.agg; + const isCount = aggType === AggregationType.COUNT; + const noAggregation = aggType === AggregationType.NONE; + let aggKeyData = aggregationMap.aggMap[id]; if (!aggKeyData) { aggKeyData = new AggDataMap(); - aggregationMap.aggMap[key] = aggKeyData; + aggregationMap.aggMap[id] = aggKeyData; } - const keyData = data[key]; + const keyData = data[id]; keyData.forEach((kvPair) => { const timestamp = kvPair[0]; - const value = this.convertValue(kvPair[1]); - const aggKey = timestamp; + const value = DataAggregator.convertValue(kvPair[1], noAggregation); + const tsKey = timestamp; const aggData = { - count: isCount ? value : 1, + count: isCount ? value : isDefinedAndNotNull(kvPair[2]) ? kvPair[2] : 1, sum: value, aggValue: value }; - aggKeyData.set(aggKey, aggData); + aggKeyData.set(tsKey, aggData); }); } return aggregationMap; } - private updateAggregatedData(data: SubscriptionData) { - const isCount = this.subsTw.aggregation.type === AggregationType.COUNT; - for (const key of Object.keys(data)) { - let aggKeyData = this.aggregationMap.aggMap[key]; + private updateAggregatedData(data: IndexedSubscriptionData) { + for (const idStr of Object.keys(data)) { + const id = Number(idStr); + const aggKey = this.aggKeyById(id); + const aggType = aggKey.agg; + const isCount = aggType === AggregationType.COUNT; + const noAggregation = aggType === AggregationType.NONE; + let aggKeyData = this.aggregationMap.aggMap[id]; if (!aggKeyData) { aggKeyData = new AggDataMap(); - this.aggregationMap.aggMap[key] = aggKeyData; + this.aggregationMap.aggMap[id] = aggKeyData; } - const keyData = data[key]; + const keyData = data[id]; keyData.forEach((kvPair) => { const timestamp = kvPair[0]; - const value = this.convertValue(kvPair[1]); - const aggTimestamp = this.noAggregation ? timestamp : (this.startTs + + const value = DataAggregator.convertValue(kvPair[1], noAggregation); + const aggTimestamp = noAggregation ? timestamp : (this.startTs + Math.floor((timestamp - this.startTs) / this.subsTw.aggregation.interval) * this.subsTw.aggregation.interval + this.subsTw.aggregation.interval / 2); let aggData = aggKeyData.get(aggTimestamp); if (!aggData) { aggData = { - count: 1, + count: isDefinedAndNotNull(kvPair[2]) ? kvPair[2] : 1, sum: value, aggValue: isCount ? 1 : value }; aggKeyData.set(aggTimestamp, aggData); } else { - this.aggFunction(aggData, value); + DataAggregator.getAggFunction(aggType)(aggData, value); } }); } } - private convertValue(val: string): any { - if (val && isNumeric(val) && (!this.noAggregation || this.noAggregation && Number(val).toString() === val)) { - return Number(val); - } - return val; + private aggKeyById(id: number): AggKey { + return this.tsKeys.find(key => key.id === id); } } diff --git a/ui-ngx/src/app/core/api/entity-data-subscription.ts b/ui-ngx/src/app/core/api/entity-data-subscription.ts index 9cffe57ae7..4dc0e7c4e1 100644 --- a/ui-ngx/src/app/core/api/entity-data-subscription.ts +++ b/ui-ngx/src/app/core/api/entity-data-subscription.ts @@ -14,9 +14,16 @@ /// limitations under the License. /// -import { DataSet, DataSetHolder, DatasourceType, widgetType } from '@shared/models/widget.models'; -import { AggregationType, getCurrentTime, SubscriptionTimewindow } from '@shared/models/time/time.models'; +import { ComparisonResultType, DataSet, DataSetHolder, DatasourceType, widgetType } from '@shared/models/widget.models'; import { + AggregationType, + ComparisonDuration, + createTimewindowForComparison, + getCurrentTime, + SubscriptionTimewindow +} from '@shared/models/time/time.models'; +import { + ComparisonTsValue, EntityData, EntityDataPageLink, EntityFilter, @@ -28,11 +35,12 @@ import { TsValue } from '@shared/models/query/query.models'; import { + AggKey, DataKeyType, EntityCountCmd, EntityDataCmd, + IndexedSubscriptionData, SubscriptionData, - SubscriptionDataHolder, TelemetryService, TelemetrySubscriber } from '@shared/models/telemetry/telemetry.models'; @@ -55,6 +63,11 @@ declare type DataUpdatedCb = (data: DataSetHolder, dataIndex: number, export interface SubscriptionDataKey { name: string; type: DataKeyType; + aggregationType?: AggregationType; + comparisonEnabled?: boolean; + timeForComparison?: ComparisonDuration; + comparisonCustomIntervalValue?: number; + comparisonResultType?: ComparisonResultType; funcBody: string; func?: DataKeyFunction; postFuncBody: string; @@ -82,9 +95,16 @@ export interface EntityDataSubscriptionOptions { export class EntityDataSubscription { + constructor(private listener: EntityDataListener, + private telemetryService: TelemetryService, + private utils: UtilsService) { + this.initializeSubscription(); + } + private entityDataSubscriptionOptions = this.listener.subscriptionOptions; private datasourceType: DatasourceType = this.entityDataSubscriptionOptions.datasourceType; private history: boolean; + private isFloatingTimewindow: boolean; private realtime: boolean; private subscriber: TelemetrySubscriber; @@ -95,13 +115,18 @@ export class EntityDataSubscription { private attrFields: Array; private tsFields: Array; private latestValues: Array; + private aggTsValues: Array; + private aggTsComparisonValues: Array; private entityDataResolveSubject: Subject; private pageData: PageData; + private data: Array>; private subsTw: SubscriptionTimewindow; private latestTsOffset: number; private dataAggregators: Array; + private tsLatestDataAggregators: Array; private dataKeys: {[key: string]: Array | SubscriptionDataKey} = {}; + private dataKeysList: SubscriptionDataKey[] = []; private datasourceData: {[index: number]: {[key: string]: DataSetHolder}}; private datasourceOrigData: {[index: number]: {[key: string]: DataSetHolder}}; private entityIdToDataIndex: {[id: string]: number}; @@ -116,15 +141,44 @@ export class EntityDataSubscription { private dataResolved = false; private started = false; - constructor(private listener: EntityDataListener, - private telemetryService: TelemetryService, - private utils: UtilsService) { - this.initializeSubscription(); + private static convertValue(val: string): any { + if (val && isNumeric(val) && Number(val).toString() === val) { + return Number(val); + } + return val; + } + + private static calculateComparisonValue(key: SubscriptionDataKey, comparisonTsValue: ComparisonTsValue): [number, any, number?][] { + let timestamp: number; + let value: any; + switch (key.comparisonResultType) { + case ComparisonResultType.PREVIOUS_VALUE: + timestamp = comparisonTsValue.previous.ts; + value = comparisonTsValue.previous.value; + break; + case ComparisonResultType.DELTA_ABSOLUTE: + case ComparisonResultType.DELTA_PERCENT: + timestamp = comparisonTsValue.previous.ts; + const currentVal = EntityDataSubscription.convertValue(comparisonTsValue.current.value); + const prevVal = EntityDataSubscription.convertValue(comparisonTsValue.previous.value); + if (isNumeric(currentVal) && isNumeric(prevVal)) { + if (key.comparisonResultType === ComparisonResultType.DELTA_ABSOLUTE) { + value = currentVal - prevVal; + } else { + value = (currentVal - prevVal) / prevVal * 100; + } + } else { + value = ''; + } + break; + } + return [[timestamp, value]]; } private initializeSubscription() { for (let i = 0; i < this.entityDataSubscriptionOptions.dataKeys.length; i++) { const dataKey = deepClone(this.entityDataSubscriptionOptions.dataKeys[i]); + this.dataKeysList.push(dataKey); dataKey.index = i; if (this.datasourceType === DatasourceType.function) { if (!dataKey.func) { @@ -142,7 +196,8 @@ export class EntityDataSubscription { if (this.datasourceType === DatasourceType.function) { key = `${dataKey.name}_${dataKey.index}_${dataKey.type}${dataKey.latest ? '_latest' : ''}`; } else { - key = `${dataKey.name}_${dataKey.type}${dataKey.latest ? '_latest' : ''}`; + const keyIndexSuffix = dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE ? `_${dataKey.index}` : ''; + key = `${dataKey.name}_${dataKey.type}${keyIndexSuffix}${dataKey.latest ? '_latest' : ''}`; } let dataKeysList = this.dataKeys[key] as Array; if (!dataKeysList) { @@ -180,6 +235,12 @@ export class EntityDataSubscription { }); this.dataAggregators = null; } + if (this.tsLatestDataAggregators) { + this.tsLatestDataAggregators.forEach((aggregator) => { + aggregator.destroy(); + }); + this.tsLatestDataAggregators = null; + } this.pageData = null; } @@ -188,16 +249,11 @@ export class EntityDataSubscription { if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { this.started = true; this.dataResolved = true; - this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow; - this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset; - this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow && - isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow); - this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow && - isDefinedAndNotNull(this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs); + this.prepareSubscriptionTimewindow(); } if (this.datasourceType === DatasourceType.entity) { const entityFields: Array = - this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.entityField).map( + this.dataKeysList.filter(dataKey => dataKey.type === DataKeyType.entityField).map( dataKey => ({ type: EntityKeyType.ENTITY_FIELD, key: dataKey.name }) ); if (!entityFields.find(key => key.key === 'name')) { @@ -219,18 +275,20 @@ export class EntityDataSubscription { }); } - this.attrFields = this.entityDataSubscriptionOptions.dataKeys.filter(dataKey => dataKey.type === DataKeyType.attribute).map( + this.attrFields = this.dataKeysList.filter(dataKey => dataKey.type === DataKeyType.attribute).map( dataKey => ({ type: EntityKeyType.ATTRIBUTE, key: dataKey.name }) ); - this.tsFields = this.entityDataSubscriptionOptions.dataKeys. - filter(dataKey => dataKey.type === DataKeyType.timeseries && !dataKey.latest).map( + this.tsFields = this.dataKeysList. + filter(dataKey => dataKey.type === DataKeyType.timeseries && + (!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE) && !dataKey.latest).map( dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name }) ); if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { - const latestTsFields = this.entityDataSubscriptionOptions.dataKeys. - filter(dataKey => dataKey.type === DataKeyType.timeseries && dataKey.latest).map( + const latestTsFields = this.dataKeysList. + filter(dataKey => dataKey.type === DataKeyType.timeseries && dataKey.latest && + (!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE)).map( dataKey => ({ type: EntityKeyType.TIME_SERIES, key: dataKey.name }) ); this.latestValues = this.attrFields.concat(latestTsFields); @@ -238,6 +296,19 @@ export class EntityDataSubscription { this.latestValues = this.attrFields.concat(this.tsFields); } + this.aggTsValues = this.dataKeysList. + filter(dataKey => dataKey.type === DataKeyType.timeseries && + dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE && !dataKey.comparisonEnabled).map( + dataKey => ({ id: dataKey.index, key: dataKey.name, agg: dataKey.aggregationType }) + ); + + this.aggTsComparisonValues = this.dataKeysList. + filter(dataKey => dataKey.type === DataKeyType.timeseries && + dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE && dataKey.comparisonEnabled).map( + dataKey => ({ id: dataKey.index, key: dataKey.name, agg: dataKey.aggregationType, + previousValueOnly: dataKey.comparisonResultType === ComparisonResultType.PREVIOUS_VALUE }) + ); + this.subscriber = new TelemetrySubscriber(this.telemetryService); this.dataCommand = new EntityDataCmd(); @@ -282,19 +353,28 @@ export class EntityDataSubscription { this.subscriber.reconnect$.subscribe(() => { if (this.started) { const targetCommand = this.entityDataSubscriptionOptions.isPaginatedDataSubscription ? this.dataCommand : this.subsCommand; - if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && - !this.history && this.tsFields.length) { + if (!this.history && (this.entityDataSubscriptionOptions.type === widgetType.timeseries && this.tsFields.length || + this.aggTsValues.length > 0 && !this.isFloatingTimewindow)) { const newSubsTw = this.listener.updateRealtimeSubscription(); this.subsTw = newSubsTw; - targetCommand.tsCmd.startTs = this.subsTw.startTs; - targetCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow; - targetCommand.tsCmd.interval = this.subsTw.aggregation.interval; - targetCommand.tsCmd.limit = this.subsTw.aggregation.limit; - targetCommand.tsCmd.agg = this.subsTw.aggregation.type; - targetCommand.tsCmd.fetchLatestPreviousPoint = this.subsTw.aggregation.stateData; - this.dataAggregators.forEach((dataAggregator) => { - dataAggregator.reset(newSubsTw); - }); + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && this.tsFields.length) { + targetCommand.tsCmd.startTs = this.subsTw.startTs; + targetCommand.tsCmd.timeWindow = this.subsTw.aggregation.timeWindow; + targetCommand.tsCmd.interval = this.subsTw.aggregation.interval; + targetCommand.tsCmd.limit = this.subsTw.aggregation.limit; + targetCommand.tsCmd.agg = this.subsTw.aggregation.type; + targetCommand.tsCmd.fetchLatestPreviousPoint = this.subsTw.aggregation.stateData; + this.dataAggregators.forEach((dataAggregator) => { + dataAggregator.reset(newSubsTw); + }); + } + if (this.aggTsValues.length > 0 && !this.isFloatingTimewindow) { + targetCommand.aggTsCmd.startTs = this.subsTw.startTs; + targetCommand.aggTsCmd.timeWindow = this.subsTw.aggregation.timeWindow; + this.tsLatestDataAggregators.forEach((dataAggregator) => { + dataAggregator.reset(newSubsTw); + }); + } } if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { this.subscriber.setTsOffset(this.subsTw.tsOffset); @@ -359,7 +439,7 @@ export class EntityDataSubscription { entityType: null }; - const countKey = this.entityDataSubscriptionOptions.dataKeys[0]; + const countKey = this.dataKeysList[0]; let dataReceived = false; @@ -422,14 +502,9 @@ export class EntityDataSubscription { if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { return; } - this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow; - this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset; - this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow && - isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow); - this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow && - isDefinedAndNotNull(this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs); + this.prepareSubscriptionTimewindow(); - this.prepareData(); + this.prepareData(true); if (this.datasourceType === DatasourceType.entity) { this.subsCommand = new EntityDataCmd(); @@ -461,7 +536,19 @@ export class EntityDataSubscription { this.started = true; } + private prepareSubscriptionTimewindow() { + this.subsTw = this.entityDataSubscriptionOptions.subscriptionTimewindow; + this.latestTsOffset = this.entityDataSubscriptionOptions.latestTsOffset; + this.history = this.entityDataSubscriptionOptions.subscriptionTimewindow && + isObject(this.entityDataSubscriptionOptions.subscriptionTimewindow.fixedWindow); + this.realtime = this.entityDataSubscriptionOptions.subscriptionTimewindow && + isDefinedAndNotNull(this.entityDataSubscriptionOptions.subscriptionTimewindow.realtimeWindowMs); + this.isFloatingTimewindow = this.entityDataSubscriptionOptions.subscriptionTimewindow && + !this.entityDataSubscriptionOptions.subscriptionTimewindow.quickInterval && !this.history; + } + private prepareSubscriptionCommands(cmd: EntityDataCmd) { + let latestValuesKeys: EntityKey[] = []; if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { if (this.tsFields.length > 0) { if (this.history) { @@ -486,17 +573,40 @@ export class EntityDataSubscription { }; } } - if (this.latestValues.length > 0) { - cmd.latestCmd = { - keys: this.latestValues - }; - } + latestValuesKeys = this.latestValues; } else if (this.entityDataSubscriptionOptions.type === widgetType.latest) { - if (this.latestValues.length > 0) { - cmd.latestCmd = { - keys: this.latestValues - }; + latestValuesKeys = this.latestValues; + } + if (this.history && (this.aggTsValues.length > 0 || this.aggTsComparisonValues.length > 0)) { + for (const aggTsComparison of this.aggTsComparisonValues) { + const subscriptionDataKey = this.dataKeyByIndex(aggTsComparison.id); + const timewindowForComparison = + createTimewindowForComparison(this.subsTw, subscriptionDataKey.timeForComparison, + subscriptionDataKey.comparisonCustomIntervalValue); + aggTsComparison.previousStartTs = timewindowForComparison.fixedWindow.startTimeMs; + aggTsComparison.previousEndTs = timewindowForComparison.fixedWindow.endTimeMs; } + cmd.aggHistoryCmd = { + keys: [...this.aggTsValues, ...this.aggTsComparisonValues], + startTs: this.subsTw.fixedWindow.startTimeMs, + endTs: this.subsTw.fixedWindow.endTimeMs + }; + } else if (!this.isFloatingTimewindow && this.aggTsValues.length > 0) { + cmd.aggTsCmd = { + keys: this.aggTsValues, + startTs: this.subsTw.startTs, + timeWindow: this.subsTw.aggregation.timeWindow + }; + if (latestValuesKeys.length > 0) { + const tsKeys = this.aggTsValues.map(key => key.key); + latestValuesKeys = latestValuesKeys.filter(latestKey => latestKey.type !== EntityKeyType.TIME_SERIES + || !tsKeys.includes(latestKey.key)); + } + } + if (latestValuesKeys.length > 0) { + cmd.latestCmd = { + keys: latestValuesKeys + }; } } @@ -510,7 +620,7 @@ export class EntityDataSubscription { this.generateData(true); } - private prepareData() { + private prepareData(isUpdate: boolean) { if (this.timeseriesTimer) { clearTimeout(this.timeseriesTimer); this.timeseriesTimer = null; @@ -526,37 +636,73 @@ export class EntityDataSubscription { }); } this.dataAggregators = []; + if (this.tsLatestDataAggregators) { + this.tsLatestDataAggregators.forEach((aggregator) => { + aggregator.destroy(); + }); + } + this.tsLatestDataAggregators = []; this.resetData(); if (this.entityDataSubscriptionOptions.type === widgetType.timeseries) { - let tsKeyNames = []; + let tsKeyIds: number[]; if (this.datasourceType === DatasourceType.function) { - for (const key of Object.keys(this.dataKeys)) { - const dataKeysList = this.dataKeys[key] as Array; - dataKeysList.forEach((subscriptionDataKey) => { - if (!subscriptionDataKey.latest) { - tsKeyNames.push(`${subscriptionDataKey.name}_${subscriptionDataKey.index}`); - } - }); - } + tsKeyIds = this.dataKeysList.filter(key => !key.latest).map(key => key.index); } else { - tsKeyNames = this.tsFields ? this.tsFields.map(field => field.key) : []; + tsKeyIds = this.dataKeysList. + filter(dataKey => dataKey.type === DataKeyType.timeseries && + (!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE) && !dataKey.latest).map( + dataKey => dataKey.index + ); } - for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) { - if (tsKeyNames.length) { - if (this.datasourceType === DatasourceType.function) { - this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames, - DataKeyType.function, dataIndex, this.notifyListener.bind(this)); - } else { - this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, tsKeyNames, - DataKeyType.timeseries, dataIndex, this.notifyListener.bind(this)); - } + const aggKeys: AggKey[] = tsKeyIds.map(key => ({id: key, key: key + '', agg: this.subsTw.aggregation.type})); + if (aggKeys.length) { + for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) { + this.dataAggregators[dataIndex] = this.createRealtimeDataAggregator(this.subsTw, aggKeys, + false, dataIndex, this.notifyListener.bind(this)); } } } + if (this.aggTsValues && this.aggTsValues.length) { + if (!this.isFloatingTimewindow) { + const aggLatestTimewindow = deepClone(this.subsTw); + aggLatestTimewindow.aggregation.stateData = false; + aggLatestTimewindow.aggregation.interval = aggLatestTimewindow.aggregation.timeWindow; + for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) { + this.tsLatestDataAggregators[dataIndex] = this.createRealtimeDataAggregator(aggLatestTimewindow, this.aggTsValues, + true, dataIndex, this.notifyListener.bind(this)); + } + } else { + this.reportNotSupported(this.aggTsValues, isUpdate); + } + } + if (!this.history && this.aggTsComparisonValues && this.aggTsComparisonValues.length) { + this.reportNotSupported(this.aggTsComparisonValues, isUpdate); + } + } + + private reportNotSupported(keys: AggKey[], isUpdate: boolean) { + const indexedData: IndexedSubscriptionData = []; + for (const key of keys) { + indexedData[key.id] = [[0, 'Not supported!']]; + } + for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) { + this.onIndexedData(indexedData, dataIndex, true, + this.entityDataSubscriptionOptions.type === widgetType.timeseries, + (data, dataIndex1, dataKeyIndex, detectChanges, isLatest) => { + if (!this.data[dataIndex1]) { + this.data[dataIndex1] = []; + } + this.data[dataIndex1][dataKeyIndex] = data; + if (isUpdate) { + this.notifyListener(data, dataIndex1, dataKeyIndex, detectChanges, isLatest); + } + }); + } } private resetData() { + this.data = []; this.datasourceData = []; this.entityIdToDataIndex = {}; for (let dataIndex = 0; dataIndex < this.pageData.data.length; dataIndex++) { @@ -609,19 +755,18 @@ export class EntityDataSubscription { this.pageData = pageData; if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { - this.prepareData(); + this.prepareData(false); } else if (isInitialData) { this.resetData(); } - const data: Array> = []; for (let dataIndex = 0; dataIndex < pageData.data.length; dataIndex++) { const entityData = pageData.data[dataIndex]; this.processEntityData(entityData, dataIndex, false, (data1, dataIndex1, dataKeyIndex) => { - if (!data[dataIndex1]) { - data[dataIndex1] = []; + if (!this.data[dataIndex1]) { + this.data[dataIndex1] = []; } - data[dataIndex1][dataKeyIndex] = data1; + this.data[dataIndex1][dataKeyIndex] = data1; } ); } @@ -630,7 +775,7 @@ export class EntityDataSubscription { this.entityDataResolveSubject.next( { pageData, - data, + data: this.data, datasourceIndex: this.listener.configDatasourceIndex, pageLink: this.entityDataSubscriptionOptions.pageLink } @@ -638,7 +783,7 @@ export class EntityDataSubscription { this.entityDataResolveSubject.complete(); } else { if (isInitialData || this.entityDataSubscriptionOptions.isPaginatedDataSubscription) { - this.listener.dataLoaded(pageData, data, + this.listener.dataLoaded(pageData, this.data, this.listener.configDatasourceIndex, this.entityDataSubscriptionOptions.pageLink); } if (this.entityDataSubscriptionOptions.isPaginatedDataSubscription && isInitialData) { @@ -648,7 +793,7 @@ export class EntityDataSubscription { this.entityDataResolveSubject.next( { pageData, - data, + data: this.data, datasourceIndex: this.listener.configDatasourceIndex, pageLink: this.entityDataSubscriptionOptions.pageLink } @@ -675,27 +820,85 @@ export class EntityDataSubscription { private processEntityData(entityData: EntityData, dataIndex: number, isUpdate: boolean, dataUpdatedCb: DataUpdatedCb) { - if ((this.entityDataSubscriptionOptions.type === widgetType.latest || - this.entityDataSubscriptionOptions.type === widgetType.timeseries) && entityData.latest) { - for (const type of Object.keys(entityData.latest)) { - const subscriptionData = this.toSubscriptionData(entityData.latest[type], false); - const dataKeyType = entityKeyTypeToDataKeyType(EntityKeyType[type]); - this.onData(subscriptionData, dataKeyType, dataIndex, true, - this.entityDataSubscriptionOptions.type === widgetType.timeseries, dataUpdatedCb); + if (this.entityDataSubscriptionOptions.type === widgetType.latest || + this.entityDataSubscriptionOptions.type === widgetType.timeseries) { + if (entityData.aggLatest) { + const aggData: IndexedSubscriptionData = []; + for (const idStr of Object.keys(entityData.aggLatest)) { + const id = Number(idStr); + const dataKey = this.dataKeyByIndex(id); + const aggLatestData = entityData.aggLatest[id]; + if (dataKey.comparisonEnabled) { + const keyData = EntityDataSubscription.calculateComparisonValue(dataKey, aggLatestData); + this.onKeyData(keyData, dataKey.name, id, dataKey.type, dataIndex, true, + this.entityDataSubscriptionOptions.type === widgetType.timeseries, true, dataUpdatedCb); + } else { + aggData[id] = [[aggLatestData.current.ts, aggLatestData.current.value, aggLatestData.current.count]]; + } + } + if (Object.keys(aggData).length > 0 && this.tsLatestDataAggregators && this.tsLatestDataAggregators[dataIndex]) { + const dataAggregator = this.tsLatestDataAggregators[dataIndex]; + let prevDataCb; + if (!isUpdate) { + prevDataCb = dataAggregator.updateOnDataCb((data, detectChanges) => { + this.onIndexedData(data, dataIndex, detectChanges, + this.entityDataSubscriptionOptions.type === widgetType.timeseries, dataUpdatedCb); + }); + } + dataAggregator.onData(aggData, false, this.history, true); + if (prevDataCb) { + dataAggregator.updateOnDataCb(prevDataCb); + } + } + } + if (entityData.latest) { + for (const type of Object.keys(entityData.latest)) { + const subscriptionData = this.toSubscriptionData(entityData.latest[type], false); + const dataKeyType = entityKeyTypeToDataKeyType(EntityKeyType[type]); + if (isUpdate && EntityKeyType[type] === EntityKeyType.TIME_SERIES) { + const keys: string[] = Object.keys(subscriptionData); + const latestTsKeys = this.latestValues.filter(key => key.type === EntityKeyType.TIME_SERIES && keys.includes(key.key)); + if (latestTsKeys.length) { + const latestTsSubsciptionData: SubscriptionData = {}; + for (const latestTsKey of latestTsKeys) { + latestTsSubsciptionData[latestTsKey.key] = subscriptionData[latestTsKey.key]; + } + this.onData(latestTsSubsciptionData, dataKeyType, dataIndex, true, + this.entityDataSubscriptionOptions.type === widgetType.timeseries, dataUpdatedCb); + } + const aggTsKeys = this.aggTsValues.filter(key => keys.includes(key.key)); + if (!this.history && aggTsKeys.length && this.tsLatestDataAggregators && this.tsLatestDataAggregators[dataIndex]) { + const dataAggregator = this.tsLatestDataAggregators[dataIndex]; + const indexedData: IndexedSubscriptionData = []; + for (const aggKey of aggTsKeys) { + indexedData[aggKey.id] = subscriptionData[aggKey.key]; + } + dataAggregator.onData(indexedData, true, false, true); + } + } else { + this.onData(subscriptionData, dataKeyType, dataIndex, true, + this.entityDataSubscriptionOptions.type === widgetType.timeseries, dataUpdatedCb); + } + } } } if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && entityData.timeseries) { const subscriptionData = this.toSubscriptionData(entityData.timeseries, true); if (this.dataAggregators && this.dataAggregators[dataIndex]) { const dataAggregator = this.dataAggregators[dataIndex]; + const keyNames = Object.keys(subscriptionData); + const dataKeys = this.timeseriesDataKeysByKeyNames(keyNames); + const indexedData: IndexedSubscriptionData = []; + for (const dataKey of dataKeys) { + indexedData[dataKey.index] = subscriptionData[dataKey.name]; + } let prevDataCb; if (!isUpdate) { prevDataCb = dataAggregator.updateOnDataCb((data, detectChanges) => { - this.onData(data, this.datasourceType === DatasourceType.function ? - DataKeyType.function : DataKeyType.timeseries, dataIndex, detectChanges, false, dataUpdatedCb); + this.onIndexedData(data, dataIndex, detectChanges, false, dataUpdatedCb); }); } - dataAggregator.onData({data: subscriptionData}, false, this.history, true); + dataAggregator.onData(indexedData, false, this.history, true); if (prevDataCb) { dataAggregator.updateOnDataCb(prevDataCb); } @@ -707,92 +910,109 @@ export class EntityDataSubscription { private onData(sourceData: SubscriptionData, type: DataKeyType, dataIndex: number, detectChanges: boolean, isTsLatest: boolean, dataUpdatedCb: DataUpdatedCb) { - for (const keyName of Object.keys(sourceData)) { - const keyData = sourceData[keyName]; - const key = `${keyName}_${type}${isTsLatest ? '_latest' : ''}`; - const dataKeyList = this.dataKeys[key] as Array; - for (let keyIndex = 0; dataKeyList && keyIndex < dataKeyList.length; keyIndex++) { - const datasourceKey = `${key}_${keyIndex}`; - if (this.datasourceData[dataIndex][datasourceKey].data) { - const dataKey = dataKeyList[keyIndex]; - const data: DataSet = []; - let prevSeries: [number, any]; - let prevOrigSeries: [number, any]; - let datasourceKeyData: DataSet; - let datasourceOrigKeyData: DataSet; - let update = false; - if (this.realtime && !isTsLatest) { - datasourceKeyData = []; - datasourceOrigKeyData = []; - } else { - datasourceKeyData = this.datasourceData[dataIndex][datasourceKey].data; - datasourceOrigKeyData = this.datasourceOrigData[dataIndex][datasourceKey].data; - } - if (datasourceKeyData.length > 0) { - prevSeries = datasourceKeyData[datasourceKeyData.length - 1]; - prevOrigSeries = datasourceOrigKeyData[datasourceOrigKeyData.length - 1]; - } else { - prevSeries = [0, 0]; - prevOrigSeries = [0, 0]; - } - this.datasourceOrigData[dataIndex][datasourceKey].data = []; - if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && !isTsLatest) { - keyData.forEach((keySeries) => { - let series = keySeries; - const time = series[0]; - this.datasourceOrigData[dataIndex][datasourceKey].data.push(series); - let value = this.convertValue(series[1]); - if (dataKey.postFunc) { - value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]); - } - prevOrigSeries = series; - series = [time, value]; - data.push(series); - prevSeries = series; - }); - update = true; - } else if (this.entityDataSubscriptionOptions.type === widgetType.latest || isTsLatest) { - if (keyData.length > 0) { - let series = keyData[0]; - const time = series[0]; - this.datasourceOrigData[dataIndex][datasourceKey].data.push(series); - let value = this.convertValue(series[1]); - if (dataKey.postFunc) { - value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]); - } - series = [time, value]; - data.push(series); - } - update = true; - } - if (update) { - this.datasourceData[dataIndex][datasourceKey].data = data; - dataUpdatedCb(this.datasourceData[dataIndex][datasourceKey], dataIndex, dataKey.index, detectChanges, isTsLatest); - } - } + for (const key of Object.keys(sourceData)) { + const keyData = sourceData[key]; + this.onKeyData(keyData, key, 0, type, + dataIndex, detectChanges, isTsLatest, false, dataUpdatedCb); + } + } + + private onIndexedData(sourceData: IndexedSubscriptionData, dataIndex: number, detectChanges: boolean, + isTsLatest: boolean, dataUpdatedCb: DataUpdatedCb) { + for (const indexStr of Object.keys(sourceData)) { + const id = Number(indexStr); + const dataKey = this.dataKeyByIndex(id); + const isAggLatest = dataKey.aggregationType && dataKey.aggregationType !== AggregationType.NONE; + const keyData = sourceData[id]; + let keyName = dataKey.name; + if (dataKey.type === DataKeyType.function) { + keyName += `_${dataKey.index}`; } + this.onKeyData(keyData, keyName, id, dataKey.type, + dataIndex, detectChanges, isTsLatest, isAggLatest, dataUpdatedCb); } } - private convertValue(val: string): any { - if (val && isNumeric(val) && Number(val).toString() === val) { - return Number(val); + private onKeyData(keyData: [number, any, number?][], keyName: string, id: number, type: DataKeyType, + dataIndex: number, detectChanges: boolean, + isTsLatest: boolean, isAggLatest: boolean, dataUpdatedCb: DataUpdatedCb) { + const keyIdSuffix = isAggLatest ? `_${id}` : ''; + const key = `${keyName}_${type}${keyIdSuffix}${isTsLatest ? '_latest' : ''}`; + const dataKeyList = this.dataKeys[key] as Array; + for (let keyIndex = 0; dataKeyList && keyIndex < dataKeyList.length; keyIndex++) { + const datasourceKey = `${key}_${keyIndex}`; + if (this.datasourceData[dataIndex][datasourceKey].data) { + const dataKey = dataKeyList[keyIndex]; + const data: DataSet = []; + let prevSeries: [number, any]; + let prevOrigSeries: [number, any]; + let datasourceKeyData: DataSet; + let datasourceOrigKeyData: DataSet; + let update = false; + if (this.realtime && !isTsLatest) { + datasourceKeyData = []; + datasourceOrigKeyData = []; + } else { + datasourceKeyData = this.datasourceData[dataIndex][datasourceKey].data; + datasourceOrigKeyData = this.datasourceOrigData[dataIndex][datasourceKey].data; + } + if (datasourceKeyData.length > 0) { + prevSeries = datasourceKeyData[datasourceKeyData.length - 1]; + prevOrigSeries = datasourceOrigKeyData[datasourceOrigKeyData.length - 1]; + } else { + prevSeries = [0, 0]; + prevOrigSeries = [0, 0]; + } + this.datasourceOrigData[dataIndex][datasourceKey].data = []; + if (this.entityDataSubscriptionOptions.type === widgetType.timeseries && !isTsLatest) { + keyData.forEach((keySeries) => { + let series = keySeries; + const time = series[0]; + this.datasourceOrigData[dataIndex][datasourceKey].data.push([series[0], series[1]]); + let value = EntityDataSubscription.convertValue(series[1]); + if (dataKey.postFunc) { + value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]); + } + prevOrigSeries = [series[0], series[1]]; + series = [series[0], value]; + data.push([series[0], series[1]]); + prevSeries = [series[0], series[1]]; + }); + update = true; + } else if (this.entityDataSubscriptionOptions.type === widgetType.latest || isTsLatest) { + if (keyData.length > 0) { + let series = keyData[0]; + const time = series[0]; + this.datasourceOrigData[dataIndex][datasourceKey].data.push([series[0], series[1]]); + let value = EntityDataSubscription.convertValue(series[1]); + if (dataKey.postFunc) { + value = dataKey.postFunc(time, value, prevSeries[1], prevOrigSeries[0], prevOrigSeries[1]); + } + series = [time, value]; + data.push([series[0], series[1]]); + } + update = true; + } + if (update) { + this.datasourceData[dataIndex][datasourceKey].data = data; + dataUpdatedCb(this.datasourceData[dataIndex][datasourceKey], dataIndex, dataKey.index, detectChanges, isTsLatest); + } + } } - return val; } private toSubscriptionData(sourceData: {[key: string]: TsValue | TsValue[]}, isTs: boolean): SubscriptionData { const subsData: SubscriptionData = {}; for (const keyName of Object.keys(sourceData)) { const values = sourceData[keyName]; - const dataSet: [number, any][] = []; + const dataSet: [number, any, number?][] = []; if (isTs) { (values as TsValue[]).forEach((keySeries) => { - dataSet.push([keySeries.ts, keySeries.value]); + dataSet.push([keySeries.ts, keySeries.value, keySeries.count]); }); } else { const tsValue = values as TsValue; - dataSet.push([tsValue.ts, tsValue.value]); + dataSet.push([tsValue.ts, tsValue.value, tsValue.count]); } subsData[keyName] = dataSet; } @@ -800,21 +1020,37 @@ export class EntityDataSubscription { } private createRealtimeDataAggregator(subsTw: SubscriptionTimewindow, - tsKeyNames: Array, - dataKeyType: DataKeyType, + tsKeys: Array, + isLatestDataAgg: boolean, dataIndex: number, dataUpdatedCb: DataUpdatedCb): DataAggregator { return new DataAggregator( (data, detectChanges) => { - this.onData(data, dataKeyType, dataIndex, detectChanges, false, dataUpdatedCb); + this.onIndexedData(data, dataIndex, detectChanges, + isLatestDataAgg && (this.entityDataSubscriptionOptions.type === widgetType.timeseries), dataUpdatedCb); }, - tsKeyNames, + tsKeys, + isLatestDataAgg, subsTw, this.utils, - this.entityDataSubscriptionOptions.ignoreDataUpdateOnIntervalTick + this.entityDataSubscriptionOptions.ignoreDataUpdateOnIntervalTick || isLatestDataAgg ); } + private dataKeyByIndex(index: number): SubscriptionDataKey { + return this.dataKeysList.find(key => key.index === index); + } + + private timeseriesDataKeysByKeyNames(keyNames: string[]): SubscriptionDataKey[] { + const result: SubscriptionDataKey[] = []; + for (const keyName of keyNames) { + const key = `${keyName}_${DataKeyType.timeseries}`; + const dataKeyList = this.dataKeys[key] as Array; + result.push(...dataKeyList); + } + return result; + } + private generateSeries(dataKey: SubscriptionDataKey, startTime: number, endTime: number): [number, any][] { const data: [number, any][] = []; let prevSeries: [number, any]; @@ -893,9 +1129,7 @@ export class EntityDataSubscription { let startTime: number; let endTime: number; let delta: number; - const generatedData: SubscriptionDataHolder = { - data: {} - }; + const generatedData: IndexedSubscriptionData = []; if (!this.history) { delta = Math.floor(this.tickElapsed / this.frequency); } @@ -928,7 +1162,7 @@ export class EntityDataSubscription { endTime = Math.min(currentTime, endTime); } } - generatedData.data[`${dataKey.name}_${dataKey.index}`] = this.generateSeries(dataKey, startTime, endTime); + generatedData[dataKey.index] = this.generateSeries(dataKey, startTime, endTime); } if (this.dataAggregators && this.dataAggregators.length) { this.dataAggregators[0].onData(generatedData, true, this.history, detectChanges); diff --git a/ui-ngx/src/app/core/api/entity-data.service.ts b/ui-ngx/src/app/core/api/entity-data.service.ts index 3d3112f007..6d8ea94dc0 100644 --- a/ui-ngx/src/app/core/api/entity-data.service.ts +++ b/ui-ngx/src/app/core/api/entity-data.service.ts @@ -31,6 +31,7 @@ import { Observable, of } from 'rxjs'; export interface EntityDataListener { subscriptionType: widgetType; + useTimewindow?: boolean; subscriptionTimewindow?: SubscriptionTimewindow; latestTsOffset?: number; configDatasource: Datasource; @@ -73,6 +74,21 @@ export class EntityDataService { } } + private static toSubscriptionDataKey(dataKey: DataKey, latest: boolean): SubscriptionDataKey { + return { + name: dataKey.name, + type: dataKey.type, + aggregationType: dataKey.aggregationType, + comparisonEnabled: dataKey.comparisonEnabled, + timeForComparison: dataKey.timeForComparison, + comparisonCustomIntervalValue: dataKey.comparisonCustomIntervalValue, + comparisonResultType: dataKey.comparisonResultType, + funcBody: dataKey.funcBody, + postFuncBody: dataKey.postFuncBody, + latest + }; + } + public prepareSubscription(listener: EntityDataListener, ignoreDataUpdateOnIntervalTick = false): Observable { const datasource = listener.configDatasource; @@ -93,10 +109,10 @@ export class EntityDataService { public startSubscription(listener: EntityDataListener) { if (listener.subscription) { - if (listener.subscriptionType === widgetType.timeseries) { + if (listener.useTimewindow) { listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow); - listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset; - } else if (listener.subscriptionType === widgetType.latest) { + } + if (listener.subscriptionType === widgetType.timeseries || listener.subscriptionType === widgetType.latest) { listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset; } listener.subscription.start(); @@ -122,10 +138,10 @@ export class EntityDataService { return of(null); } listener.subscription = new EntityDataSubscription(listener, this.telemetryService, this.utils); - if (listener.subscriptionType === widgetType.timeseries) { + if (listener.useTimewindow) { listener.subscriptionOptions.subscriptionTimewindow = deepClone(listener.subscriptionTimewindow); - listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset; - } else if (listener.subscriptionType === widgetType.latest) { + } + if (listener.subscriptionType === widgetType.timeseries || listener.subscriptionType === widgetType.latest) { listener.subscriptionOptions.latestTsOffset = listener.latestTsOffset; } return listener.subscription.subscribe(); @@ -146,11 +162,11 @@ export class EntityDataService { ignoreDataUpdateOnIntervalTick: boolean): EntityDataSubscriptionOptions { const subscriptionDataKeys: Array = []; datasource.dataKeys.forEach((dataKey) => { - subscriptionDataKeys.push(this.toSubscriptionDataKey(dataKey, false)); + subscriptionDataKeys.push(EntityDataService.toSubscriptionDataKey(dataKey, false)); }); if (datasource.latestDataKeys) { datasource.latestDataKeys.forEach((dataKey) => { - subscriptionDataKeys.push(this.toSubscriptionDataKey(dataKey, true)); + subscriptionDataKeys.push(EntityDataService.toSubscriptionDataKey(dataKey, true)); }); } const entityDataSubscriptionOptions: EntityDataSubscriptionOptions = { @@ -171,14 +187,4 @@ export class EntityDataService { entityDataSubscriptionOptions.ignoreDataUpdateOnIntervalTick = ignoreDataUpdateOnIntervalTick; return entityDataSubscriptionOptions; } - - private toSubscriptionDataKey(dataKey: DataKey, latest: boolean): SubscriptionDataKey { - return { - name: dataKey.name, - type: dataKey.type, - funcBody: dataKey.funcBody, - postFuncBody: dataKey.postFuncBody, - latest - }; - } } diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index 41aeb5dd14..a01e731b8d 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -28,6 +28,7 @@ import { DataSetHolder, Datasource, DatasourceData, + datasourcesHasAggregation, DatasourceType, LegendConfig, LegendData, @@ -95,6 +96,7 @@ export class WidgetSubscription implements IWidgetSubscription { timezone: string; subscriptionTimewindow: SubscriptionTimewindow; useDashboardTimewindow: boolean; + useTimewindow: boolean; tsOffset = 0; hasDataPageLink: boolean; @@ -200,6 +202,7 @@ export class WidgetSubscription implements IWidgetSubscription { this.originalTimewindow = null; this.timeWindow = {}; this.useDashboardTimewindow = options.useDashboardTimewindow; + this.useTimewindow = true; if (this.useDashboardTimewindow) { this.timeWindowConfig = deepClone(options.dashboardTimewindow); } else { @@ -245,15 +248,16 @@ export class WidgetSubscription implements IWidgetSubscription { this.timeWindow = {}; this.useDashboardTimewindow = options.useDashboardTimewindow; this.stateData = options.stateData; - if (this.type === widgetType.latest) { - this.timezone = options.dashboardTimewindow.timezone; - this.updateTsOffset(); - } + this.useTimewindow = this.type === widgetType.timeseries || datasourcesHasAggregation(this.configuredDatasources); if (this.useDashboardTimewindow) { this.timeWindowConfig = deepClone(options.dashboardTimewindow); } else { this.timeWindowConfig = deepClone(options.timeWindowConfig); } + if (this.type === widgetType.latest) { + this.timezone = this.useTimewindow ? this.timeWindowConfig.timezone : options.dashboardTimewindow.timezone; + this.updateTsOffset(); + } this.subscriptionTimewindow = null; this.comparisonEnabled = options.comparisonEnabled && isHistoryTypeTimewindow(this.timeWindowConfig); @@ -443,6 +447,7 @@ export class WidgetSubscription implements IWidgetSubscription { const resolveResultObservables = this.configuredDatasources.map((datasource, index) => { const listener: EntityDataListener = { subscriptionType: this.type, + useTimewindow: this.useTimewindow, configDatasource: datasource, configDatasourceIndex: index, dataLoaded: (pageData, data1, datasourceIndex, pageLink) => { @@ -626,22 +631,31 @@ export class WidgetSubscription implements IWidgetSubscription { } onDashboardTimewindowChanged(newDashboardTimewindow: Timewindow) { - if (this.type === widgetType.timeseries || this.type === widgetType.alarm) { + let doUpdate = false; + let isTimewindowTypeChanged = false; + if (this.useTimewindow) { if (this.useDashboardTimewindow) { + if (this.type === widgetType.latest) { + if (newDashboardTimewindow && this.timezone !== newDashboardTimewindow.timezone) { + this.timezone = newDashboardTimewindow.timezone; + doUpdate = this.updateTsOffset(); + } + } if (!isEqual(this.timeWindowConfig, newDashboardTimewindow) && newDashboardTimewindow) { - const isTimewindowTypeChanged = timewindowTypeChanged(this.timeWindowConfig, newDashboardTimewindow); + isTimewindowTypeChanged = timewindowTypeChanged(this.timeWindowConfig, newDashboardTimewindow); this.timeWindowConfig = deepClone(newDashboardTimewindow); - this.update(isTimewindowTypeChanged); + doUpdate = true; } } } else if (this.type === widgetType.latest) { if (newDashboardTimewindow && this.timezone !== newDashboardTimewindow.timezone) { this.timezone = newDashboardTimewindow.timezone; - if (this.updateTsOffset()) { - this.update(); - } + doUpdate = this.updateTsOffset(); } } + if (doUpdate) { + this.update(isTimewindowTypeChanged); + } } updateDataVisibility(index: number): void { @@ -660,6 +674,12 @@ export class WidgetSubscription implements IWidgetSubscription { updateTimewindowConfig(newTimewindow: Timewindow): void { if (!this.useDashboardTimewindow) { + if (this.type === widgetType.latest) { + if (newTimewindow && this.timezone !== newTimewindow.timezone) { + this.timezone = newTimewindow.timezone; + this.updateTsOffset(); + } + } const isTimewindowTypeChanged = timewindowTypeChanged(this.timeWindowConfig, newTimewindow); this.timeWindowConfig = newTimewindow; this.update(isTimewindowTypeChanged); @@ -874,11 +894,12 @@ export class WidgetSubscription implements IWidgetSubscription { } const datasource = this.configuredDatasources[datasourceIndex]; if (datasource) { - if (this.type === widgetType.timeseries && this.timeWindowConfig) { + if (this.useTimewindow && this.timeWindowConfig) { this.updateRealtimeSubscription(); } entityDataListener = { subscriptionType: this.type, + useTimewindow: this.useTimewindow, configDatasource: datasource, configDatasourceIndex: datasourceIndex, subscriptionTimewindow: this.subscriptionTimewindow, @@ -940,7 +961,7 @@ export class WidgetSubscription implements IWidgetSubscription { private updateDataTimewindow() { if (!this.hasDataPageLink) { - if (this.type === widgetType.timeseries && this.timeWindowConfig) { + if (this.useTimewindow && this.timeWindowConfig) { this.updateRealtimeSubscription(); if (this.comparisonEnabled) { this.updateSubscriptionForComparison(); @@ -952,11 +973,11 @@ export class WidgetSubscription implements IWidgetSubscription { private dataSubscribe() { this.updateDataTimewindow(); if (!this.hasDataPageLink) { - if (this.type === widgetType.timeseries && this.timeWindowConfig && this.subscriptionTimewindow.fixedWindow) { + if (this.useTimewindow && this.timeWindowConfig && this.subscriptionTimewindow.fixedWindow) { this.onDataUpdated(); } const forceUpdate = !this.datasources.length; - const notifyDataLoaded = !this.entityDataListeners.filter((listener) => listener.subscription ? true : false).length; + const notifyDataLoaded = !this.entityDataListeners.filter((listener) => !!listener.subscription).length; this.entityDataListeners.forEach((listener) => { if (this.comparisonEnabled && listener.configDatasource.isAdditional) { listener.subscriptionTimewindow = this.timewindowForComparison; diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index c157ca869e..90238ebaba 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -34,6 +34,7 @@ import { Datasource, DatasourceType, Widget, widgetType } from '@app/shared/mode import { EntityType } from '@shared/models/entity-type.models'; import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models'; import { EntityId } from '@app/shared/models/id/entity-id'; +import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models'; @Injectable({ providedIn: 'root' @@ -215,6 +216,9 @@ export class DashboardUtilsService { delete datasource.deviceAliasId; } }); + if (widget.type === widgetType.latest) { + widget.config.timewindow = initModelFromDefaultTimewindow(widget.config.timewindow, true, this.timeService); + } // Temp workaround if (widget.isSystemType && widget.bundleAlias === 'charts' && widget.typeAlias === 'timeseries') { widget.typeAlias = 'basic_timeseries'; diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 4ebaccb774..c145794163 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -1062,7 +1062,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC dataKeys: config.alarmSource.dataKeys || [] }; } - const newWidget: Widget = { + let newWidget: Widget = { isSystemType: widget.isSystemType, bundleAlias: widget.bundleAlias, typeAlias: widgetTypeInfo.alias, @@ -1076,6 +1076,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC row: 0, col: 0 }; + newWidget = this.dashboardUtils.validateAndUpdateWidget(newWidget); if (widgetTypeInfo.typeParameters.useCustomDatasources) { this.addWidgetToDashboard(newWidget); } else { diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss index 6f9fcc2102..eefcbb5044 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.scss @@ -19,6 +19,11 @@ width: 100%; height: 100%; display: block; + + mat-drawer-container { + overflow: clip; + } + .tb-entity-table { .tb-entity-table-content { width: 100%; diff --git a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts index 5dfa528e9a..074381f5de 100644 --- a/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/entity/entities-table.component.ts @@ -305,7 +305,6 @@ export class EntitiesTableComponent extends PageComponent implements IEntitiesTa this.textSearchMode = true; this.pageLink.textSearch = decodeURI(params.textSearch); } else { - this.textSearchMode = false; this.pageLink.textSearch = null; } this.updateData(); diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html index 4f85de08b0..2327d708da 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.html @@ -36,6 +36,7 @@ [dashboard]="data.dashboard" [aliasController]="data.aliasController" [widget]="data.widget" + [widgetType]="data.widgetType" [showPostProcessing]="data.showPostProcessing" [callbacks]="data.callbacks" formControlName="dataKey"> diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts index 5c67242958..a3d5d8fd65 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config-dialog.component.ts @@ -22,7 +22,7 @@ import { AppState } from '@core/core.state'; import { FormBuilder, FormControl, FormGroup, FormGroupDirective, NgForm, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@shared/components/dialog.component'; -import { DataKey, Widget } from '@shared/models/widget.models'; +import { DataKey, Widget, widgetType } from '@shared/models/widget.models'; import { DataKeysCallbacks } from './data-keys.component.models'; import { DataKeyConfigComponent } from '@home/components/widget/data-key-config.component'; import { Dashboard } from '@shared/models/dashboard.models'; @@ -35,6 +35,7 @@ export interface DataKeyConfigDialogData { dashboard: Dashboard; aliasController: IAliasController; widget: Widget; + widgetType: widgetType; entityAliasId?: string; showPostProcessing?: boolean; callbacks?: DataKeysCallbacks; diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html index 19cc902098..9088f5cc33 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.html @@ -62,6 +62,75 @@ +
+ + datakey.aggregation + + + {{ aggregationTypesTranslations.get(aggregationTypes[aggregation]) | translate }} + + + + {{ dataKeyFormGroup.get('aggregationType').value ? (dataKeyAggregationTypeHintTranslations.get(aggregationTypes[dataKeyFormGroup.get('aggregationType').value]) | translate) : '' }} +
+ {{ 'datakey.aggregation-type-hint-common' | translate }} +
+
+
+
+ datakey.delta-calculation + + + + + {{ 'datakey.enable-delta-calculation' | translate }} + + {{ 'datakey.enable-delta-calculation-hint' | translate }} + + + +
+ + widgets.chart.time-for-comparison + + + {{ 'widgets.chart.time-for-comparison-previous-interval' | translate }} + + + {{ 'widgets.chart.time-for-comparison-days' | translate }} + + + {{ 'widgets.chart.time-for-comparison-weeks' | translate }} + + + {{ 'widgets.chart.time-for-comparison-months' | translate }} + + + {{ 'widgets.chart.time-for-comparison-years' | translate }} + + + {{ 'widgets.chart.time-for-comparison-custom-interval' | translate }} + + + + + widgets.chart.custom-interval-value + + + + datakey.delta-calculation-result + + + {{ comparisonResultTypeTranslations.get(comparisonResultTypes[comparisonResultType]) | translate }} + + + +
+
+
+
+
datakey.data-generation-func
diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.scss index 8b71ee3664..7fd1fba872 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.scss @@ -28,6 +28,36 @@ padding-left: 12px; } } + + .fields-group { + padding: 0 16px 8px; + margin-bottom: 10px; + border: 1px groove rgba(0, 0, 0, .25); + border-radius: 4px; + + legend { + color: rgba(0, 0, 0, .7); + width: fit-content; + } + + legend + * { + display: block; + margin-top: 16px; + } + + &.fields-group-slider { + padding: 0; + + legend { + margin-left: 16px; + } + + > .tb-settings { + margin-top: 0; + padding: 0 16px 8px; + } + } + } } } @@ -42,5 +72,50 @@ } } } + .mat-expansion-panel { + &.tb-settings { + box-shadow: none; + + .mat-content { + overflow: visible; + } + + .mat-expansion-panel-header { + padding: 0; + color: rgba(0, 0, 0, 0.87); + height: 140px; + + &:hover { + background: none; + } + + .mat-expansion-indicator { + padding: 2px; + } + } + + .mat-expansion-panel-header-description { + align-items: center; + } + + > .mat-expansion-panel-content { + > .mat-expansion-panel-body { + padding: 0; + } + } + } + + .mat-expansion-panel-content { + font: inherit; + } + } + + .mat-slide { + margin: 8px 0; + } + + .mat-slide-toggle-content { + white-space: normal; + } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts index b97f17f0e0..a461796111 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/data-key-config.component.ts @@ -18,7 +18,13 @@ import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@an import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; -import { DataKey, Widget } from '@shared/models/widget.models'; +import { + ComparisonResultType, comparisonResultTypeTranslationMap, + DataKey, + dataKeyAggregationTypeHintTranslationMap, + Widget, + widgetType +} from '@shared/models/widget.models'; import { ControlValueAccessor, FormBuilder, @@ -43,6 +49,7 @@ import { JsonFormComponentData } from '@shared/components/json-form/json-form-co import { WidgetService } from '@core/http/widget.service'; import { Dashboard } from '@shared/models/dashboard.models'; import { IAliasController } from '@core/api/widget-api.models'; +import { aggregationTranslations, AggregationType, ComparisonDuration } from '@shared/models/time/time.models'; @Component({ selector: 'tb-data-key-config', @@ -65,6 +72,22 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con dataKeyTypes = DataKeyType; + widgetTypes = widgetType; + + aggregations = [AggregationType.NONE, ...Object.keys(AggregationType).filter(type => type !== AggregationType.NONE)]; + + aggregationTypes = AggregationType; + + aggregationTypesTranslations = aggregationTranslations; + + dataKeyAggregationTypeHintTranslations = dataKeyAggregationTypeHintTranslationMap; + + comparisonResultTypes = ComparisonResultType; + + comparisonResults = Object.keys(ComparisonResultType); + + comparisonResultTypeTranslations = comparisonResultTypeTranslationMap; + @Input() entityAliasId: string; @@ -80,6 +103,9 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con @Input() widget: Widget; + @Input() + widgetType: widgetType; + @Input() dataKeySettingsSchema: any; @@ -155,6 +181,11 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con } this.dataKeyFormGroup = this.fb.group({ name: [null, []], + aggregationType: [null, []], + comparisonEnabled: [null, []], + timeForComparison: [null, [Validators.required]], + comparisonCustomIntervalValue: [null, [Validators.required, Validators.min(1000)]], + comparisonResultType: [null, [Validators.required]], label: [null, [Validators.required]], color: [null, [Validators.required]], units: [null, []], @@ -164,6 +195,32 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con postFuncBody: [null, []] }); + this.dataKeyFormGroup.get('aggregationType').valueChanges.subscribe( + (aggType) => { + if (!this.dataKeyFormGroup.get('label').dirty) { + let newLabel = this.dataKeyFormGroup.get('name').value; + if (aggType !== AggregationType.NONE) { + const prefix = this.translate.instant(aggregationTranslations.get(aggType)); + newLabel = prefix + ' ' + newLabel; + } + this.dataKeyFormGroup.get('label').patchValue(newLabel); + } + this.updateComparisonValidators(); + } + ); + + this.dataKeyFormGroup.get('comparisonEnabled').valueChanges.subscribe( + () => { + this.updateComparisonValues(); + } + ); + + this.dataKeyFormGroup.get('timeForComparison').valueChanges.subscribe( + () => { + this.updateComparisonValues(); + } + ); + this.dataKeyFormGroup.valueChanges.subscribe(() => { this.updateModel(); }); @@ -199,22 +256,86 @@ export class DataKeyConfigComponent extends PageComponent implements OnInit, Con if (this.modelValue.postFuncBody && this.modelValue.postFuncBody.length) { this.modelValue.usePostProcessing = true; } + if (this.widgetType === widgetType.latest && this.modelValue.type === DataKeyType.timeseries && !this.modelValue.aggregationType) { + this.modelValue.aggregationType = AggregationType.NONE; + } this.dataKeyFormGroup.patchValue(this.modelValue, {emitEvent: false}); + this.updateValidators(); + if (this.displayAdvanced) { + this.dataKeySettingsData.model = this.modelValue.settings; + this.dataKeySettingsFormGroup.patchValue({ + settings: this.dataKeySettingsData + }, {emitEvent: false}); + } + } + + private updateValidators() { this.dataKeyFormGroup.get('name').setValidators(this.modelValue.type !== DataKeyType.function && - this.modelValue.type !== DataKeyType.count - ? [Validators.required] : []); + this.modelValue.type !== DataKeyType.count + ? [Validators.required] : []); if (this.modelValue.type === DataKeyType.count) { this.dataKeyFormGroup.get('name').disable({emitEvent: false}); } else { this.dataKeyFormGroup.get('name').enable({emitEvent: false}); } this.dataKeyFormGroup.get('name').updateValueAndValidity({emitEvent: false}); - if (this.displayAdvanced) { - this.dataKeySettingsData.model = this.modelValue.settings; - this.dataKeySettingsFormGroup.patchValue({ - settings: this.dataKeySettingsData - }, {emitEvent: false}); + this.updateComparisonValidators(); + } + + private updateComparisonValues() { + const comparisonEnabled = this.dataKeyFormGroup.get('comparisonEnabled').value; + if (comparisonEnabled) { + const timeForComparison: ComparisonDuration = this.dataKeyFormGroup.get('timeForComparison').value; + if (!timeForComparison) { + this.dataKeyFormGroup.get('timeForComparison').patchValue('previousInterval', {emitEvent: false}); + } else if (timeForComparison === 'customInterval') { + const comparisonCustomIntervalValue = this.dataKeyFormGroup.get('comparisonCustomIntervalValue').value; + if (!comparisonCustomIntervalValue) { + this.dataKeyFormGroup.get('comparisonCustomIntervalValue').patchValue(7200000, {emitEvent: false}); + } + } + const comparisonResultType: ComparisonResultType = this.dataKeyFormGroup.get('comparisonResultType').value; + if (!comparisonResultType) { + this.dataKeyFormGroup.get('comparisonResultType').patchValue(ComparisonResultType.DELTA_ABSOLUTE, {emitEvent: false}); + } + } + this.updateComparisonValidators(); + } + + private updateComparisonValidators() { + const aggregationType: AggregationType = this.dataKeyFormGroup.get('aggregationType').value; + if (aggregationType && aggregationType !== AggregationType.NONE) { + this.dataKeyFormGroup.get('comparisonEnabled').enable({emitEvent: false}); + const comparisonEnabled = this.dataKeyFormGroup.get('comparisonEnabled').value; + if (comparisonEnabled) { + this.dataKeyFormGroup.get('timeForComparison').enable({emitEvent: false}); + const timeForComparison: ComparisonDuration = this.dataKeyFormGroup.get('timeForComparison').value; + if (timeForComparison) { + this.dataKeyFormGroup.get('comparisonResultType').enable({emitEvent: false}); + if (timeForComparison === 'customInterval') { + this.dataKeyFormGroup.get('comparisonCustomIntervalValue').enable({emitEvent: false}); + } else { + this.dataKeyFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent: false}); + } + } else { + this.dataKeyFormGroup.get('comparisonResultType').disable({emitEvent: false}); + this.dataKeyFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent: false}); + } + } else { + this.dataKeyFormGroup.get('timeForComparison').disable({emitEvent: false}); + this.dataKeyFormGroup.get('comparisonResultType').disable({emitEvent: false}); + this.dataKeyFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent: false}); + } + } else { + this.dataKeyFormGroup.get('comparisonEnabled').disable({emitEvent: false}); + this.dataKeyFormGroup.get('timeForComparison').disable({emitEvent: false}); + this.dataKeyFormGroup.get('comparisonResultType').disable({emitEvent: false}); + this.dataKeyFormGroup.get('comparisonCustomIntervalValue').disable({emitEvent: false}); } + this.dataKeyFormGroup.get('comparisonEnabled').updateValueAndValidity({emitEvent: false}); + this.dataKeyFormGroup.get('timeForComparison').updateValueAndValidity({emitEvent: false}); + this.dataKeyFormGroup.get('comparisonResultType').updateValueAndValidity({emitEvent: false}); + this.dataKeyFormGroup.get('comparisonCustomIntervalValue').updateValueAndValidity({emitEvent: false}); } private updateModel() { diff --git a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html index 3527a5ca2e..5df377cf07 100644 --- a/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/data-keys.component.html @@ -58,12 +58,7 @@ {{key.label}}
:
-
- f({{key.name}}) - - {{key.name}} - -
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index db593e64f2..2286510ba3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -20,7 +20,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { DataKey, - Datasource, + Datasource, datasourcesHasAggregation, datasourcesHasOnlyComparisonAggregation, DatasourceType, datasourceTypeTranslationMap, defaultLegendConfig, @@ -42,7 +42,7 @@ import { Validators } from '@angular/forms'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; -import { deepClone, isDefined, isObject, isUndefined } from '@app/core/utils'; +import { deepClone, isDefined, isObject } from '@app/core/utils'; import { alarmFields, AlarmSearchStatus, @@ -51,7 +51,7 @@ import { alarmSeverityTranslations } from '@shared/models/alarm.models'; import { IAliasController } from '@core/api/widget-api.models'; -import { EntityAlias, EntityAliases } from '@shared/models/alias.models'; +import { EntityAlias } from '@shared/models/alias.models'; import { UtilsService } from '@core/services/utils.service'; import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { TranslateService } from '@ngx-translate/core'; @@ -67,13 +67,14 @@ import { MatDialog } from '@angular/material/dialog'; import { EntityService } from '@core/http/entity.service'; import { JsonFormComponentData } from '@shared/components/json-form/json-form-component.models'; import { WidgetActionsData } from './action/manage-widget-actions.component.models'; -import { Dashboard, DashboardState } from '@shared/models/dashboard.models'; +import { Dashboard } from '@shared/models/dashboard.models'; import { entityFields } from '@shared/models/entity.models'; -import { Filter, Filters } from '@shared/models/query/query.models'; +import { Filter } from '@shared/models/query/query.models'; import { FilterDialogComponent, FilterDialogData } from '@home/components/filter/filter-dialog.component'; import { COMMA, ENTER, SEMICOLON } from '@angular/cdk/keycodes'; import { MatChipInputEvent } from '@angular/material/chips'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; +import { AggregationType } from '@shared/models/time/time.models'; const emptySettingsSchema: JsonSchema = { type: 'object', @@ -334,10 +335,10 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont this.targetDeviceSettings = this.fb.group({}); this.alarmSourceSettings = this.fb.group({}); this.advancedSettings = this.fb.group({}); - if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) { - this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(null)); - this.dataSettings.addControl('displayTimewindow', this.fb.control(null)); - this.dataSettings.addControl('timewindow', this.fb.control(null)); + if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) { + this.dataSettings.addControl('useDashboardTimewindow', this.fb.control(true)); + this.dataSettings.addControl('displayTimewindow', this.fb.control({value: true, disabled: true})); + this.dataSettings.addControl('timewindow', this.fb.control({value: null, disabled: true})); this.dataSettings.get('useDashboardTimewindow').valueChanges.subscribe((value: boolean) => { if (value) { this.dataSettings.get('displayTimewindow').disable({emitEvent: false}); @@ -467,7 +468,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont }, {emitEvent: false} ); - if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) { + if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) { const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ? config.useDashboardTimewindow : true; this.dataSettings.patchValue( @@ -733,6 +734,24 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, Cont !!this.modelValue.settingsDirective && !!this.modelValue.settingsDirective.length); } + public displayTimewindowConfig(): boolean { + if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm) { + return true; + } else if (this.widgetType === widgetType.latest) { + const datasources = this.dataSettings.get('datasources').value; + return datasourcesHasAggregation(datasources); + } + } + + public onlyHistoryTimewindow(): boolean { + if (this.widgetType === widgetType.latest) { + const datasources = this.dataSettings.get('datasources').value; + return datasourcesHasOnlyComparisonAggregation(datasources); + } else { + return false; + } + } + public onDatasourceDrop(event: CdkDragDrop) { const datasourcesFormArray = this.datasourcesFormArray(); const datasourceForm = datasourcesFormArray.at(event.previousIndex); diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html index a6520e02bd..ad8f65a48b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.html @@ -46,6 +46,9 @@ ; @@ -325,6 +333,10 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { hasAggregation: boolean; + onlyQuickInterval: boolean; + + onlyHistoryTimewindow: boolean; + style: {[klass: string]: any}; showWidgetTitlePanel: boolean; @@ -420,12 +432,28 @@ export class DashboardWidget implements GridsterItem, IDashboardWidget { this.dropShadow = isDefined(this.widget.config.dropShadow) ? this.widget.config.dropShadow : true; this.enableFullscreen = isDefined(this.widget.config.enableFullscreen) ? this.widget.config.enableFullscreen : true; - this.hasTimewindow = (this.widget.type === widgetType.timeseries || this.widget.type === widgetType.alarm) ? + let canHaveTimewindow = false; + let onlyQuickInterval = false; + let onlyHistoryTimewindow = false; + if (this.widget.type === widgetType.timeseries || this.widget.type === widgetType.alarm) { + canHaveTimewindow = true; + } else if (this.widget.type === widgetType.latest) { + canHaveTimewindow = datasourcesHasAggregation(this.widget.config.datasources); + onlyQuickInterval = canHaveTimewindow; + if (canHaveTimewindow) { + onlyHistoryTimewindow = datasourcesHasOnlyComparisonAggregation(this.widget.config.datasources); + } + } + + this.hasTimewindow = canHaveTimewindow ? (isDefined(this.widget.config.useDashboardTimewindow) ? (!this.widget.config.useDashboardTimewindow && (isUndefined(this.widget.config.displayTimewindow) || this.widget.config.displayTimewindow)) : false) : false; + this.onlyQuickInterval = onlyQuickInterval; + this.onlyHistoryTimewindow = onlyHistoryTimewindow; + this.hasAggregation = this.widget.type === widgetType.timeseries; this.style = { diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html index e8d64ae68b..d658de9589 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html @@ -40,8 +40,8 @@ + {{ twoFactorAuthProvidersData.get(provider.value.providerType).name | translate }} - {{ twoFactorAuthProvidersData.get(provider.value.providerType).name | translate }} diff --git a/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.ts b/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.ts index 2a98f0b530..dfedf674a3 100644 --- a/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.ts @@ -187,8 +187,7 @@ export class TwoFactorAuthLoginComponent extends PageComponent implements OnInit cancelLogin() { if (this.prevProvider) { - this.selectedProvider = this.prevProvider; - this.prevProvider = null; + this.selectProvider(this.prevProvider); } else { this.authService.logout(); } diff --git a/ui-ngx/src/app/shared/adapter/custom-datatime-adapter.ts b/ui-ngx/src/app/shared/adapter/custom-datatime-adapter.ts new file mode 100644 index 0000000000..50417a5914 --- /dev/null +++ b/ui-ngx/src/app/shared/adapter/custom-datatime-adapter.ts @@ -0,0 +1,36 @@ +/// +/// Copyright © 2016-2022 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 { NativeDatetimeAdapter } from '@mat-datetimepicker/core'; + +@Injectable() +export class CustomDateAdapter extends NativeDatetimeAdapter { + + parse(value: string | number): Date { + if (typeof value === 'number') { + return new Date(value); + } + let newDate = value; + const formatToParts = Intl.DateTimeFormat(this.locale).formatToParts(); + if (formatToParts[0].type.toLowerCase() === 'day') { + const literal = formatToParts[1].value; + newDate = newDate.replace(new RegExp(`(\\d+[${literal}])(\\d+[${literal}])`), '$2$1'); + } + return newDate ? new Date(Date.parse(newDate)) : null; + } + +} diff --git a/ui-ngx/src/app/shared/components/file-input.component.ts b/ui-ngx/src/app/shared/components/file-input.component.ts index ad2fd9f65e..deee44596c 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.ts +++ b/ui-ngx/src/app/shared/components/file-input.component.ts @@ -188,20 +188,18 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, let fileName = null; let fileContent = null; let files = null; - if (typeof reader.result === 'string') { - fileContent = reader.result; - if (fileContent && fileContent.length > 0) { - if (!this.workFromFileObj) { + if (reader.readyState === reader.DONE) { + if (!this.workFromFileObj) { + fileContent = reader.result; + if (fileContent && fileContent.length > 0) { if (this.contentConvertFunction) { fileContent = this.contentConvertFunction(fileContent); } - if (fileContent) { - fileName = file.name; - } - } else { - files = file.file; - fileName = file.name; + fileName = fileContent ? file.name : null; } + } else if (file.name || file.file){ + files = file.file; + fileName = file.name; } } resolve({fileContent, fileName, files}); diff --git a/ui-ngx/src/app/shared/components/phone-input.component.ts b/ui-ngx/src/app/shared/components/phone-input.component.ts index 238f3c7134..a620fa6263 100644 --- a/ui-ngx/src/app/shared/components/phone-input.component.ts +++ b/ui-ngx/src/app/shared/components/phone-input.component.ts @@ -142,7 +142,7 @@ export class PhoneInputComponent implements OnInit, ControlValueAccessor, Valida this.getFlagAndPhoneNumberData(value); let phoneNumber = this.phoneFormGroup.get('phoneNumber').value; if (phoneNumber) { - if (code !== this.countryCallingCode && phoneNumber.includes(code)) { + if (code !== '+' && code !== this.countryCallingCode && phoneNumber.includes(code)) { phoneNumber = phoneNumber.replace(code, this.countryCallingCode); this.phoneFormGroup.get('phoneNumber').patchValue(phoneNumber); } diff --git a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss new file mode 100644 index 0000000000..aeb3798f35 --- /dev/null +++ b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.scss @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2022 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 '../../../../scss/constants'; + +:host { + min-width: 355px; + + @media #{$mat-xs} { + min-width: 0; + width: 100%; + } +} diff --git a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts index c318bc4dfb..c8167dbc0e 100644 --- a/ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts +++ b/ui-ngx/src/app/shared/components/time/quick-time-interval.component.ts @@ -21,6 +21,7 @@ import { QuickTimeInterval, QuickTimeIntervalTranslationMap } from '@shared/mode @Component({ selector: 'tb-quick-time-interval', templateUrl: './quick-time-interval.component.html', + styleUrls: ['./quick-time-interval.component.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html index 1bb893d219..be9c38897b 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.html @@ -29,32 +29,57 @@
- + -
- timewindow.last - +
+
+ + +
+
+ timewindow.last + +
-
- timewindow.interval - +
+
+ + +
+
+ timewindow.interval + +
+ +
diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts index 09fa20aaa3..67ee2ac72b 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts @@ -36,6 +36,7 @@ export const TIMEWINDOW_PANEL_DATA = new InjectionToken('TimewindowPanelDat export interface TimewindowPanelData { historyOnly: boolean; + quickIntervalOnly: boolean; timewindow: Timewindow; aggregation: boolean; timezone: boolean; @@ -51,6 +52,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { historyOnly = false; + quickIntervalOnly = false; + aggregation = false; timezone = false; @@ -83,6 +86,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { public viewContainerRef: ViewContainerRef) { super(store); this.historyOnly = data.historyOnly; + this.quickIntervalOnly = data.quickIntervalOnly; this.timewindow = data.timewindow; this.aggregation = data.aggregation; this.timezone = data.timezone; @@ -91,6 +95,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { ngOnInit(): void { const hideInterval = this.timewindow.hideInterval || false; + const hideLastInterval = this.timewindow.hideLastInterval || false; + const hideQuickInterval = this.timewindow.hideQuickInterval || false; const hideAggregation = this.timewindow.hideAggregation || false; const hideAggInterval = this.timewindow.hideAggInterval || false; const hideTimezone = this.timewindow.hideTimezone || false; @@ -103,10 +109,11 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { ? this.timewindow.realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL, disabled: hideInterval }), - timewindowMs: [ - this.timewindow.realtime && typeof this.timewindow.realtime.timewindowMs !== 'undefined' - ? this.timewindow.realtime.timewindowMs : null - ], + timewindowMs: this.fb.control({ + value: this.timewindow.realtime && typeof this.timewindow.realtime.timewindowMs !== 'undefined' + ? this.timewindow.realtime.timewindowMs : null, + disabled: hideInterval || hideLastInterval + }), interval: [ this.timewindow.realtime && typeof this.timewindow.realtime.interval !== 'undefined' ? this.timewindow.realtime.interval : null @@ -114,7 +121,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { quickInterval: this.fb.control({ value: this.timewindow.realtime && typeof this.timewindow.realtime.quickInterval !== 'undefined' ? this.timewindow.realtime.quickInterval : null, - disabled: hideInterval + disabled: hideInterval || hideQuickInterval }) } ), @@ -289,8 +296,40 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit { this.timewindowForm.get('history.fixedTimewindow').enable({emitEvent: false}); this.timewindowForm.get('history.quickInterval').enable({emitEvent: false}); this.timewindowForm.get('realtime.realtimeType').enable({emitEvent: false}); - this.timewindowForm.get('realtime.timewindowMs').enable({emitEvent: false}); - this.timewindowForm.get('realtime.quickInterval').enable({emitEvent: false}); + if (!this.timewindow.hideLastInterval) { + this.timewindowForm.get('realtime.timewindowMs').enable({emitEvent: false}); + } + if (!this.timewindow.hideQuickInterval) { + this.timewindowForm.get('realtime.quickInterval').enable({emitEvent: false}); + } + } + this.timewindowForm.markAsDirty(); + } + + onHideLastIntervalChanged() { + if (this.timewindow.hideLastInterval) { + this.timewindowForm.get('realtime.timewindowMs').disable({emitEvent: false}); + if (!this.timewindow.hideQuickInterval) { + this.timewindowForm.get('realtime.realtimeType').setValue(RealtimeWindowType.INTERVAL); + } + } else { + if (!this.timewindow.hideInterval) { + this.timewindowForm.get('realtime.timewindowMs').enable({emitEvent: false}); + } + } + this.timewindowForm.markAsDirty(); + } + + onHideQuickIntervalChanged() { + if (this.timewindow.hideQuickInterval) { + this.timewindowForm.get('realtime.quickInterval').disable({emitEvent: false}); + if (!this.timewindow.hideLastInterval) { + this.timewindowForm.get('realtime.realtimeType').setValue(RealtimeWindowType.LAST_INTERVAL); + } + } else { + if (!this.timewindow.hideInterval) { + this.timewindowForm.get('realtime.quickInterval').enable({emitEvent: false}); + } } this.timewindowForm.markAsDirty(); } diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.ts b/ui-ngx/src/app/shared/components/time/timewindow.component.ts index 6f34131241..9b6127181e 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow.component.ts @@ -75,13 +75,41 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces @Input() set historyOnly(val) { - this.historyOnlyValue = coerceBooleanProperty(val); + const newHistoryOnlyValue = coerceBooleanProperty(val); + if (this.historyOnlyValue !== newHistoryOnlyValue) { + this.historyOnlyValue = newHistoryOnlyValue; + if (this.onHistoryOnlyChanged()) { + this.notifyChanged(); + } + } } get historyOnly() { return this.historyOnlyValue; } + alwaysDisplayTypePrefixValue = false; + + @Input() + set alwaysDisplayTypePrefix(val) { + this.alwaysDisplayTypePrefixValue = coerceBooleanProperty(val); + } + + get alwaysDisplayTypePrefix() { + return this.alwaysDisplayTypePrefixValue; + } + + quickIntervalOnlyValue = false; + + @Input() + set quickIntervalOnly(val) { + this.quickIntervalOnlyValue = coerceBooleanProperty(val); + } + + get quickIntervalOnly() { + return this.quickIntervalOnlyValue; + } + aggregationValue = false; @Input() @@ -240,6 +268,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces { timewindow: deepClone(this.innerValue), historyOnly: this.historyOnly, + quickIntervalOnly: this.quickIntervalOnly, aggregation: this.aggregation, timezone: this.timezone, isEdit: this.isEdit @@ -265,6 +294,17 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces return Injector.create({parent: this.viewContainerRef.injector, providers}); } + private onHistoryOnlyChanged(): boolean { + if (this.historyOnlyValue && this.innerValue) { + if (this.innerValue.selectedTab !== TimewindowType.HISTORY) { + this.innerValue.selectedTab = TimewindowType.HISTORY; + this.updateDisplayValue(); + return true; + } + } + return false; + } + registerOnChange(fn: any): void { this.propagateChange = fn; } @@ -278,9 +318,15 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces } writeValue(obj: Timewindow): void { - this.innerValue = initModelFromDefaultTimewindow(obj, this.timeService); + this.innerValue = initModelFromDefaultTimewindow(obj, this.quickIntervalOnly, this.timeService); this.timewindowDisabled = this.isTimewindowDisabled(); - this.updateDisplayValue(); + if (this.onHistoryOnlyChanged()) { + setTimeout(() => { + this.notifyChanged(); + }); + } else { + this.updateDisplayValue(); + } } notifyChanged() { @@ -297,7 +343,7 @@ export class TimewindowComponent implements OnInit, OnDestroy, ControlValueAcces this.millisecondsToTimeStringPipe.transform(this.innerValue.realtime.timewindowMs); } } else { - this.innerValue.displayValue = !this.historyOnly ? (this.translate.instant('timewindow.history') + ' - ') : ''; + this.innerValue.displayValue = (!this.historyOnly || this.alwaysDisplayTypePrefix) ? (this.translate.instant('timewindow.history') + ' - ') : ''; if (this.innerValue.history.historyType === HistoryWindowType.LAST_INTERVAL) { this.innerValue.displayValue += this.translate.instant('timewindow.last-prefix') + ' ' + this.millisecondsToTimeStringPipe.transform(this.innerValue.history.timewindowMs); diff --git a/ui-ngx/src/app/shared/models/query/query.models.ts b/ui-ngx/src/app/shared/models/query/query.models.ts index 81ab569a88..c1bf255cd3 100644 --- a/ui-ngx/src/app/shared/models/query/query.models.ts +++ b/ui-ngx/src/app/shared/models/query/query.models.ts @@ -738,7 +738,6 @@ export function createDefaultEntityDataPageLink(pageSize: number): EntityDataPag } export const singleEntityDataPageLink: EntityDataPageLink = createDefaultEntityDataPageLink(1); -export const defaultEntityDataPageLink: EntityDataPageLink = createDefaultEntityDataPageLink(1024); export interface EntityCountQuery { entityFilter: EntityFilter; @@ -761,12 +760,19 @@ export interface AlarmDataQuery extends AbstractDataQuery { export interface TsValue { ts: number; value: string; + count?: number; +} + +export interface ComparisonTsValue { + current?: TsValue; + previous?: TsValue; } export interface EntityData { entityId: EntityId; latest: {[entityKeyType: string]: {[key: string]: TsValue}}; timeseries: {[key: string]: Array}; + aggLatest?: {[id: number]: ComparisonTsValue}; } export interface AlarmData extends AlarmInfo { diff --git a/ui-ngx/src/app/shared/models/rule-node.models.ts b/ui-ngx/src/app/shared/models/rule-node.models.ts index d4273c9911..c6549f5b39 100644 --- a/ui-ngx/src/app/shared/models/rule-node.models.ts +++ b/ui-ngx/src/app/shared/models/rule-node.models.ts @@ -347,6 +347,14 @@ export enum MessageType { POST_TELEMETRY_REQUEST = 'POST_TELEMETRY_REQUEST', TO_SERVER_RPC_REQUEST = 'TO_SERVER_RPC_REQUEST', RPC_CALL_FROM_SERVER_TO_DEVICE = 'RPC_CALL_FROM_SERVER_TO_DEVICE', + RPC_QUEUED = 'RPC_QUEUED', + RPC_SENT = 'RPC_SENT', + RPC_DELIVERED = 'RPC_DELIVERED', + RPC_SUCCESSFUL = 'RPC_SUCCESSFUL', + RPC_TIMEOUT = 'RPC_TIMEOUT', + RPC_EXPIRED = 'RPC_EXPIRED', + RPC_FAILED = 'RPC_FAILED', + RPC_DELETED = 'RPC_DELETED', ACTIVITY_EVENT = 'ACTIVITY_EVENT', INACTIVITY_EVENT = 'INACTIVITY_EVENT', CONNECT_EVENT = 'CONNECT_EVENT', @@ -358,13 +366,12 @@ export enum MessageType { ENTITY_UNASSIGNED = 'ENTITY_UNASSIGNED', ATTRIBUTES_UPDATED = 'ATTRIBUTES_UPDATED', ATTRIBUTES_DELETED = 'ATTRIBUTES_DELETED', + ALARM_ACKNOWLEDGED = 'ALARM_ACKNOWLEDGED', + ALARM_CLEARED = 'ALARM_CLEARED', + ENTITY_ASSIGNED_FROM_TENANT = 'ENTITY_ASSIGNED_FROM_TENANT', + ENTITY_ASSIGNED_TO_TENANT = 'ENTITY_ASSIGNED_TO_TENANT', TIMESERIES_UPDATED = 'TIMESERIES_UPDATED', - TIMESERIES_DELETED = 'TIMESERIES_DELETED', - RPC_QUEUED = 'RPC_QUEUED', - RPC_DELIVERED = 'RPC_DELIVERED', - RPC_SUCCESSFUL = 'RPC_SUCCESSFUL', - RPC_TIMEOUT = 'RPC_TIMEOUT', - RPC_FAILED = 'RPC_FAILED' + TIMESERIES_DELETED = 'TIMESERIES_DELETED' } export const messageTypeNames = new Map( @@ -373,6 +380,14 @@ export const messageTypeNames = new Map( [MessageType.POST_TELEMETRY_REQUEST, 'Post telemetry'], [MessageType.TO_SERVER_RPC_REQUEST, 'RPC Request from Device'], [MessageType.RPC_CALL_FROM_SERVER_TO_DEVICE, 'RPC Request to Device'], + [MessageType.RPC_QUEUED, 'RPC Queued'], + [MessageType.RPC_SENT, 'RPC Sent'], + [MessageType.RPC_DELIVERED, 'RPC Delivered'], + [MessageType.RPC_SUCCESSFUL, 'RPC Successful'], + [MessageType.RPC_TIMEOUT, 'RPC Timeout'], + [MessageType.RPC_EXPIRED, 'RPC Expired'], + [MessageType.RPC_FAILED, 'RPC Failed'], + [MessageType.RPC_DELETED, 'RPC Deleted'], [MessageType.ACTIVITY_EVENT, 'Activity Event'], [MessageType.INACTIVITY_EVENT, 'Inactivity Event'], [MessageType.CONNECT_EVENT, 'Connect Event'], @@ -384,13 +399,12 @@ export const messageTypeNames = new Map( [MessageType.ENTITY_UNASSIGNED, 'Entity Unassigned'], [MessageType.ATTRIBUTES_UPDATED, 'Attributes Updated'], [MessageType.ATTRIBUTES_DELETED, 'Attributes Deleted'], + [MessageType.ALARM_ACKNOWLEDGED, 'Alarm Acknowledged'], + [MessageType.ALARM_CLEARED, 'Alarm Cleared'], + [MessageType.ENTITY_ASSIGNED_FROM_TENANT, 'Entity Assigned From Tenant'], + [MessageType.ENTITY_ASSIGNED_TO_TENANT, 'Entity Assigned To Tenant'], [MessageType.TIMESERIES_UPDATED, 'Timeseries Updated'], - [MessageType.TIMESERIES_DELETED, 'Timeseries Deleted'], - [MessageType.RPC_QUEUED, 'RPC Queued'], - [MessageType.RPC_DELIVERED, 'RPC Delivered'], - [MessageType.RPC_SUCCESSFUL, 'RPC Successful'], - [MessageType.RPC_TIMEOUT, 'RPC Timeout'], - [MessageType.RPC_FAILED, 'RPC Failed'] + [MessageType.TIMESERIES_DELETED, 'Timeseries Deleted'] ] ); diff --git a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts index 4b8c21b891..5fef4b832e 100644 --- a/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts +++ b/ui-ngx/src/app/shared/models/telemetry/telemetry.models.ts @@ -173,12 +173,35 @@ export interface TimeSeriesCmd { fetchLatestPreviousPoint?: boolean; } +export interface AggKey { + id: number; + key: string; + agg: AggregationType; + previousStartTs?: number; + previousEndTs?: number; + previousValueOnly?: boolean; +} + +export interface AggEntityHistoryCmd { + keys: Array; + startTs: number; + endTs: number; +} + +export interface AggTimeSeriesCmd { + keys: Array; + startTs: number; + timeWindow: number; +} + export class EntityDataCmd implements WebsocketCmd { cmdId: number; query?: EntityDataQuery; historyCmd?: EntityHistoryCmd; latestCmd?: LatestValueCmd; tsCmd?: TimeSeriesCmd; + aggHistoryCmd?: AggEntityHistoryCmd; + aggTsCmd?: AggTimeSeriesCmd; public isEmpty(): boolean { return !this.query && !this.historyCmd && !this.latestCmd && !this.tsCmd; @@ -212,15 +235,6 @@ export class AlarmDataUnsubscribeCmd implements WebsocketCmd { } export class TelemetryPluginCmdsWrapper { - attrSubCmds: Array; - tsSubCmds: Array; - historyCmds: Array; - entityDataCmds: Array; - entityDataUnsubscribeCmds: Array; - alarmDataCmds: Array; - alarmDataUnsubscribeCmds: Array; - entityCountCmds: Array; - entityCountUnsubscribeCmds: Array; constructor() { this.attrSubCmds = []; @@ -233,6 +247,24 @@ export class TelemetryPluginCmdsWrapper { this.entityCountCmds = []; this.entityCountUnsubscribeCmds = []; } + attrSubCmds: Array; + tsSubCmds: Array; + historyCmds: Array; + entityDataCmds: Array; + entityDataUnsubscribeCmds: Array; + alarmDataCmds: Array; + alarmDataUnsubscribeCmds: Array; + entityCountCmds: Array; + entityCountUnsubscribeCmds: Array; + + private static popCmds(cmds: Array, leftCount: number): Array { + const toPublish = Math.min(cmds.length, leftCount); + if (toPublish > 0) { + return cmds.splice(0, toPublish); + } else { + return []; + } + } public hasCommands(): boolean { return this.tsSubCmds.length > 0 || @@ -261,38 +293,33 @@ export class TelemetryPluginCmdsWrapper { public preparePublishCommands(maxCommands: number): TelemetryPluginCmdsWrapper { const preparedWrapper = new TelemetryPluginCmdsWrapper(); let leftCount = maxCommands; - preparedWrapper.tsSubCmds = this.popCmds(this.tsSubCmds, leftCount); + preparedWrapper.tsSubCmds = TelemetryPluginCmdsWrapper.popCmds(this.tsSubCmds, leftCount); leftCount -= preparedWrapper.tsSubCmds.length; - preparedWrapper.historyCmds = this.popCmds(this.historyCmds, leftCount); + preparedWrapper.historyCmds = TelemetryPluginCmdsWrapper.popCmds(this.historyCmds, leftCount); leftCount -= preparedWrapper.historyCmds.length; - preparedWrapper.attrSubCmds = this.popCmds(this.attrSubCmds, leftCount); + preparedWrapper.attrSubCmds = TelemetryPluginCmdsWrapper.popCmds(this.attrSubCmds, leftCount); leftCount -= preparedWrapper.attrSubCmds.length; - preparedWrapper.entityDataCmds = this.popCmds(this.entityDataCmds, leftCount); + preparedWrapper.entityDataCmds = TelemetryPluginCmdsWrapper.popCmds(this.entityDataCmds, leftCount); leftCount -= preparedWrapper.entityDataCmds.length; - preparedWrapper.entityDataUnsubscribeCmds = this.popCmds(this.entityDataUnsubscribeCmds, leftCount); + preparedWrapper.entityDataUnsubscribeCmds = TelemetryPluginCmdsWrapper.popCmds(this.entityDataUnsubscribeCmds, leftCount); leftCount -= preparedWrapper.entityDataUnsubscribeCmds.length; - preparedWrapper.alarmDataCmds = this.popCmds(this.alarmDataCmds, leftCount); + preparedWrapper.alarmDataCmds = TelemetryPluginCmdsWrapper.popCmds(this.alarmDataCmds, leftCount); leftCount -= preparedWrapper.alarmDataCmds.length; - preparedWrapper.alarmDataUnsubscribeCmds = this.popCmds(this.alarmDataUnsubscribeCmds, leftCount); + preparedWrapper.alarmDataUnsubscribeCmds = TelemetryPluginCmdsWrapper.popCmds(this.alarmDataUnsubscribeCmds, leftCount); leftCount -= preparedWrapper.alarmDataUnsubscribeCmds.length; - preparedWrapper.entityCountCmds = this.popCmds(this.entityCountCmds, leftCount); + preparedWrapper.entityCountCmds = TelemetryPluginCmdsWrapper.popCmds(this.entityCountCmds, leftCount); leftCount -= preparedWrapper.entityCountCmds.length; - preparedWrapper.entityCountUnsubscribeCmds = this.popCmds(this.entityCountUnsubscribeCmds, leftCount); + preparedWrapper.entityCountUnsubscribeCmds = TelemetryPluginCmdsWrapper.popCmds(this.entityCountUnsubscribeCmds, leftCount); return preparedWrapper; } - - private popCmds(cmds: Array, leftCount: number): Array { - const toPublish = Math.min(cmds.length, leftCount); - if (toPublish > 0) { - return cmds.splice(0, toPublish); - } else { - return []; - } - } } export interface SubscriptionData { - [key: string]: [number, any][]; + [key: string]: [number, any, number?][]; +} + +export interface IndexedSubscriptionData { + [id: number]: [number, any, number?][]; } export interface SubscriptionDataHolder { @@ -434,16 +461,7 @@ export class EntityDataUpdate extends DataUpdate { super(msg); } - public prepareData(tsOffset: number) { - if (this.data) { - this.processEntityData(this.data.data, tsOffset); - } - if (this.update) { - this.processEntityData(this.update, tsOffset); - } - } - - private processEntityData(data: Array, tsOffset: number) { + private static processEntityData(data: Array, tsOffset: number) { for (const entityData of data) { if (entityData.timeseries) { for (const key of Object.keys(entityData.timeseries)) { @@ -471,28 +489,28 @@ export class EntityDataUpdate extends DataUpdate { } } } + + public prepareData(tsOffset: number) { + if (this.data) { + EntityDataUpdate.processEntityData(this.data.data, tsOffset); + } + if (this.update) { + EntityDataUpdate.processEntityData(this.update, tsOffset); + } + } } export class AlarmDataUpdate extends DataUpdate { - allowedEntities: number; - totalEntities: number; constructor(msg: AlarmDataUpdateMsg) { super(msg); this.allowedEntities = msg.allowedEntities; this.totalEntities = msg.totalEntities; } + allowedEntities: number; + totalEntities: number; - public prepareData(tsOffset: number) { - if (this.data) { - this.processAlarmData(this.data.data, tsOffset); - } - if (this.update) { - this.processAlarmData(this.update, tsOffset); - } - } - - private processAlarmData(data: Array, tsOffset: number) { + private static processAlarmData(data: Array, tsOffset: number) { for (const alarmData of data) { alarmData.createdTime += tsOffset; if (alarmData.ackTs) { @@ -524,6 +542,15 @@ export class AlarmDataUpdate extends DataUpdate { } } } + + public prepareData(tsOffset: number) { + if (this.data) { + AlarmDataUpdate.processAlarmData(this.data.data, tsOffset); + } + if (this.update) { + AlarmDataUpdate.processAlarmData(this.update, tsOffset); + } + } } export class EntityCountUpdate extends CmdUpdate { diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index 2f90d39ca1..d657d5cda4 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -96,6 +96,8 @@ export interface Timewindow { displayValue?: string; displayTimezoneAbbr?: string; hideInterval?: boolean; + hideQuickInterval?: boolean; + hideLastInterval?: boolean; hideAggregation?: boolean; hideAggInterval?: boolean; hideTimezone?: boolean; @@ -188,6 +190,8 @@ export function defaultTimewindow(timeService: TimeService): Timewindow { return { displayValue: '', hideInterval: false, + hideLastInterval: false, + hideQuickInterval: false, hideAggregation: false, hideAggInterval: false, hideTimezone: false, @@ -223,10 +227,12 @@ function getTimewindowType(timewindow: Timewindow): TimewindowType { } } -export function initModelFromDefaultTimewindow(value: Timewindow, timeService: TimeService): Timewindow { +export function initModelFromDefaultTimewindow(value: Timewindow, quickIntervalOnly: boolean, timeService: TimeService): Timewindow { const model = defaultTimewindow(timeService); if (value) { model.hideInterval = value.hideInterval; + model.hideLastInterval = value.hideLastInterval; + model.hideQuickInterval = value.hideQuickInterval; model.hideAggregation = value.hideAggregation; model.hideAggInterval = value.hideAggInterval; model.hideTimezone = value.hideTimezone; @@ -281,6 +287,9 @@ export function initModelFromDefaultTimewindow(value: Timewindow, timeService: T } model.timezone = value.timezone; } + if (quickIntervalOnly) { + model.realtime.realtimeType = RealtimeWindowType.INTERVAL; + } return model; } @@ -304,6 +313,8 @@ export function toHistoryTimewindow(timewindow: Timewindow, startTimeMs: number, } return { hideInterval: timewindow.hideInterval || false, + hideLastInterval: timewindow.hideLastInterval || false, + hideQuickInterval: timewindow.hideQuickInterval || false, hideAggregation: timewindow.hideAggregation || false, hideAggInterval: timewindow.hideAggInterval || false, hideTimezone: timewindow.hideTimezone || false, @@ -694,6 +705,8 @@ export function createTimewindowForComparison(subscriptionTimewindow: Subscripti export function cloneSelectedTimewindow(timewindow: Timewindow): Timewindow { const cloned: Timewindow = {}; cloned.hideInterval = timewindow.hideInterval || false; + cloned.hideLastInterval = timewindow.hideLastInterval || false; + cloned.hideQuickInterval = timewindow.hideQuickInterval || false; cloned.hideAggregation = timewindow.hideAggregation || false; cloned.hideAggInterval = timewindow.hideAggInterval || false; cloned.hideTimezone = timewindow.hideTimezone || false; diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 35b3b0fc21..92b977e346 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -17,7 +17,7 @@ import { BaseData } from '@shared/models/base-data'; import { TenantId } from '@shared/models/id/tenant-id'; import { WidgetTypeId } from '@shared/models/id/widget-type-id'; -import { Timewindow } from '@shared/models/time/time.models'; +import { AggregationType, ComparisonDuration, Timewindow } from '@shared/models/time/time.models'; import { EntityType } from '@shared/models/entity-type.models'; import { AlarmSearchStatus, AlarmSeverity } from '@shared/models/alarm.models'; import { DataKeyType } from './telemetry/telemetry.models'; @@ -173,7 +173,7 @@ export interface WidgetTypeParameters { hasAdditionalLatestDataKeys?: boolean; warnOnPageDataOverflow?: boolean; ignoreDataUpdateOnIntervalTick?: boolean; - + processNoDataByWidget?: boolean; } export interface WidgetControllerDescriptor { @@ -259,8 +259,27 @@ export function defaultLegendConfig(wType: widgetType): LegendConfig { }; } +export enum ComparisonResultType { + PREVIOUS_VALUE = 'PREVIOUS_VALUE', + DELTA_ABSOLUTE = 'DELTA_ABSOLUTE', + DELTA_PERCENT = 'DELTA_PERCENT' +} + +export const comparisonResultTypeTranslationMap = new Map( + [ + [ComparisonResultType.PREVIOUS_VALUE, 'datakey.delta-calculation-result-previous-value'], + [ComparisonResultType.DELTA_ABSOLUTE, 'datakey.delta-calculation-result-delta-absolute'], + [ComparisonResultType.DELTA_PERCENT, 'datakey.delta-calculation-result-delta-percent'] + ] +); + export interface KeyInfo { name: string; + aggregationType?: AggregationType; + comparisonEnabled?: boolean; + timeForComparison?: ComparisonDuration; + comparisonCustomIntervalValue?: number; + comparisonResultType?: ComparisonResultType; label?: string; color?: string; funcBody?: string; @@ -269,6 +288,18 @@ export interface KeyInfo { decimals?: number; } +export const dataKeyAggregationTypeHintTranslationMap = new Map( + [ + [AggregationType.MIN, 'datakey.aggregation-type-min-hint'], + [AggregationType.MAX, 'datakey.aggregation-type-max-hint'], + [AggregationType.AVG, 'datakey.aggregation-type-avg-hint'], + [AggregationType.SUM, 'datakey.aggregation-type-sum-hint'], + [AggregationType.COUNT, 'datakey.aggregation-type-count-hint'], + [AggregationType.NONE, 'datakey.aggregation-type-none-hint'], + ] +); + + export interface DataKey extends KeyInfo { type: DataKeyType; pattern?: string; @@ -322,6 +353,37 @@ export interface Datasource { [key: string]: any; } +export function datasourcesHasAggregation(datasources?: Array): boolean { + if (datasources) { + const foundDatasource = datasources.find(datasource => { + const found = datasource.dataKeys && datasource.dataKeys.find(key => key.type === DataKeyType.timeseries && + key.aggregationType && key.aggregationType !== AggregationType.NONE); + return !!found; + }); + if (foundDatasource) { + return true; + } + } + return false; +} + +export function datasourcesHasOnlyComparisonAggregation(datasources?: Array): boolean { + if (!datasourcesHasAggregation(datasources)) { + return false; + } + if (datasources) { + const foundDatasource = datasources.find(datasource => { + const found = datasource.dataKeys && datasource.dataKeys.find(key => key.type === DataKeyType.timeseries && + key.aggregationType && key.aggregationType !== AggregationType.NONE && !key.comparisonEnabled); + return !!found; + }); + if (foundDatasource) { + return false; + } + } + return true; +} + export interface FormattedData { $datasource: Datasource; entityName: string; diff --git a/ui-ngx/src/app/shared/services/custom-paginator-intl.ts b/ui-ngx/src/app/shared/services/custom-paginator-intl.ts new file mode 100644 index 0000000000..ef2ab076d5 --- /dev/null +++ b/ui-ngx/src/app/shared/services/custom-paginator-intl.ts @@ -0,0 +1,40 @@ +/// +/// Copyright © 2016-2022 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 { MatPaginatorIntl } from '@angular/material/paginator'; +import { Subject } from 'rxjs'; +import { TranslateService } from '@ngx-translate/core'; + +@Injectable() +export class CustomPaginatorIntl implements MatPaginatorIntl { + constructor(private translate: TranslateService) {} + changes = new Subject(); + + firstPageLabel = this.translate.instant('paginator.first-page-label'); + itemsPerPageLabel = this.translate.instant('paginator.items-per-page'); + lastPageLabel = this.translate.instant('paginator.last-page-label'); + + nextPageLabel = this.translate.instant('paginator.next-page-label'); + previousPageLabel = this.translate.instant('paginator.previous-page-label'); + separator = this.translate.instant('paginator.items-per-page-separator'); + + getRangeLabel(page: number, pageSize: number, length: number): string { + const startNumber = page * pageSize + 1; + const endNumber = pageSize * (page + 1); + return `${startNumber} – ${endNumber > length ? length : endNumber} ${this.separator} ${length}`; + } +} diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index aaa29f02c0..8cb71e0d9d 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -37,7 +37,7 @@ import { MatGridListModule } from '@angular/material/grid-list'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatMenuModule } from '@angular/material/menu'; -import { MatPaginatorModule } from '@angular/material/paginator'; +import { MatPaginatorIntl, MatPaginatorModule } from '@angular/material/paginator'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatRadioModule } from '@angular/material/radio'; @@ -53,7 +53,7 @@ import { MatTabsModule } from '@angular/material/tabs'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatTooltipModule } from '@angular/material/tooltip'; import { MatListModule } from '@angular/material/list'; -import { MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core'; +import { DatetimeAdapter, MatDatetimepickerModule, MatNativeDatetimeModule } from '@mat-datetimepicker/core'; import { NgxDaterangepickerMd } from 'ngx-daterangepicker-material'; import { GridsterModule } from 'angular-gridster2'; import { FlexLayoutModule } from '@angular/flex-layout'; @@ -165,6 +165,8 @@ import { DragDropModule } from '@angular/cdk/drag-drop'; import { MultipleImageInputComponent } from '@shared/components/multiple-image-input.component'; import { BranchAutocompleteComponent } from '@shared/components/vc/branch-autocomplete.component'; import { PhoneInputComponent } from '@shared/components/phone-input.component'; +import { CustomDateAdapter } from '@shared/adapter/custom-datatime-adapter'; +import { CustomPaginatorIntl } from '@shared/services/custom-paginator-intl'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -188,8 +190,10 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) provide: MAT_DATE_LOCALE, useValue: 'en-GB' }, + { provide: DatetimeAdapter, useClass: CustomDateAdapter }, { provide: HELP_MARKDOWN_COMPONENT_TOKEN, useValue: HelpMarkdownComponent }, { provide: SHARED_MODULE_TOKEN, useValue: SharedModule }, + { provide: MatPaginatorIntl, useClass: CustomPaginatorIntl }, TbPopoverService ], declarations: [ diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_disabled_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_disabled_fn.md index b532cfa3f8..7c74cccae3 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_disabled_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_disabled_fn.md @@ -10,6 +10,9 @@ A JavaScript function evaluating whether current node should be disabled (not se **Parameters:**
    +
  • widgetCtx: WidgetContext - A reference to WidgetContext that has all necessary API + and data used by widget instance. +
  • nodeCtx: HierarchyNodeContext - An HierarchyNodeContext object containing entity field holding basic entity properties
    (ex. id, name, label) and data field holding other entity attributes/timeseries declared in widget datasource configuration. diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_has_children_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_has_children_fn.md index 1a71858c9b..9e1888dfb0 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_has_children_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_has_children_fn.md @@ -10,6 +10,9 @@ A JavaScript function evaluating whether current node has children (whether it c **Parameters:**
      +
    • widgetCtx: WidgetContext - A reference to WidgetContext that has all necessary API + and data used by widget instance. +
    • nodeCtx: HierarchyNodeContext - An HierarchyNodeContext object containing entity field holding basic entity properties
      (ex. id, name, label) and data field holding other entity attributes/timeseries declared in widget datasource configuration. diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_icon_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_icon_fn.md index 1a3c0aec8e..2da8c7932c 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_icon_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_icon_fn.md @@ -10,6 +10,9 @@ A JavaScript function used to compute node icon info. **Parameters:**
        +
      • widgetCtx: WidgetContext - A reference to WidgetContext that has all necessary API + and data used by widget instance. +
      • nodeCtx: HierarchyNodeContext - An HierarchyNodeContext object containing entity field holding basic entity properties
        (ex. id, name, label) and data field holding other entity attributes/timeseries declared in widget datasource configuration. diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_opened_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_opened_fn.md index e85a2b1aa9..08c9607f0b 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_opened_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_opened_fn.md @@ -10,6 +10,9 @@ A JavaScript function evaluating whether current node should be opened (expanded **Parameters:**
          +
        • widgetCtx: WidgetContext - A reference to WidgetContext that has all necessary API + and data used by widget instance. +
        • nodeCtx: HierarchyNodeContext - An HierarchyNodeContext object containing entity field holding basic entity properties
          (ex. id, name, label) and data field holding other entity attributes/timeseries declared in widget datasource configuration. diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_relation_query_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_relation_query_fn.md index f312483d07..2120fe4eea 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_relation_query_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_relation_query_fn.md @@ -10,6 +10,9 @@ A JavaScript function used to compute child nodes relations query for current no **Parameters:**
            +
          • widgetCtx: WidgetContext - A reference to WidgetContext that has all necessary API + and data used by widget instance. +
          • nodeCtx: HierarchyNodeContext - An HierarchyNodeContext object containing entity field holding basic entity properties
            (ex. id, name, label) and data field holding other entity attributes/timeseries declared in widget datasource configuration. diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_text_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_text_fn.md index 76b70a73fb..5ce79b7d15 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_text_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/node_text_fn.md @@ -10,6 +10,9 @@ A JavaScript function used to compute text or HTML code for the current node. **Parameters:**
              +
            • widgetCtx: WidgetContext - A reference to WidgetContext that has all necessary API + and data used by widget instance. +
            • nodeCtx: HierarchyNodeContext - An HierarchyNodeContext object containing entity field holding basic entity properties
              (ex. id, name, label) and data field holding other entity attributes/timeseries declared in widget datasource configuration. diff --git a/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/nodes_sort_fn.md b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/nodes_sort_fn.md index 779424b553..2c2375f1e4 100644 --- a/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/nodes_sort_fn.md +++ b/ui-ngx/src/assets/help/en_US/widget/lib/entities_hierarchy/nodes_sort_fn.md @@ -10,6 +10,9 @@ A JavaScript function used to compare nodes of the same level when sorting. **Parameters:**
                +
              • widgetCtx: WidgetContext - A reference to WidgetContext that has all necessary API + and data used by widget instance. +
              • nodeCtx1: HierarchyNodeContext - First node object to be compared.
              • diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 7216f7b707..1fd934d92d 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1039,7 +1039,22 @@ "value-description": "the current value;", "prev-value-description": "result of the previous function call;", "time-prev-description": "timestamp of the previous value;", - "prev-orig-value-description": "original previous value;" + "prev-orig-value-description": "original previous value;", + "aggregation": "Aggregation", + "aggregation-type-hint-common": "For performance reasons, the aggregated values calculation is available only for fixed time intervals like \"current day\", \"current month\", etc, and is not available for sliding window intervals like 'last 30 minutes' or 'last 24 hours'.", + "aggregation-type-none-hint": "Take latest value.", + "aggregation-type-min-hint": "Find minimum value among data points within a selected time window.", + "aggregation-type-max-hint": "Find maximum value among data points within a selected time window.", + "aggregation-type-avg-hint": "Calculate an average value among data points within a selected time window.", + "aggregation-type-sum-hint": "Sum all values of the data points within a selected time window.", + "aggregation-type-count-hint": "Total number of the data points within a selected time window.", + "delta-calculation": "Delta calculation", + "enable-delta-calculation": "Enable delta calculation", + "enable-delta-calculation-hint": "When enabled, the data key value is calculated based on the aggregated values for a selected time window and a specified comparison period. For performance reasons, the delta calculation is available only for history time windows and not for real-time values. For example, you may calculate the delta between the energy consumption for yesterday compared to the energy consumption for the day before yesterday.", + "delta-calculation-result": "Delta calculation result", + "delta-calculation-result-previous-value": "Previous value", + "delta-calculation-result-delta-absolute": "Delta (absolute)", + "delta-calculation-result-delta-percent": "Delta (percent)" }, "datasource": { "type": "Datasource type", @@ -4656,6 +4671,14 @@ "row-double-click": "On row double click" } }, + "paginator" : { + "items-per-page": "Items per page:", + "first-page-label": "First page", + "last-page-label": "Last page", + "next-page-label": "Next page", + "previous-page-label": "Previous page", + "items-per-page-separator": "of" + }, "language": { "language": "Language", "locales": { @@ -4668,6 +4691,7 @@ "ko_KR": "한국어", "ru_RU": "Русский", "es_ES": "Español", + "es_CA": "Catalan", "ja_JP": "日本語", "tr_TR": "Türkçe", "fa_IR": "فارسي", diff --git a/ui-ngx/src/assets/locale/locale.constant-es_CA.json b/ui-ngx/src/assets/locale/locale.constant-es_CA.json new file mode 100644 index 0000000000..3d6f2c163c --- /dev/null +++ b/ui-ngx/src/assets/locale/locale.constant-es_CA.json @@ -0,0 +1,5830 @@ +{ + "access": { + "unauthorized": "No autoritzat", + "unauthorized-access": "Accés no autoritzat", + "unauthorized-access-text": "Has d'iniciar sessió per tenir accés a aquest recurs!", + "access-forbidden": "Accés prohibit", + "access-forbidden-text": "No tens suficients drets per accedir a aquesta ubicació!
                Intenta iniciar sessió amb un altre usuari si encara vols accedir a aquesta ubicació.", + "refresh-token-expired": "La sessió ha expirat", + "refresh-token-failed": "No és pot actualitzar la sessió", + "permission-denied": "Permís Denegat", + "permission-denied-text": "No tens suficients drets per fer aquesta operació!" + }, + "action": { + "activate": "Activar", + "suspend": "Suspendre", + "save": "Guardar", + "saveAs": "Guardar com", + "cancel": "Cancel·lar", + "ok": "OK", + "delete": "Eliminar", + "add": "Afegir", + "yes": "Sí", + "no": "No", + "update": "Actualitzar", + "remove": "Eliminar", + "select": "Seleccionar", + "search": "Cercar", + "clear-search": "Eliminar la recerca", + "assign": "Assignar", + "unassign": "Anul·lar l'assignació", + "share": "Compartir", + "make-private": "Fer privat", + "make-public": "Fer públic", + "apply": "Aplicar", + "apply-changes": "Aplicar canvis", + "edit-mode": "Mode Edició", + "enter-edit-mode": "Mode Edició", + "decline-changes": "Desfer canvis", + "open": "Obrir", + "close": "Tancar", + "back": "Darrere", + "run": "Executar", + "sign-in": "Entrar!", + "edit": "Editar", + "view": "Veure", + "create": "Crear", + "drag": "Arrossegar", + "refresh": "Actualitzar", + "undo": "Desfer", + "copy": "Copiar", + "paste": "Pegar", + "copy-reference": "Copiar referència", + "paste-reference": "Pegar referència", + "import": "Importar", + "export": "Exportar", + "share-via": "Compartir via {{provider}}", + "move": "Moure", + "continue": "Continuar", + "discard-changes": "Cancelar canvis", + "download": "Descarregar", + "next-with-label": "Següent: {{label}}", + "read-more": "Llegir més", + "hide": "Ocultar", + "done": "Acabat", + "print": "Imprimir", + "restore": "Restaurar", + "confirm": "Confirmar" + }, + "aggregation": { + "aggregation": "Agrupació", + "function": "Funció d'Agrupació", + "limit": "Valors Max", + "group-interval": "Interval d'agrupació", + "min": "Min", + "max": "Max", + "avg": "Mitjana", + "sum": "Suma", + "count": "Contar", + "none": "Ningú" + }, + "admin": { + "general": "General", + "general-settings": "Configuració general", + "home-settings": "Configuració d'inici", + "outgoing-mail": "Servidor de correu", + "outgoing-mail-settings": "Configuració del servidor de correu de sortida", + "system-settings": "Sistema", + "test-mail-sent": "Correu de prova enviat correctament!", + "base-url": "Base d'URL", + "base-url-required": "Base d'URL requerida.", + "prohibit-different-url": "Prohibir l'ús de hostname a capçaleres de sol·licitud del client", + "prohibit-different-url-hint": "Aquest ajust ha de ser activat en entorns de producció. Pot ocasionar errors de seguretat si està desactivat", + "mail-from": "Correu de", + "mail-from-required": "Cal correu electrònic", + "smtp-protocol": "Protocol SMTP", + "smtp-host": "Host SMTP", + "smtp-host-required": "Host SMTP requerit.", + "smtp-port": "Port SMTP", + "smtp-port-required": "Has d'ingresar un Port SMTP.", + "smtp-port-invalid": "No sembla un Port SMTP vàlid.", + "timeout-msec": "Temps d'espera (ms)", + "timeout-required": "Temps d'espera requerit.", + "timeout-invalid": "No sembla un temps d'espera vàlid.", + "enable-tls": "Habilitar TLS", + "tls-version": "Versió TLS", + "enable-proxy": "Habilitar proxy", + "proxy-host": "Host proxy", + "proxy-host-required": "Es necessita port Proxy.", + "proxy-port": "Port proxy", + "proxy-port-required": "Es necessita port Proxy.", + "proxy-port-range": "El port proxy ha d'estar amb un rang d'1 a 655535.", + "proxy-user": "Usuari proxy", + "proxy-password": "Contrasenya proxy", + "change-password": "Canvia la contrasenya", + "send-test-mail": "Enviar correu de prova", + "use-system-mail-settings": "Utilitzar la configuració del servidor de correu del sistema.", + "mail-templates": "Plantilles de correu.", + "mail-template-settings": "Configuració de plantilles de correu.", + "use-system-mail-template-settings": "Utilitza les plantilles de correu del sistema", + "mail-template": { + "mail-template": "Plantilles del correu", + "test": "Missatge de prova de correu", + "activation": "Missatge d'activació de compte", + "account-activated": "Missatge de compte activat", + "account-lockout": "Missatge de bloqueig del compte", + "reset-password": "Missatge per a restabliment de contrasenya", + "password-was-reset": "Missatge de contrasenya restablerta", + "user-activated": "Missatge activat per l'usuari", + "user-registered": "Missatge registrat de l'usuari", + "api-usage-state-enabled": "Estat d'ús de l'API activat", + "api-usage-state-warning": "Avís d'estat d'ús de l'API", + "api-usage-state-disabled": "Estat d'ús de l'API desactivat" + }, + "mail-subject": "Assumpte del correu", + "mail-body": "Cos del correu", + "sms-provider": "Proveïdor SMS", + "sms-provider-settings": "Configuració proveïdor SMS", + "use-system-sms-settings": "Utilitza la configuració del proveïdor de SMS del sistema", + "sms-provider-type": "Tipus de proveïdor SMS", + "sms-provider-type-required": "Es requereix proveïdor SMS.", + "sms-provider-type-aws-sns": "Amazon SNS", + "sms-provider-type-twilio": "Twilio", + "sms-provider-type-smpp": "SMPP", + "aws-access-key-id": "ID de clau d'accés AWS", + "aws-access-key-id-required": "Es requereix ID de clau d'accés AWS", + "aws-secret-access-key": "Clau d'accés secret AWS", + "aws-secret-access-key-required": "Es requereix Clau d'accés secret AWS", + "aws-region": "Regió AWS", + "aws-region-required": "Es requereix regió AWS", + "number-from": "Núm. de telèfon Origen", + "number-from-required": "Es requereix Núm. de telèfon origen.", + "number-to": "Núm. de telèfon de destí", + "number-to-required": "Es requereix Núm. de telèfon de destí.", + "phone-number-hint": "Núm. de telèfon en format E.164, ex. +3435550123", + "phone-number-hint-twilio": "Núm. de telèfon en format E.164/SID del número de telèfon/SID del servei de missatgeria, ex. +3435550123/PNXXX/MGXXX", + "phone-number-pattern": "Núm. Invàlid. Ha d'estar en format E.164, ex. +3435550123.", + "phone-number-pattern-twilio": "El número de telèfon no és vàlid. Hauria d'estar en el format E.164/SIF/Servei de Missatgeria del Número de telèfon, ex. +3435550123/PNXXX/MGXXX.", + "sms-message": "Missatge SMS", + "sms-message-required": "Es requereix missatge SMS.", + "sms-message-max-length": "Els SMS no poden ser més llargs de 1600 caràcters", + "twilio-account-sid": "SID de compte Twilio", + "twilio-account-sid-required": "Es requereix SID de compte Twilio", + "twilio-account-token": "Token de compte Twilio", + "twilio-account-token-required": "Es requereix Token Twilio", + "send-test-sms": "Enviar SMS de prova", + "test-sms-sent": "SMS enviat amb èxit!", + "security-settings": "Configuracions de seguretat", + "password-policy": "Política de contrasenyes", + "minimum-password-length": "Longitud mínima de contrasenya", + "minimum-password-length-required": "Es requereix una longitud mínima de contrasenya", + "minimum-password-length-range": "La longitud mínima de la contrasenya ha d'estar en un rang de 5 a 50", + "minimum-uppercase-letters": "Nom mínim de lletres majúscules", + "minimum-uppercase-letters-range": "El nom mínim de lletres majúscules no pot ser negatiu", + "minimum-lowercase-letters": "Nom mínim de lletres minúscules", + "minimum-lowercase-letters-range": "El nom mínim de lletres minúscules no pot ser negatiu", + "minimum-digits": "Nom mínim de dígits", + "minimum-digits-range": "El nom mínim de dígits no pot ser negatiu", + "minimum-special-characters": "Nom mínim de caràcters especials.", + "minimum-special-characters-range": "El nom mínim de caràcters especials no pot ser negatiu.", + "password-expiration-period-days": "Període de caducitat de contrasenya en dies", + "password-expiration-period-days-range": "El període de caducitat de la contrasenya en dies no pot ser negatiu", + "password-reuse-frequency-days": "Freqüència de reutilització de contrasenya en dies", + "password-reuse-frequency-days-range": "La freqüència de reutilització de contrasenya en dies no pot ser negativa", + "allow-whitespace": "Permet espais en blanc", + "general-policy": "Política general", + "max-failed-login-attempts": "Nom màxim d'intents fallits d'inici de sessió, abans que el compte estigui bloquejat", + "minimum-max-failed-login-attempts-range": "El nom màxim d'intents fallits d'inici de sessió no pot ser negatiu", + "user-lockout-notification-email": "En cas de bloqueig del compte de l'usuari, enviï una notificació per correu electrònic", + "domain-name": "Nom de domini", + "domain-name-unique": "El nom de domini i protocol ha de ser únic.", + "domain-name-max-length": "El nom del domini ha de ser inferior a 256", + "error-verification-url": "Un nom de domini no ha de contenir símbols '/' i ':'. Exemple: thingsboard.io", + "oauth2": { + "access-token-uri": "URI Access token", + "access-token-uri-required": "Es requereix URI Access token.", + "activate-user": "Activar usuari", + "add-domain": "Afegir domini", + "delete-domain": "Esborrar domini", + "add-provider": "Afegir proveïdor", + "delete-provider": "Esborrar proveïdor", + "allow-user-creation": "Permetre creació d'usuari", + "always-fullscreen": "Sempre pantalla completa", + "authorization-uri": "URI Autorizació", + "authorization-uri-required": "Es requereix l'autorització d'URI.", + "client-authentication-method": "Mètode d'autenticació", + "client-id": "ID Client", + "client-id-required": "Es requereix ID Client.", + "client-id-max-length": "L'identificador del client ha de ser inferior a 256", + "client-secret": "Secret de Client", + "client-secret-required": "Es requereix Secret de Client.", + "client-secret-max-length": "El secret del client hauria de ser inferior a 2049", + "custom-setting": "Configuració personalitzats", + "customer-name-pattern": "Patró nomeni de client", + "customer-name-pattern-max-length": "El patró del nom del client hauria de ser inferior a 256", + "parent-customer-name-pattern": "Patró del nom del client pare", + "user-groups-name-pattern": "Patró de nom de grups d'usuaris", + "default-dashboard-name": "Nom de panell per defecte", + "default-dashboard-name-max-length": "El nom del tauler predeterminat hauria de ser inferior a 256", + "delete-domain-text": "Atenció, després de la confirmació el domini i totes les dades del proveïdor no estaran disponibles.", + "delete-domain-title": "Eliminar els configuració del domini '{{domainName}}'?", + "delete-registration-text": "Atenció, després de la confirmació les dades del proveïdor no estaran disponibles.", + "delete-registration-title": "Eliminar el proveïdor '{{name}}'?", + "email-attribute-key": "Clau d'atribut de correu electrònic", + "email-attribute-key-required": "Cal la clau de l'atribut de correu electrònic.", + "email-attribute-key-max-length": "La clau de l'atribut de correu electrònic ha de ser inferior a 32", + "first-name-attribute-key": "Clau de atributs de nom", + "first-name-attribute-key-max-length": "La clau de l'atribut de nom ha de ser inferior a 32", + "general": "General", + "jwk-set-uri": "URI web key JSON", + "last-name-attribute-key": "Clau d'atributs de cognom", + "last-name-attribute-key-max-length": "La clau de l'atribut del cognom ha de ser inferior a 32", + "login-button-icon": "Icona del botó d'inici de sessió", + "login-button-label": "Etiqueta de proveïdor", + "login-button-label-placeholder": "Inicia la sessió amb $(Provider label)", + "login-button-label-required": "Clau d'etiqueta requerida.", + "login-provider": "Proveïdor d'inici de sessió", + "mapper": "Cartografiador", + "new-domain": "Nou domini", + "oauth2": "OAuth2", + "password-max-length": "La contrasenya ha de ser inferior a 256", + "redirect-uri-template": "Plantilla de redirecció URI", + "copy-redirect-uri": "Copiar URI de redirecció", + "registration-id": "ID de registre", + "registration-id-required": "Es requereix ID de registre.", + "registration-id-unique": "L'ID de registre ha de ser únic en el sistema.", + "scope": "Abast", + "scope-required": "Es requereix abast.", + "tenant-name-pattern": "Patró de nom de propietari", + "tenant-name-pattern-required": "Es requereix patró del nom del propietari.", + "tenant-name-pattern-max-length": "El patró de nom de l'inquilí pot ser inferior a 256", + "tenant-name-strategy": "Estratègia de Nom de Propietari", + "type": "Tipus de cartografiador", + "uri-pattern-error": "Format de URI invàlid.", + "url": "URL", + "url-pattern": "Format URL invàlid.", + "url-required": "Es requereix URL.", + "url-max-length": "L'URL ha de ser inferior a 256", + "user-info-uri": "URI Informació de l'usuari", + "user-info-uri-required": "Es requereix URI de la informació usuari.", + "username-max-length": "El nom d'usuari ha de ser inferior a 256", + "user-name-attribute-name": "Clau d'atributs de nom d'usuari", + "user-name-attribute-name-required": "Es requereix clau d'atributs de nom d'usuari", + "protocol": "Protocol", + "domain-schema-http": "HTTP", + "domain-schema-https": "HTTPS", + "domain-schema-mixed": "HTTP+HTTPS", + "enable": "Activar configuració OAuth2", + "domains": "Dominis", + "mobile-apps": "Aplicacions mòbils", + "no-mobile-apps": "No s'ha configurat cap aplicació", + "mobile-package": "Paquet d'aplicació", + "mobile-package-placeholder": "Ex.: my.example.app", + "mobile-package-hint": "Per a Android: el vostre propi identificador únic d'aplicació. Per a iOS: identificador de paquet de producte", + "mobile-package-unique": "El paquet d'aplicació ha de ser únic.", + "mobile-app-secret": "Secret de l'aplicació", + "invalid-mobile-app-secret": "El secret de l'aplicació ha de contenir només caràcters alfanumèrics i ha de tenir entre 16 i 2048 caràcters de longitud.", + "copy-mobile-app-secret": "Copia el secret de l'aplicació", + "add-mobile-app": "Afegeix una aplicació", + "delete-mobile-app": "Suprimeix la informació de l'aplicació", + "providers": "Proveïdors", + "platform-web": "Web", + "platform-android": "Android", + "platform-ios": "iOS", + "all-platforms": "Totes les plataformes", + "allowed-platforms": "Plataformes permeses" + }, + "smpp-provider": { + "smpp-version": "Versión SMPP", + "smpp-host": "Host SMPP", + "smpp-host-required": "Host SMPP requerido", + "smpp-port": "Puerto SMPP", + "smpp-port-required": "Puerto SMPP requerido", + "system-id": "System ID", + "system-id-required": "System ID requerido", + "password": "Contraseña", + "password-required": "Contraseña requerida", + "type-settings": "Configuració de tipo", + "source-settings": "Configuració de origen", + "destination-settings": "Configuració de destino", + "additional-settings": "Configuració adicionales", + "system-type": "Tipo de sistema", + "bind-type": "Tipo de enlace", + "service-type": "Tipo de servicio", + "source-address": "Dirección de origen", + "source-ton": "Origen TON", + "source-npi": "Origen NPI", + "destination-ton": "Destino TON (Tipo de número)", + "destination-npi": "Destino NPI (Numbering Plan Identification)", + "address-range": "Rango de dirección", + "coding-scheme": "Esquema de codificación", + "bind-type-tx": "Transmisor", + "bind-type-rx": "Receptor", + "bind-type-trx": "Transceptor", + "ton-unknown": "Desconocido", + "ton-international": "Internacional", + "ton-national": "Nacional", + "ton-network-specific": "Específico de red", + "ton-subscriber-number": "Número de suscriptor", + "ton-alphanumeric": "Alfanumérico", + "ton-abbreviated": "Abreviado", + "npi-unknown": "0 - Desconocido", + "npi-isdn": "1 - RDSI/Teléfono plan numérico (E163/E164)", + "npi-data-numbering-plan": "3 - Datos plan numérico (X.121)", + "npi-telex-numbering-plan": "4 - Telex plan numérico (F.69)", + "npi-land-mobile": "6 - Línea Móvil (E.212)", + "npi-national-numbering-plan": "8 - Nacional", + "npi-private-numbering-plan": "9 - Privado", + "npi-ermes-numbering-plan": "10 - ERMES (ETSI DE/PS 3 01-3)", + "npi-internet": "13 - Internet (IP)", + "npi-wap-client-id": "18 - WAP Client Id (a definir por WAP Forum)", + "scheme-smsc": "0 - Alfabeto por defecto SMSC (ASCII para códigos cortos y largos y GSM para gratuitos)", + "scheme-ia5": "1 - IA5 (ASCII para códigos cortos y largos, Latin 9 para gratuitos (ISO-8859-9))", + "scheme-octet-unspecified-2": "2 - Octetos sin especificar (binario 8-bit)", + "scheme-latin-1": "3 - Latin 1 (ISO-8859-1)", + "scheme-octet-unspecified-4": "4 - Octetos sin especificar (binario 8-bit)", + "scheme-jis": "5 - JIS (X 0208-1990)", + "scheme-cyrillic": "6 - Ciríllico (ISO-8859-5)", + "scheme-latin-hebrew": "7 - Latin/Hebreo (ISO-8859-8)", + "scheme-ucs-utf": "8 - UCS2/UTF-16 (ISO/IEC-10646)", + "scheme-pictogram-encoding": "9 - Codificación por Pictograma", + "scheme-music-codes": "10 - Códigos musicales (ISO-2022-JP)", + "scheme-extended-kanji-jis": "13 - Kanji extendido JIS (X 0212-1990)", + "scheme-korean-graphic-character-set": "14 - Set de carácteres Koreanos (KS C 5601/KS X 1001)" + }, + "queue-select-name": "Seleccionar nom de cua", + "queue-name": "Nom", + "queue-name-required": "Cal el nom de cua!", + "queues": "Cues", + "queue-partitions": "Particions", + "queue-submit-strategy": "Estratègia d'enviaments", + "queue-processing-strategy": "Estratègia de processament", + "queue-configuration": "Configuració Cua", + "repository-settings": "Ajustos del repositori", + "repository-url": "URL del repositori", + "repository-url-required": "Es requereix la URL del repositori.", + "default-branch": "Nom de branca per defecte", + "authentication-settings": "Ajustos d'autenticació", + "auth-method": "Mètode d'autenticació", + "auth-method-username-password": "Usuari / Contrasenya", + "auth-method-private-key": "Clau privada", + "password-access-token": "Contrasenya / Tóken d'accés", + "change-password-access-token": "Canviar contrasenya / Tóken d'accés", + "private-key": "Clau privada", + "drop-private-key-file-or": "Arrossegar i deixar anar un fitxer de clau privada o", + "passphrase": "Frase de contrasenya", + "enter-passphrase": "Entrar frase de contrasenya", + "change-passphrase": "Canviar frase de contrasenya", + "check-access": "Verificar accés", + "check-repository-access-success": "Accés al repositori verificat satisfactòriament!", + "delete-repository-settings-title": "Estàs segur d'esborrar els ajustos del repositori?", + "delete-repository-settings-text": "Atenció, després de la confirmació els ajustos del repositori seran eliminats i la característica de control de versions no estarà disponible.", + "auto-commit-settings": "Ajustis Acte-publicar", + "auto-commit-entities": "Entitats Acte-publicar", + "no-auto-commit-entities-prompt": "No hi ha entitats configurades per a acte-publicar", + "delete-auto-commit-settings-title": "Estàs segur d'esborrar els ajustos d'acte-publicar?", + "delete-auto-commit-settings-text": "Atenció, després de la confirmació els ajustos d'acte-publicar seran esborrats i la característica d'acte-publicar es desactivarà per a totes les entitats.", + "2fa": { + "2fa": "Autenticació de dos factors (2FA)", + "available-providers": "Proveïdors disponibles", + "issuer-name": "Nom d'emissor", + "issuer-name-required": "Cal el nom d'emissor.", + "max-verification-failures-before-user-lockout": "Màxim de fallades de verificació abans de bloquejar compte", + "max-verification-failures-before-user-lockout-pattern": "Màxim de fallades ha de ser un nombre enter positiu.", + "number-of-checking-attempts": "Nombre d'intents de verificació", + "number-of-checking-attempts-pattern": "Nombre d'intents ha de ser un nombre enter positiu.", + "number-of-checking-attempts-required": "Cal el nombre d'intents.", + "number-of-codes": "Nombre de codis", + "number-of-codes-pattern": "Nombre de codis ha de ser un nombre enter positiu.", + "number-of-codes-required": "Cal el nombre de codis.", + "provider": "Proveïdor", + "retry-verification-code-period": "reintents de codi de verificació (seg)", + "retry-verification-code-period-pattern": "El període mínim és de 5 seg", + "retry-verification-code-period-required": "Cal reintent.", + "total-allowed-time-for-verification": "Total de temps permès per a verificació (seg)", + "total-allowed-time-for-verification-pattern": "El mínim del total de temps permès és de 60 seg", + "total-allowed-time-for-verification-required": "Cal total del temps.", + "use-system-two-factor-auth-settings": "Usar ajustos 2FA del sistema", + "verification-code-check-rate-limit": "Límit de revisió mèdica del codi de verificació", + "verification-code-lifetime": "Temps de vida del codi de verificació (seg)", + "verification-code-lifetime-pattern": "Temps de vida del codi de verificació ha de ser un nombre enter positiu.", + "verification-code-lifetime-required": "Cal el temps de vida.", + "verification-message-template": "Plantilla del missatge de verificació", + "verification-limitations": "Límits de verificació", + "verification-message-template-pattern": "El missatge de verificació ha de contenir el patró: ${code}", + "verification-message-template-required": "Cal plantilla de missatge de verificació.", + "within-time": "Rang de temps (seg)", + "within-time-pattern": "Temps ha de ser un nombre enter positiu.", + "within-time-required": "Cal el temps." + } + }, + "alarm": { + "alarm": "Alarma", + "alarms": "Alarmes", + "select-alarm": "Seleccionar alarma", + "no-alarms-matching": "No s'han trobat alarmes coincidents amb '{{entity}}' .", + "alarm-required": "Cal alarma", + "alarm-status": "Estat d'alarma", + "alarm-status-list": "Llista d'estats d'Alarmes", + "any-status": "Qualsevol estat", + "search-status": { + "ANY": "Totes", + "ACTIVE": "Actives", + "CLEARED": "Borrades", + "ACK": "Reconegudes", + "UNACK": "Ignorades" + }, + "display-status": { + "ACTIVE_UNACK": "Activa no reconeguda", + "ACTIVE_ACK": "Activa reconeguda", + "CLEARED_UNACK": "Normalitzada no reconeguda", + "CLEARED_ACK": "Normalitzada reconeguda" + }, + "no-alarms-prompt": "No és van trobar alarmes", + "created-time": "Hora de creació", + "type": "Tipus", + "severity": "Gravetat", + "originator": "Origen", + "originator-type": "Tipus d'origen", + "details": "Detalls", + "status": "Estat", + "alarm-details": "Detalls Alarma", + "start-time": "Hora d'inici", + "end-time": "Hora fi", + "ack-time": "Hora de reconeixement", + "clear-time": "Hora de normalització", + "alarm-severity-list": "Llista de gravetat d'alarmes", + "any-severity": "Qualsevol gravetat", + "severity-critical": "Crítica", + "severity-major": "Major", + "severity-minor": "Menor", + "severity-warning": "Perill", + "severity-indeterminate": "Indeterminada", + "acknowledge": "Reconèixer", + "clear": "Normalitzar", + "search": "Buscar alarmes", + "selected-alarms": "{ count, plural, 1 {1 alarma} other {# alarmas} } seleccionades", + "no-data": "Sense dades que mostrar", + "polling-interval": "Cicle de refresc d'alarmes (seg)", + "polling-interval-required": "Es requereix un cicle de refresc vàlid.", + "min-polling-interval-message": "El cicle ha de ser almenys d'1 segon.", + "aknowledge-alarms-title": "Reconèixer { count, plural, 1 {1 alarma} other {# alarmas} }", + "aknowledge-alarms-text": "Aquestes segur de reconèixer { count, plural, 1 {1 alarma} other {# alarmas} }?", + "aknowledge-alarm-title": "Reconèixer Alarma", + "aknowledge-alarm-text": "Estàs segur de reconèixer Alarma?", + "clear-alarms-title": "Normalitzar { count, plural, 1 {1 alarma} other {# alarmas} }", + "clear-alarms-text": "Netejar { count, plural, 1 {1 alarma} other {# alarmas} }?", + "clear-alarm-title": "Netejar Alarma", + "clear-alarm-text": "Netejar Alarma?", + "alarm-status-filter": "Filtre d'estats d'Alarmes", + "alarm-filter": "Filtre d'Alarmes", + "max-count-load": "Nom màxim d'alarmes a carregar (0 - ilimitado)", + "max-count-load-required": "Es requereix nom màxim d'alarmes.", + "max-count-load-error-min": "El valor mínim és 0.", + "fetch-size": "Mida de cerca (Fetch)", + "fetch-size-required": "Es requereix grandària de cerca.", + "fetch-size-error-min": "El valor mínim és 10.", + "alarm-type-list": "Llista de tipus d'alarma", + "any-type": "Qualsevol tipus", + "search-propagated-alarms": "Buscar alarmes propagades" + }, + "alias": { + "add": "Afegir àlies", + "edit": "Editar àlies", + "name": "Nom Àlies", + "name-required": "Es requereix nom d'àlies", + "duplicate-alias": "Ja existeix aquest nom d'àlies.", + "filter-type-single-entity": "Única entitat", + "filter-type-entity-group": "Entitats de grup", + "filter-type-entity-list": "Llista d'entitats", + "filter-type-entity-name": "Nom d'entitat", + "filter-type-entity-type": "Tipus d'entitat", + "filter-type-entity-group-list": "Llista d'entitats de grup", + "filter-type-entity-group-name": "Nom d'entitat de grup", + "filter-type-entities-by-group-name": "Entitats per nom de grup", + "filter-type-state-entity": "Entitat des de l'estat del panell", + "filter-type-state-entity-description": "Entitat presa dels paràmetres de panell", + "filter-type-state-entity-owner": "Propietari de l'entitat des de l'estat del tauler", + "filter-type-state-entity-owner-description": "Propietari de l'entitat presa dels paràmetres d'estat del tauler de control", + "filter-type-asset-type": "Tipus d'actiu", + "filter-type-asset-type-description": "Actius del tipus '{{assetType}}'", + "filter-type-asset-type-and-name-description": "Actius del tipus '{{assetType}}' i el nom del qual comenci per '{{prefix}}'", + "filter-type-device-type": "Tipus de dispositiu", + "filter-type-device-type-description": "Dispositius de tipus '{{deviceType}}'", + "filter-type-device-type-and-name-description": "Dispositius de tipus '{{deviceType}}' i el nom del qual comenci per '{{prefix}}'", + "filter-type-entity-view-type": "Tipus de vista d'entitat", + "filter-type-entity-view-type-description": "Vistes d'entitat del tipus '{{entityView}}'", + "filter-type-entity-view-type-and-name-description": "Vistes d'entitat del tipus '{{entityView}}' i el nom del qual comenci per '{{prefix}}'", + "filter-type-edge-type": "Tipus de vora", + "filter-type-edge-type-description": "Vores del tipus '{{edgeType}}'", + "filter-type-edge-type-and-name-description": "Vores del tipus '{{edgeType}}' i el nom del qual comenci per '{{prefix}}'", + "filter-type-relations-query": "Consulta de relacions", + "filter-type-relations-query-description": "{{entities}} que tenen {{relationType}} relació {{direction}} {{rootEntity}}", + "filter-type-edge-search-query": "Consultar cerca de vora", + "filter-type-edge-search-query-description": "Vores amb tipus {{edgeTypes}} que tenen {{relationType}} relació {{direction}} {{rootEntity}}", + "filter-type-asset-search-query": "Cerca d'actius", + "filter-type-asset-search-query-description": "Actius amb tipus {{assetTypes}} que tenen {{relationType}} relació {{direction}} {{rootEntity}}", + "filter-type-device-search-query": "Cerca de dispositius", + "filter-type-device-search-query-description": "Dispositius amb tipus {{deviceTypes}} que tenen {{relationType}} relació {{direction}} {{rootEntity}}", + "filter-type-entity-view-search-query": "Consulta de cerca de vista d'entitat", + "filter-type-entity-view-search-query-description": "Vistes d'entitat amb tipus {{entityViewTypes}} que tenen tipus de relació {{relationType}} amb direcció {{direction}} {{rootEntity}}", + "filter-type-apiUsageState": "Ús d'API", + "entity-filter": "Filtre per entitat", + "resolve-multiple": "Prendre com a múltiples entitats", + "filter-type": "Filtre per tipus", + "filter-type-required": "Es requereix filtre per tipus.", + "entity-filter-no-entity-matched": "No s'han trobat entitats amb el filtre especificat.", + "no-entity-filter-specified": "No hi ha filtre d'entitats especificat", + "root-state-entity": "Utilitzar estat de panell com a arrel", + "group-state-entity": "Utilitzar l'entitat del panell d'estats com a entitat de grup", + "group-state-entity-owner": "Usa l'entitat de l'estat del tauler com a propietari del grup d'entitats", + "last-level-relation": "Buscar només la relació d'últim nivell", + "root-entity": "Entitat arrel", + "state-entity-parameter-name": "Nom de paràmetre d'entitat d'estat", + "default-state-entity": "Entitat d'estat predeterminada", + "default-state-entity-group": "Grup d'entitats d'estat predeterminat", + "default-entity-parameter-name": "Per defecte", + "max-relation-level": "Nivell máx de relació", + "unlimited-level": "Nivell il·limitat", + "state-entity": "Entitat estat del panell", + "entities-of-group-state-entity": "Entitats del grup d'entitats del panell d'estat", + "all-entities": "Totes les entitats", + "any-relation": "qualsevol", + "type-assigned-to-edge": "Assignat a vora", + "type-unassigned-from-edge": "Sense assignar des de vores" + }, + "asset": { + "asset": "Actiu", + "assets": "Actius", + "management": "Gestió d'Actius", + "view-assets": "Veure actius", + "add": "Afegir Actiu", + "asset-type-max-length": "El tipus d'actiu ha de ser inferior a 256", + "assign-to-customer": "Assignar al client", + "assign-asset-to-customer": "Assignar Actiu(s) A Client", + "assign-asset-to-customer-text": "Selecciona els actius a assignar al client", + "no-assets-text": "No és van trobar actius", + "assign-to-customer-text": "Selecciona el client al qual assignar els actius", + "public": "Públic", + "assignedToCustomer": "Assignat al client", + "make-public": "Fer actiu públic", + "make-private": "Fer actiu privat", + "unassign-from-customer": "Cancel·lar l'assignació d'actiu del client", + "delete": "Esborrar actiu", + "asset-public": "L'actiu és públic", + "asset-type": "Tipus d'actiu", + "asset-type-required": "Es requereix tipus d'actiu.", + "select-asset-type": "Selecciona tipus d'actiu", + "enter-asset-type": "Entrar tipus d'actiu", + "any-asset": "Qualsevol actiu", + "no-asset-types-matching": "No s'han trobat actius coincidint amb '{{entitySubtype}}' .", + "asset-type-list-empty": "No hi ha cap mena d'actiu seleccionat.", + "asset-types": "Tipus d'actiu", + "name": "Nom", + "name-required": "Cal nom.", + "name-max-length": "El nom ha de ser inferior a 256", + "label-max-length": "L'etiqueta ha de ser inferior a 256", + "description": "Descripció", + "type": "Tipus", + "type-required": "Cal tipus.", + "details": "Detalls", + "events": "Esdeveniments", + "add-asset-text": "Afegir nou actiu", + "asset-details": "Detalls d'actiu", + "assign-assets": "Assignar activus", + "assign-assets-text": "Assignar { count, plural, 1 {1 activo} other {# activos} } a client", + "delete-assets": "Esborrar actius", + "unassign-assets": "Cancel·lar assignació d'actiu", + "unassign-assets-action-title": "Cancel·lar assignació de { count, plural, 1 {1 activo} other {# activos} } del client", + "assign-new-asset": "Assignar nou actiu", + "delete-asset-title": "Eliminar l'actiu '{{assetName}}'?", + "delete-asset-text": "Atenció, després de la confirmació l'actiu i les seves dades seran esborrades i irrecuperables.", + "delete-assets-title": "Eliminar els actius { count, plural, 1 {1 activo} other {# activos} }?", + "delete-assets-action-title": "Esborrar { count, plural, 1 {1 activo} other {# activos} }", + "delete-assets-text": "Atenció, després de la confirmació tots els actius seleccionats i les seves dades seran esborrades i irrecuperables.", + "make-public-asset-title": "Fer l'actiu '{{assetName}}' públic?", + "make-public-asset-text": "Després de la confirmació, l'actiu i les seves dades és faran públics i accessibles per uns altres.", + "make-private-asset-title": "Fer l'actiu '{{assetName}}' privat?", + "make-private-asset-text": "Després de la confirmació, l'actiu i les seves dades és faran privats i accessibles per uns altres.", + "unassign-asset-title": "Cancel·lar l'assignació de l'actiu '{{assetName}}'?", + "unassign-asset-text": "Després de la confirmació, l'actiu serà desassignat i no serà accessible pel client.", + "unassign-asset": "Cancel·lar assignació d'actiu", + "unassign-assets-title": "Cancel·lar les assignacions { count, plural, 1 {1 activo} other {# activos} }?", + "unassign-assets-text": "Després de la confirmació tots els actius seleccionats seran desassignats i no seran accessibles pel client.", + "copyId": "Copiar ID d'actiu", + "idCopiedMessage": "L'ID ha estat copiat al porta-retalls", + "select-asset": "Seleccionar actiu", + "no-assets-matching": "No s'han trobat actius que coincideixin amb '{{entity}}' .", + "asset-required": "Cal nom d'actiu", + "name-starts-with": "El nom de l'actiu que comenci amb", + "help-text": "Utilitzeu «%» segons la necessitat: «%asset.name.contains%», «%asset.name.ends», «asset.starts.with».", + "selected-assets": "{ count, plural, 1 {1 activo} other {# activos} } seleccionats", + "search": "Buscar actius", + "select-group-to-add": "Seleccionar grup objectiu per a afegir actius seleccionats", + "select-group-to-move": "Seleccionar grup objectiu per a moure actius seleccionats", + "remove-assets-from-group": "Està segur que desitja eliminar { count, plural, 1 {1 asset} other {# assets} } del grup '{{entityGroup}}'?", + "group": "Grup d'actius", + "list-of-groups": "{ count, plural, 1 {One asset group} other {List of # asset groups} }", + "group-name-starts-with": "Grups d'actius els noms dels quals comencen amb '{{prefix}}'", + "import": "Importar actius", + "asset-file": "Arxiu de l'actiu", + "label": "Etiqueta", + "assign-asset-to-edge": "Assignar actiu(s) a la vora", + "unassign-asset-from-edge": "Anul·lar actiu de vora", + "unassign-asset-from-edge-title": "Està segur que desitja desassignar l'actiu '{{assetName}}'?", + "unassign-asset-from-edge-text": "Després de la confirmació, l'actiu no serà assignat i la vora no podrà accedir a ell", + "unassign-assets-from-edge-title": "Està segur que desitja desassignar {count, plural, 1 {1 activo} other {# activos} }?", + "unassign-assets-from-edge-text": "Després de la confirmació, tots els actius seleccionats quedaran sense assignar i la vora no podrà accedir a ells.", + "assign-asset-to-edge-text": "Si us plau, seleccioni els actius per a assignar a la vora", + "unassign-assets-from-edge-action-title": "Anul·lar assignació {count, plural, 1 {1 activo} other {# activos} } des de la vora" + }, + "attribute": { + "attributes": "Atributs", + "latest-telemetry": "Última telemetria", + "attributes-scope": "Abast dels atributs del dispositiu", + "scope-latest-telemetry": "Última telemetria", + "scope-client": "Atributs del Client", + "scope-server": "Atributs del Servidor", + "scope-shared": "Atributs Compartits", + "add": "Afegir atribut", + "add-attribute-prompt": "Si us plau afegir atribut", + "key": "Clau", + "key-max-length": "La clau ha de ser inferior a 256", + "last-update-time": "Hora d'última actualització", + "key-required": "Clau de l'atribut requerida.", + "value": "Valor", + "value-required": "Cal valor de l'atribut.", + "delete-attributes-title": "Eliminar { count, plural, 1 {1 atributo} other {# atributos} }?", + "delete-attributes-text": "Atenció, després de la confirmació l'atribut serà eliminat, i la informació relacionada serà irrecuperable.", + "delete-attributes": "Esborrar atribut", + "enter-attribute-value": "Ingressar valor de l'atribut", + "show-on-widget": "Mostrar en Widget", + "widget-mode": "Widget", + "next-widget": "Widget següent", + "prev-widget": "Widget anterior", + "add-to-dashboard": "Afegir al Panell", + "add-widget-to-dashboard": "Afegir widget al Panell", + "selected-attributes": "{ count, plural, 1 {1 atributo} other {# atributos} } seleccionats", + "selected-telemetry": "{ count, plural, 1 {1 telemetría} other {# telemetrías} } seleccionades", + "no-attributes-text": "No és va trobar cap atribut", + "no-telemetry-text": "No és va trobar cap telemetria" + }, + "api-usage": { + "api-usage": "Ús de API", + "alarm": "Alarma", + "alarms-created": "Alarmes creades", + "alarms-created-daily-activity": "Alarmes creades activitat diària", + "alarms-created-hourly-activity": "Alarmes creades per hora", + "alarms-created-monthly-activity": "Alarmes creades activitat mensual", + "data-points": "Punts de dades", + "data-points-storage-days": "Dies d'enregistrament de punts de dades", + "email": "Correu electrònic", + "email-messages": "Missatges de correu electrònic", + "email-messages-daily-activity": "Activitat diaria de correus", + "email-messages-monthly-activity": "Activitat mensual de correus", + "exceptions": "Excepcions", + "executions": "Execucions", + "javascript": "JavaScript", + "javascript-executions": "Execucions JavaScript", + "javascript-functions": "Funcions JavaScript", + "javascript-functions-daily-activity": "Activitat diària de funcions JavaScript", + "javascript-functions-hourly-activity": "Activitat horària de funcions JavaScript", + "javascript-functions-monthly-activity": "Activitat mensual de funcions JavaScript", + "latest-error": "Últim error", + "messages": "Missatges", + "notifications": "Notificacions", + "notifications-email-sms": "Notificacions (Email/SMS)", + "notifications-hourly-activity": "Notificacions per hora", + "permanent-failures": "${entityName} Fallades permanents", + "permanent-timeouts": "${entityName} Temps d'espera permanentes", + "processing-failures": "${entityName} Fallades de processament", + "processing-failures-and-timeouts": "Fallades de processament i temps d'espera", + "processing-timeouts": "${entityName} Temps d'espera de processament", + "queue-stats": "Estadístiques de cues", + "rule-chain": "Cadena de regles", + "rule-engine": "Motor de regles", + "rule-engine-daily-activity": "Activitat diària de motor de regles", + "rule-engine-executions": "Execucions de motor de regles", + "rule-engine-hourly-activity": "Activitat horària de motor de regles", + "rule-engine-monthly-activity": "Activitat mensual de motor de regles", + "rule-engine-statistics": "Estadístiques del motor de regles", + "rule-node": "Node de regles", + "sms": "SMS", + "sms-messages": "Missatges SMS", + "sms-messages-daily-activity": "Activitat diària de missatges SMS", + "sms-messages-monthly-activity": "Activitat mensual de missatges SMS", + "successful": "${entityName} Exitós", + "telemetry": "Telemetria", + "telemetry-persistence": "Persistència de telemetria", + "telemetry-persistence-daily-activity": "Activitat diaria de persistencia de telemetria", + "telemetry-persistence-hourly-activity": "Activitat horària de persistencia de telemetria", + "telemetry-persistence-monthly-activity": "Activitat mensual de persistencia de telemetria", + "transport": "Transport", + "transport-daily-activity": "Activitat diaria de transport", + "transport-data-points": "Punts de dades de transport", + "transport-hourly-activity": "Activitat horària de transport", + "transport-messages": "Missatges de transport", + "transport-monthly-activity": "Activitat mensual de transport", + "view-details": "Veure detalls", + "view-statistics": "Veure estadístiques", + "email-messages-hourly-activity": "Activitat horària de Correus electrònics", + "sms-messages-hourly-activity": "Activitat horària de missatges SMS" + }, + "audit-log": { + "audit": "Auditoria", + "audit-logs": "Registre d'Auditoria", + "timestamp": "Marca horària", + "entity-type": "Tipus Entitat", + "entity-name": "Nom Entitat", + "user": "Usuari", + "type": "Tipus", + "status": "Estat", + "details": "Detalls", + "type-added": "Afegir", + "type-deleted": "Esborrat", + "type-updated": "Actualitzat", + "type-attributes-updated": "Atributs actualizats", + "type-attributes-deleted": "Atributs esborrats", + "type-rpc-call": "Trucada RPC", + "type-credentials-updated": "Credencials actualizades", + "type-assigned-to-customer": "Assignat al Client", + "type-unassigned-from-customer": "Designat al Client", + "type-assigned-to-edge": "Assignat a la vora", + "type-unassigned-from-edge": "Sense assignar de la vora", + "type-activated": "Activat", + "type-suspended": "Suspès", + "type-credentials-read": "Lectura de credencials", + "type-attributes-read": "Lectura d'atributs", + "type-added-to-entity-group": "Agregat al grup", + "type-removed-from-entity-group": "Eliminat del grup", + "type-relation-add-or-update": "Relació actualitzada", + "type-relation-delete": "Relació esborrada", + "type-relations-delete": "Esborrades totes les relacions", + "type-alarm-ack": "Alarma Acusada", + "type-alarm-clear": "Alarma Netejada", + "type-rest-api-rule-engine-call": "Trucada a l'API REST del motor de regles", + "type-made-public": "Fet públic", + "type-made-private": "Fet privat", + "type-login": "Iniciar sessió", + "type-logout": "Tancar sessió", + "type-lockout": "Tancament per bloqueig", + "status-success": "Èxit", + "status-failure": "Fallada", + "audit-log-details": "Detall del registre d'auditoria", + "no-audit-logs-prompt": "No és van trobar registres", + "action-data": "Dades d'acció", + "failure-details": "Detalls del error", + "search": "Buscar registres d'auditoria", + "clear-search": "Esborrar cerca", + "type-assigned-from-tenant": "Assignat des de l'administrador", + "type-assigned-to-tenant": "Assignat a l'administrador", + "type-provision-success": "Dispositiu aprovisionat", + "type-provision-failure": "Aprovisionament fallit", + "type-timeseries-updated": "Telemetria actualitzada", + "type-timeseries-deleted": "Telemetria esborrada", + "type-owner-changed": "S'ha canviat el propietari" + }, + "confirm-on-exit": { + "message": "Tens canvis sense guardar. Abandonar la pàgina?", + "html-message": "Tens canvis sense guardar.
                Abandonar la pàgina?", + "title": "Canvis sense guardar" + }, + "contact": { + "country": "País", + "city": "Ciutat", + "state": "Estat / Província", + "postal-code": "Códig Postal", + "postal-code-invalid": "Format de codi postal invàlid.", + "address": "Direcció", + "address2": "Direcció 2", + "phone": "Telèfon", + "email": "Correu electrònic", + "no-address": "Sense Direcció", + "state-max-length": "La longitud de l'estat ha de ser inferior a 256", + "phone-max-length": "El número de telèfon hauria de ser inferior a 256", + "city-max-length": "La ciutat especificada ha de ser inferior a 256" + }, + "common": { + "username": "Usuari", + "password": "Contrasenya", + "enter-username": "Introdueix el nom d'usuari.", + "enter-password": "Introdueix la contrasenya", + "enter-search": "Introdueix cerca", + "created-time": "Data de creació", + "loading": "Carregant...", + "proceed": "Continua", + "open-details-page": "Obre la pàgina de detalls" + }, + "converter": { + "converter": "Convertidor de dades", + "converters": "Convertidors de dades", + "select-converter": "Seleccionar convertidor de dades", + "no-converters-matching": "Convertidors de dades que coincideixin amb '{{entity}}' no van ser trobats.", + "converter-required": "Cal convertidor de dades", + "delete": "Eliminar convertidor", + "management": "Gestió de convertidors de dades", + "add-converter-text": "Afegir nou convertidor de dades", + "no-converters-text": "Convertidors de dades no trobats", + "selected-converters": "{ count, plural, 1 {1 data converter} other {# data converters} } seleccionats", + "delete-converter-title": "Està segur que desitja eliminar el convertidor de dades '{{converterName}}'?", + "delete-converter-text": "Anar amb compte, després de la confirmació, el convertidor de dades i totes les dades relacionades és tornaran irrecuperables.", + "delete-converters-title": "Està segur que desitja eliminar { count, plural, 1 {1 data converter} other {# data converters} }?", + "delete-converters-action-title": "Eliminar { count, plural, 1 {1 data converter} other {# data converters} }", + "delete-converters-text": "Anar amb compte, després de la confirmació s'eliminaran tots els convertidors de dades seleccionades i totes les dades relacionades és tornaran irrecuperables.", + "events": "Esdeveniments", + "add": "Afegeix convertidor de dades", + "search": "Cerca convertidors de dades", + "converter-details": "Detalls del convertidor de dades", + "details": "Detalls", + "copyId": "Copiar ID del convertidor", + "idCopiedMessage": "ID del convertidor ha estat copiada al porta-retalls", + "debug-mode": "Mode de depuració", + "created-time": "Hora de creació", + "name": "Nom", + "name-required": "Cal el nom.", + "name-max-length": "El nom ha de ser inferior a 256", + "description": "Descripció", + "decoder": "Descodificador", + "encoder": "Codificador", + "test-decoder-fuction": "Prova funció del descodificador", + "test-encoder-fuction": "Prova funció del codificador", + "decoder-input-params": "Paràmetres d'entrada del descodificador", + "encoder-input-params": "Paràmetres d'entrada del codificador", + "payload": "Càrrega útil", + "payload-content-type": "Tipus de contingut de la càrrega útil", + "payload-content": "Contingut de la càrrega útil", + "message": "Missatge", + "message-type": "Tipus de missatge", + "message-type-required": "Cal el tipus de missatge", + "test": "Prova", + "metadata": "Metadades", + "metadata-required": "Les entrades de metadades no poden estar buides.", + "integration-metadata": "Metadades d'integració", + "integration-metadata-required": "Les entrades de metadades d'integració no poden estar buides.", + "output": "Sortida", + "import": "Importar convertidor", + "export": "Exportar convertidor", + "export-failed-error": "No és pot exportar convertidor: {{error}}", + "create-new-converter": "Crear nou convertidor", + "converter-file": "Arxiu del convertidor", + "invalid-converter-file-error": "No és pot importar el convertidor: Estructura del convertidor de dades invàlida.", + "type": "Tipus", + "type-required": "Cal el tipus", + "type-uplink": "Enllaç ascendent", + "type-downlink": "Enllaç descendent" + }, + "content-type": { + "json": "Json", + "text": "Text", + "binary": "Binari (Base64)" + }, + "customer": { + "customer": "Client", + "customers": "Clients", + "management": "Gestió de Clients", + "dashboard": "Panell del Client", + "dashboards": "Panells del Client", + "devices": "Dispositius del client", + "entity-views": "Vistes d'Entitat del client", + "assets": "Actius del Client", + "public-dashboards": "Panells Públics", + "public-devices": "Dispositius Públics", + "public-assets": "Actius Públics", + "public-entity-views": "Vistes d'Entitat Públiques", + "add": "Afegir client", + "delete": "Eliminar client", + "manage-customer-user-groups": "Gestionar grups d'usuaris de clients", + "manage-customer-groups": "Gestionar grups de clients", + "manage-customer-device-groups": "Gestioneu grups de dispositius de clients", + "manage-customer-asset-groups": "Gestionar grups d'actius de clients", + "manage-customer-entity-view-groups": "Gestioneu els grups de visualització d'entitats de clients", + "manage-customer-edge-groups": "Gestionar grups de clients perifèrics", + "manage-customer-dashboard-groups": "Gestioneu grups de panells de clients", + "manage-customer-users": "Gestionar d'usuaris del client", + "manage-customers": "Gestionar clients", + "manage-customer-devices": "Gestionar dels dispositius del client", + "manage-customer-entity-views": "Gestioneu les visualitzacions de l'entitat del client", + "manage-customer-dashboards": "Gestionar els panells del client", + "manage-public-devices": "Gestionar dispositius públics", + "manage-public-dashboards": "Gestionar panells públics", + "manage-customer-assets": "Gestionar actius del client", + "manage-public-assets": "Gestionar actius públics", + "add-customer-text": "Afegir nou client", + "no-customers-text": "No s'han trobat clients", + "customer-details": "Detalls del client", + "delete-customer-title": "Eliminar el client '{{customerTitle}}'?", + "delete-customer-text": "Atenció, després de la confirmació el client serà eliminat i tota la informació relacionada serà irrecuperable.", + "delete-customers-title": "Eliminar { count, plural, 1 {1 cliente} other {# clientes} }?", + "delete-customers-action-title": "Esborrar { count, plural, 1 {1 cliente} other {# clientes} }", + "delete-customers-text": "Atenció, després de la confirmació tots els clients seleccionats seran eliminats i la informació relacionada serà irrecuperable.", + "manage-user-groups": "Gestionar grups d'usuaris", + "manage-asset-groups": "Gestionar grups d'actius", + "manage-device-groups": "Gestionar grups de dispositius", + "manage-dashboard-groups": "Gestionar grups de panells", + "manage-entity-view-groups": "Gestioneu grups de visualitzacions d'entitats", + "manage-edge-groups": "Gestionar grups edge", + "manage-users": "Gestionar usuaris", + "manage-assets": "Gestionar actius", + "manage-devices": "Gestionar dispositius", + "manage-dashboards": "Gestionar panells", + "title": "Títol client", + "title-required": "Cal el títol del client.", + "title-max-length": "El títol ha de ser inferior a 256", + "description": "Descripció", + "details": "Detalls", + "events": "Esdeveniments", + "copyId": "Copiar ID del client", + "idCopiedMessage": "El ID del client s'ha copiat al porta-retalls", + "select-customer": "Seleccionar Client", + "no-customers-matching": "No s'han trobat clients que coincideixin amb '{{entity}}' .", + "customer-required": "Cal client", + "selected-customers": "{ count, plural, 1 {1 cliente} other {# clientes} } seleccionats", + "search": "Buscar clients", + "select-group-to-add": "Seleccionar el grup objectiu per afegir clients seleccionats", + "select-group-to-move": "Seleccionar el grup abkectiu per moure clients seleccionats", + "remove-customers-from-group": "Esteu segur que voleu eliminar { count, plural, 1 {1 customer} other {# customers} } del grup '{{entityGroup}}'?", + "group": "Grup clients", + "list-of-groups": "{ count, plural, 1 {One customer group} other {List of # customer groups} }", + "group-name-starts-with": "Grups de clients els quals començen amb '{{prefix}}'", + "select-default-customer": "Seleccionar un client per defecte", + "default-customer": "Client per defecte", + "default-customer-required": "El client per defecte és necessari per depurar el tauler de control al nivell de l'arrendatari", + "allow-white-labeling": "Permet l'etiqueta blanca", + "edges": "Vores del client", + "manage-edges": "Gestionar les vores", + "public-edges": "Vores públiques", + "manage-customer-edges": "Administrar vores dels clients", + "manage-public-edges": "Administrar vores públiques" + }, + "customers-hierarchy": { + "customers-hierarchy": "Jerarquia dels clients", + "open-nav-tree": "Obre l'arbre de navegació", + "return-to-top-level": "Torna al nivell superior" + }, + "custom-menu": { + "custom-menu": "Menú personalitzat", + "custom-menu-hint": "Definiu el menú personalitzat JSON a continuació. Aquest JSON conté una llista d'elements de menú personalitzats." + }, + "custom-translation": { + "custom-translation": "Traducció personalitzada", + "translation-map": "Mapa de traducció", + "key": "Clau de traducció", + "import": "Importa la traducció", + "export": "Exporta la traducció", + "export-data": "Exporta les dades de traducció", + "import-data": "Importa les dades de traducció", + "translation-file": "Fitxer de traducció", + "invalid-translation-file-error": "No s'ha pogut importar el fitxer de traducció: l'estructura de dades de traducció no és vàlida.", + "custom-translation-hint": "Definiu el JSON de traducció personalitzat a continuació. Aquest JSON sobreescriurà la traducció predeterminada. Feu clic a «Baixa el fitxer locale» per a obtenir la traducció existent. També podeu utilitzar el fitxer baixat com a referència a les parelles disponibles de traducció clau-valor.", + "download-locale-file": "Baixa el fitxer locale" + }, + "datetime": { + "date-from": "Data des de", + "time-from": "Hora des de", + "date-to": "Data fins a", + "time-to": "Hora fins a" + }, + "dashboard": { + "dashboard": "Panell", + "dashboards": "Panells", + "management": "Gestió de Panells", + "view-dashboards": "Veure Panells", + "add": "Afegir Panell", + "assign-dashboard-to-customer": "Assignar panell(s) al client", + "assign-dashboard-to-customer-text": "Per favor, selecciona algún panell per assignar al Client.", + "assign-dashboard-to-edge-title": "Assignar panell(s) a Vora", + "assign-to-customer-text": "Si us plau, seleccioni algun client per a assignar el(els) panell(s).", + "assign-to-customer": "Assignar al client", + "unassign-from-customer": "Desassignar del client", + "make-public": "Fer panell públic", + "make-private": "Fer panell privat", + "manage-assigned-customers": "Administrar clients assignats", + "assigned-customers": "Clients assignats", + "assign-to-customers": "Assignar Panell / Panells a Clients", + "assign-to-customers-text": "Selecciona els clients per assignar els panells", + "unassign-from-customers": "Desassignar Panell / Panells de clients", + "unassign-from-customers-text": "Selecciona els clients per desassignar els panells", + "no-dashboards-text": "Cap panell trobat", + "no-widgets": "Cap widget configurat", + "add-widget": "Afegir nou widget", + "title": "Títol", + "image": "Imatge del tauler", + "mobile-app-settings": "Paràmetres de l'aplicació mòbil", + "mobile-order": "Ordre del tauler en una aplicació mòbil", + "mobile-hide": "Amaga el tauler a l'aplicació mòbil", + "update-image": "Actualitza la imatge del tauler", + "take-screenshot": "Pren una captura de pantalla", + "select-widget-title": "Seleccionar widget", + "select-widget-value": "{{title}}: seleccionar widget", + "select-widget-subtitle": "Llista de tipus de widgets disponibles", + "delete": "Eliminar panell", + "title-required": "Cal títol.", + "title-max-length": "El títol ha de ser inferior a 256", + "description": "Descripció", + "details": "Detalls", + "dashboard-details": "Detalls del panell", + "add-dashboard-text": "Afegir nou panell", + "assign-dashboards": "Assignar panells", + "assign-new-dashboard": "Assignar nou panell", + "assign-dashboards-text": "Assignar { count, plural, 1 {1 panel} other {# paneles} } al client", + "unassign-dashboards-action-text": "Desassignar { count, plural, 1 {1 panel} other {# paneles} } als clients", + "delete-dashboards": "Eliminar panells", + "unassign-dashboards": "Desassignar panells", + "unassign-dashboards-action-title": "Desassignar { count, plural, 1 {1 paneles} other {# paneles} } del client", + "delete-dashboard-title": "Eliminar el panell '{{dashboardTitle}}'?", + "delete-dashboard-text": "Atenció, el panell seleccionat serà eliminat i la informació relacionada serà irrecuperable.", + "delete-dashboards-title": "Eliminar { count, plural, 1 {1 panel} other {# paneles} }?", + "delete-dashboards-action-title": "Eliminar { count, plural, 1 {1 panel} other {# paneles} }", + "delete-dashboards-text": "Atenció, els panells seleccionats seran eliminats i la informació relacionada serà irrecuperable.", + "unassign-dashboard-title": "Desassignar el panell '{{dashboardTitle}}'?", + "unassign-dashboard-text": "Després de la confirmació, el panell serà desassignat i no podrà ser accessible pel client.", + "unassign-dashboard": "Desassignar panell", + "unassign-dashboards-title": "Desassignar { count, plural, 1 {1 panel} other {# paneles} }?", + "unassign-dashboards-text": "Atenció, després de la confirmació els panells seleccionats seran desassignats i no podran ser accessibles pel client.", + "public-dashboard-title": "El panell ara és públic", + "public-dashboard-text": "El teu panell {{dashboardTitle}} ara és públic i podrà ser accedit des de: aquí:", + "public-dashboard-notice": "Nota: No oblidis fer públics els dispositius relacionats per accedir a les seves dades.", + "public-dashboard-link": "Enllaç públic del tauler", + "public-dashboard-link-text": "El vostre tauler públic {{dashboardTitle}} és accessible a través del proper públic link:", + "public-dashboard-link-notice": "Note: No oblideu fer públics els dispositius, actius i entitats relacionats per a accedir a les seves dades.", + "make-private-dashboard-title": "Fer el panell '{{dashboardTitle}}' privat?", + "make-private-dashboard-text": "Després de la confirmació, el panell serà privat i no podrà ser accessible per uns altres.", + "make-private-dashboard": "Fer panell privat", + "socialshare-text": "'{{dashboardTitle}}' impulsat per ThingsBoard", + "socialshare-title": "'{{dashboardTitle}}' impulsat per ThingsBoard", + "select-dashboard": "Seleccionar panell", + "no-dashboards-matching": "Panell '{{entity}}' no trobat.", + "dashboard-required": "Cal panell.", + "select-existing": "Seleccionar panells existentes", + "create-new": "Crear nou panell", + "new-dashboard-title": "Nou títol", + "open-dashboard": "Obrir panell", + "set-background": "Definir fons", + "background-color": "Color de fons", + "background-image": "Imatge de fons", + "background-size-mode": "Manera grandària de fons", + "no-image": "No s'ha seleccionat cap imatge", + "empty-image": "Sense imatge", + "drop-image": "Deixi anar una imatge o faci clic per seleccionar un arxiu per carregar.", + "maximum-upload-file-size": "Mida màxima del fitxer de pujada: {{ size }}", + "cannot-upload-file": "No s'ha pogut pujar el fitxer", + "settings": "Configuració", + "layout-settings": "Paràmetres de la disposició", + "columns-count": "Número de columnes", + "columns-count-required": "Cal número de columnes.", + "min-columns-count-message": "Sol es permet un número mínim de 10 columnes.", + "max-columns-count-message": "Sol es permet un número màxim de 1000 columnes.", + "widgets-margins": "Marge entre widgets", + "margin-required": "Cal valor de marge.", + "min-margin-message": "0 és el valor de marge mínim permès.", + "max-margin-message": "50 és el valor de marge màxim permès.", + "horizontal-margin": "Marge horitzontal", + "horizontal-margin-required": "Cal marge horitzontal.", + "min-horizontal-margin-message": "Sol es permet marge horitzontal mínim de 0.", + "max-horizontal-margin-message": "Sol es permet marge horitzontal màxim de 50.", + "vertical-margin": "Marge vertical", + "vertical-margin-required": "Cal marge vertical.", + "min-vertical-margin-message": "Sol es permet marge vertical mínim de 0.", + "max-vertical-margin-message": "Sol es permet marge vertical màxim de 50.", + "autofill-height": "Alçada disseny autoomplert", + "mobile-layout": "Configuració de disseny mòbil", + "mobile-row-height": "Alçada de la fila per a mòbils, px", + "mobile-row-height-required": "Cal alçada de fila.", + "min-mobile-row-height-message": "Només es permeten 5 píxels com alçada mínima de fila (móvil).", + "max-mobile-row-height-message": "Només es permeten 200 píxels com alçada màxima de fila (móvil).", + "title-settings": "Paràmetres del títol", + "display-title": "Mostrar títol del panell", + "title-color": "Color del títol", + "toolbar-settings": "Paràmetres de la barra d'eines", + "hide-toolbar": "Amaga la barra d'eines", + "toolbar-always-open": "Mantenir la barra d'eines oberta", + "display-dashboards-selection": "Mostrar selecció de panells", + "display-entities-selection": "Mostrar selecció d'entitats", + "display-filters": "Mostrar filtres", + "display-dashboard-timewindow": "Mostrar finestra del temps", + "display-dashboard-export": "Mostrar exportar", + "display-update-dashboard-image": "Mostra la imatge del tauler d'actualització", + "dashboard-logo-settings": "Configuració del logotip del tauler de control", + "display-dashboard-logo": "Mostra el logotip en mode de pantalla completa del tauler", + "dashboard-logo-image": "Imatge del logotip del tauler de control", + "advanced-settings": "Configuració avançada", + "dashboard-css": "Tauler CSS", + "import": "Importar panell", + "export": "Exportar panell", + "export-failed-error": "Impossible exportar panell: {{error}}", + "export-pdf": "Exportar com PDF", + "export-png": "Exportar com PNG", + "export-jpg": "Exportar com JPEG", + "export-json-config": "Exportar configuració JSON", + "download-dashboard-progress": "Generant panell {{reportType}} ...", + "create-new-dashboard": "Crear nou panell", + "dashboard-file": "Arxivar panell", + "invalid-dashboard-file-error": "Impossible importar panell: Estructura de dades invàlides.", + "dashboard-import-missing-aliases-title": "Configurar àlies utilitzats pel panell importat", + "create-new-widget": "Crear nou widget", + "import-widget": "Importar widget", + "widget-file": "Arxiu de widget", + "invalid-widget-file-error": "Impossible importar widget: Estructura de dades invàlida.", + "widget-import-missing-aliases-title": "Configurar àlies utilitzats pel widget", + "open-toolbar": "Obrir toolbar del panell", + "close-toolbar": "Tancar toolbar", + "configuration-error": "Error de configuració", + "alias-resolution-error-title": "Error de configuració d'àlies del panell", + "invalid-aliases-config": "No es pot trobar cap dispositiu que coincideixi amb alguns dels àlies de filtre.
                Posi's en contacte amb el seu administrador per resoldre aquest problema.", + "select-devices": "Seleccionar dispositius", + "assignedToCustomer": "Assignat al client", + "assignedToCustomers": "Assignat als clients", + "public": "Públic", + "public-link": "Enllaç públic", + "copy-public-link": "Copiar enllaç públic", + "public-link-copied-message": "El link públic del panell s'ha copiat al porta-retalls", + "manage-states": "Administrar estats de panells", + "states": "Estats de panells", + "search-states": "Buscar estats de panells", + "selected-states": "{ count, plural, 1 {1 estado panel} other {# estat paneles} } seleccionats", + "edit-state": "Editar estat panell", + "delete-state": "Esborrar estat panell", + "add-state": "Afegir estat panell", + "no-states-text": "No s'han trobat estats", + "state": "Estat de panell", + "state-name": "Nom", + "state-name-required": "Cal nom de l'estat.", + "state-id": "ID Estat", + "state-id-required": "Cal el ID de l'estat.", + "state-id-exists": "Ja existeix un ID de l'estat.", + "is-root-state": "Estat arrel(Root)", + "delete-state-title": "Esborrar estat del panell", + "delete-state-text": "Eliminar l'estat del panell amb nom: '{{stateName}}'?", + "show-details": "Mostrar detalls", + "hide-details": "Ocultar detalls", + "select-state": "Seleccionar estat destí (target state)", + "state-controller": "Controlador d'estats", + "selected-dashboards": "{ count, plural, 1 {1 panel} other {# paneles} } seleccionats", + "search": "Buscar panells", + "home-dashboard": "Tauler d'inici", + "home-dashboard-hide-toolbar": "Amaga la barra d'eines del tauler d'inici", + "select-group-to-add": "Seleccioneu el grup de destinació per a afegir els panells seleccionats", + "select-group-to-move": "Seleccioneu el grup de destinació per a moure els panells seleccionats", + "remove-dashboards-from-group": "Esteu segur que voleu eliminar { count, plural, 1 {1 dashboard} other {# dashboards} } del grup '{{entityGroup}}'?", + "group": "Grup de penells", + "list-of-groups": "{ count, plural, 1 {One dashboard group} other {List of # dashboard groups} }", + "group-name-starts-with": "Grups de tauler amb noms que comencen per '{{prefix}}'", + "unassign-dashboard-from-edge-text": "Després de la confirmació, el tauler no serà assignat i la vora no podrà accedir a ell", + "unassign-dashboards-from-edge-title": "Esteu segur que voleu desassignar { count, plural, 1 {1 dashboard} other {# dashboards} }?", + "unassign-dashboards-from-edge-text": "Després de la confirmació, és anul·larà l'assignació de tots els panells seleccionats i no seran accessibles per de vora", + "assign-dashboard-to-edge": "Assignar panell(és) a la vora", + "assign-dashboard-to-edge-text": "Si us plau selecciona els panells per assignar a la vora", + "non-existent-dashboard-state-error": "Estat del tauler amb ID \"{{ stateId }}\" no s'ha trobat" + }, + "datakey": { + "settings": "Configuració", + "advanced": "Avançat", + "label": "Etiqueta", + "color": "Color", + "units": "Símbol especial per mostrar juntament amb el valor", + "decimals": "Nombre de dígits després de la coma", + "data-generation-func": "Funció de generació de dades", + "use-data-post-processing-func": "Utilitzar funció de postprocessament de dades", + "configuration": "Configuració de clau de dades", + "timeseries": "Sèrie de temps", + "attributes": "Atributs", + "entity-field": "Camp d'entitat", + "alarm": "Camps d'alarma", + "timeseries-required": "Sèries del temps del dispositiu requerit.", + "timeseries-or-attributes-required": "Sèries del temps/*Atributs requerits.", + "alarm-fields-timeseries-or-attributes-required": "Es requereixen camps d'alarma o sèries del temps/atributs.", + "maximum-timeseries-or-attributes": "Màxim { count, plural, 1 {1 timeseries/atributo és permès.} other {# timeseries/atributos són permitidos} }", + "alarm-fields-required": "Cal camps d'alarma", + "function-types": "Tipus de funcions", + "function-types-required": "Cal tipus de funcions.", + "alarm-keys": "Claus d'alarmes", + "alarm-key": "Clau d'alarma", + "alarm-key-functions": "Funcions de les claus d'alarmes", + "alarm-key-function": "Funció de clau d'alarma", + "latest-keys": "Últimes claus", + "latest-key": "Última clau", + "latest-key-functions": "Funcions d'últimes claus", + "latest-key-function": "Funció d'última clau", + "timeseries-keys": "Claus de sèries de temps", + "timeseries-key": "Clau de sèries de temps", + "timeseries-key-functions": "Funcions de sèries de temps", + "timeseries-key-function": "Funció de sèrie de temps", + "maximum-function-types": "Màxim { count, plural, 1 {1 tipo de función está permitida.} other {# tipos de funciones están permitidos} }", + "time-description": "Hora del valor actual", + "value-description": "El valor actual", + "prev-value-description": "resultat de la crida anterior de la funció", + "time-prev-description": "hora del valor previ", + "prev-orig-value-description": "valor original previ" + }, + "datasource": { + "type": "Tipus de font de dades", + "name": "Nom", + "label": "Etiqueta", + "add-datasource-prompt": "Si us plau, agrega una font de dades" + }, + "details": { + "details": "Detalls", + "edit-mode": "Manera d'edició", + "edit-json": "Editar JSON", + "toggle-edit-mode": "Anar a Manera d'Edició" + }, + "device": { + "device": "Dispositiu", + "device-required": "Cal dispositiu.", + "devices": "Dispositius", + "management": "Gestió de Dispositius", + "view-devices": "Veure Dispositius", + "device-alias": "Àlies de dispositiu", + "device-type-max-length": "El tipus de dispositiu ha de ser inferior a 256", + "aliases": "Àlies de dispositius", + "no-alias-matching": "'{{alias}}' no trobat.", + "no-aliases-found": "Cap àlies trobat.", + "no-key-matching": "'{{key}}' no trobat.", + "no-keys-found": "Ninguna clau trobada.", + "create-new-alias": "Crear nou àlies!", + "create-new-key": "Crear nova clau!", + "duplicate-alias-error": "Àlies duplicat '{{alias}}'.
                L'àlies dels dispositius han de ser únics dins del panell.", + "configure-alias": "Configurar àlies '{{alias}}'", + "no-devices-matching": "No s'ha trobat dispositiu '{{entity}}'", + "alias": "Àlies", + "alias-required": "Cal àlies de dispositiu.", + "remove-alias": "Eliminar àlies", + "add-alias": "Afegir àlies", + "name-starts-with": "Nom comenci amb", + "help-text": "Useu «%» segons la necessitat: «%device.name.contains%», «%device.name.ends», «device.starts.with».", + "device-list": "Llista de dispositius", + "use-device-name-filter": "Utilitzar filtre", + "device-list-empty": "Cap dispositiu seleccionat.", + "device-name-filter-required": "Cal nom del filtre.", + "device-name-filter-no-device-matched": "Cap dispositiu trobat que comenci amb '{{device}}'.", + "add": "Afegir dispositiu", + "assign-to-customer": "Assignar a client", + "assign-device-to-customer": "Assignar dispositiu(s) a Client", + "assign-device-to-customer-text": "Si us plau, seleccioni els dispositius que seran assignats al client", + "assign-device-to-edge-title": "Assignar Dispositiu(s) a Vora", + "assign-device-to-edge-text": "Selecciona els dispositius a assignar a la Vora", + "make-public": "Fer dispositiu públic", + "make-private": "Fer dispositiu privat", + "no-devices-text": "Cap dispositiu trobat", + "assign-to-customer-text": "Si us plau, seleccioni el client per assignar el(els) dispositiu(s)", + "device-details": "Detalls del dispositiu", + "add-device-text": "Afegir nou dispositiu", + "credentials": "Credencials", + "manage-credentials": "Gestionar credencials", + "delete": "Eliminar dispositiu", + "assign-devices": "Assignar dispositiu", + "assign-devices-text": "Assignar { count, plural, 1 {1 dispositivo} other {# dispositivos} } al client", + "delete-devices": "Eliminar dispositiu", + "unassign-from-customer": "Desassignar el client", + "unassign-devices": "Desassignar dispositius", + "unassign-devices-action-title": "Desassignar { count, plural, 1 {1 dispositivo} other {# dispositivos} } del client", + "unassign-device-from-edge-title": "Està segur que vol desassignar el dispositiu '{{deviceName}}'?", + "unassign-device-from-edge-text": "Després de la confirmació, el dispositiu no serà assignat i la vora no podrà accedir a ell", + "unassign-devices-from-edge": "Desassigna els dispositius de la vora", + "assign-new-device": "Assignar nou dispositiu", + "make-public-device-title": "Fer el dispositiu '{{deviceName}}' públic?", + "make-public-device-text": "Després de la confirmació, el dispositiu i la informació relacionada seran públics i podrà ser accessible per uns altres.", + "make-private-device-title": "Fer el dispositiu '{{deviceName}}' privat?", + "make-private-device-text": "Després de la confirmació, el dispositiu i la informació relacionada seran privats i no podrà ser accessible per uns altres.", + "view-credentials": "Veure credencials", + "delete-device-title": "Eliminar el dispositiu '{{deviceName}}'?", + "delete-device-text": "Atenció, després de la confirmació els dispositius seran eliminats i la informació relacionada serà irrecuperable.", + "delete-devices-title": "Eliminar { count, plural, 1 {1 dispositivo} other {# dispositivos} }?", + "delete-devices-action-title": "Eliminar { count, plural, 1 {1 dispositivo} other {# dispositivos} }", + "delete-devices-text": "Atenció, després de la confirmació els dispositius seleccionats seran eliminats i la informació relacionada serà irrecuperable.", + "unassign-device-title": "Desassignar el dispositiu '{{deviceName}}'?", + "unassign-device-text": "Després de la confirmació, el dispositiu serà desasignado i no podrà ser accessible pel client.", + "unassign-device": "Desassignar dispositiu", + "unassign-devices-title": "Desassignar { count, plural, 1 {1 dispositivo} other {# dispositivos} }?", + "unassign-devices-text": "Després de la confirmació, els dispositius seleccionats seran desasignados i no podran ser accedits pel client.", + "device-credentials": "Credencials del dispositiu", + "loading-device-credentials": "S'estan carregant les credencials del dispositiu..", + "credentials-type": "Tipus de credencial", + "access-token": "Tóken d'accés", + "access-token-required": "Cal access token.", + "access-token-invalid": "Access token ha de tenir entre 1 a 32 caràcters.", + "certificate-pem-format": "Certificat en format PEM", + "certificate-pem-format-required": "Cal certificat.", + "lwm2m-security-config": { + "identity": "Identitat del client", + "identity-required": "Cal identitat del client.", + "identity-tooltip": "L'identificador PSK és un identificador PSK arbitrari de fins a 128 bytes, tal com es descriu a l'estàndard [RFC7925].\nL'identificador PSK S'HA DE convertir primer en una cadena de caràcters i després codificar-se en octets mitjançant UTF-8.", + "client-key": "Clau del client", + "client-key-required": "Cal clau del client.", + "client-key-tooltip-prk": "La clau pública o l'identificador RPK ha d'estar en l'estàndard [RFC7250] i codificat en format Base64!", + "client-key-tooltip-psk": "La clau PSK ha de tenir el format estàndard [RFC4279] i HexDec: 32, 64, 128 caràcters!", + "endpoint": "Nom del client del punt final", + "endpoint-required": "Cal nom del client del punt final.", + "client-public-key": "Clau pública del client", + "client-public-key-hint": "Si la clau pública del client està buida, s'utilitzarà el certificat de confiança", + "client-public-key-tooltip": "La clau pública X509 ha d'estar en format X509v3 codificat amb DER i suportar exclusivament l'algoritme EC i després codificada en format Base64!", + "mode": "Mode de configuració de seguretat", + "client-tab": "Configuració de seguretat del client", + "client-certificate": "Certificat de client", + "bootstrap-tab": "Client Bootstrap", + "bootstrap-server": "Servidor Bootstrap", + "lwm2m-server": "Servidor LwM2M", + "client-publicKey-or-id": "Clau pública o identificació del client", + "client-publicKey-or-id-required": "Cal Clau pública o identificació del client.", + "client-publicKey-or-id-tooltip-psk": "L'identificador PSK és un identificador PSK arbitrari de fins a 128 bytes, tal com es descriu a l'estàndard [RFC7925].\nL'identificador PSK S'HA DE convertir primer en una cadena de caràcters i després codificar-se en octets mitjançant UTF-8.", + "client-publicKey-or-id-tooltip-rpk": "La clau pública o l'identificador RPK ha d'estar en l'estàndard [RFC7250] i codificat en format Base64!", + "client-publicKey-or-id-tooltip-x509": "La clau pública X509 ha d'estar en format X509v3 codificat amb DER i suportar exclusivament l'algorisme EC i després codificar-se en format Base64", + "client-secret-key": "Clau secreta del client", + "client-secret-key-required": "Cal clau secreta del client.", + "client-secret-key-tooltip-psk": "La clau PSK ha de tenir el format estàndard [RFC4279] i HexDec: 32, 64, 128 caràcters!", + "client-secret-key-tooltip-prk": "La clau secreta RPK ha d'estar en format PKCS_8 (codificació DER, estàndard [RFC5958]) i després codificada en format Base64!", + "client-secret-key-tooltip-x509": "La clau secreta X509 ha d'estar en format PKCS_8 (codificació DER, estàndard [RFC5958]) i després codificada en format Base64!" + }, + "client-id": "ID Client", + "client-id-pattern": "Conté caràcter invàlid.", + "user-name": "Nom Usuari", + "user-name-required": "Cal nom d'usuari.", + "client-id-or-user-name-necessary": "L'ID Client i/o el Nom d'usuari són necessaris", + "password": "Contrasenya", + "secret": "Secreta", + "secret-required": "Cal secreta.", + "device-type": "Tipus de dispositiu", + "device-type-required": "Cal tipus de dispositiu.", + "select-device-type": "Seleccionar tipus de dispositiu", + "enter-device-type": "Entrar tipus de dispositiu", + "any-device": "Cualquier dispositiu", + "no-device-types-matching": "No hi ha cap tipus de dispositiu que coincideixin amb '{{entitySubtype}}' .", + "device-type-list-empty": "No hi ha cap tipus de dispositiu seleccionats.", + "device-types": "Tipus de dispositiu", + "name": "Nom", + "name-required": "Cal el nom.", + "name-max-length": "El nom ha de ser inferior a 256", + "label-max-length": "L'etiqueta ha de ser inferior a 256", + "description": "Descripció", + "label": "Etiqueta", + "events": "Esdeveniments", + "details": "Detalls", + "copyId": "Copiar ID", + "copyAccessToken": "Copiar access token", + "copy-mqtt-authentication": "Copiar credencials MQTT", + "idCopiedMessage": "Id del dispositiu copiat al porta-retalls", + "accessTokenCopiedMessage": "Access token del dispositiu copiat al porta-retalls", + "mqtt-authentication-copied-message": "Les dades d'autenticació MQTT s'han copiat al porta-retalls", + "assignedToCustomer": "Assignat al client", + "unable-delete-device-alias-title": "Impossible eliminar àlies del dispositiu", + "unable-delete-device-alias-text": "Àlies '{{deviceAlias}}' no pot ser eliminat. Esta siendo usado per el(els) widget(s):
                {{widgetsList}}", + "is-gateway": "És gateway", + "overwrite-activity-time": "Sobreescriure hora d'activitat per al dispositiu connectat", + "public": "Públic", + "device-public": "El dispositiu és públic", + "select-device": "Seleccionar dispositiu", + "selected-devices": "{ count, plural, 1 {1 dispositivo} other {# dispositivos} } seleccionats", + "search": "Buscar dispositius", + "select-group-to-add": "Seleccionar grup objetiu per afegir dispositius seleccionats", + "select-group-to-move": "Seleccionar grup objetiu per moure dispositius seleccionats", + "remove-devices-from-group": "Està segur de que desitja eliminar { count, plural, 1 {1 device} other {# devices} } del grup '{{entityGroup}}'?", + "group": "Grup de dispositius", + "list-of-groups": "{ count, plural, 1 {One device group} other {List of # device groups} }", + "group-name-starts-with": "Grup de dispositius els quals comencen amb '{{prefix}}'", + "import": "Importar dispositiu", + "device-file": "Arxiu de dispositiu", + "device-configuration": "Configuració del dispositiu", + "transport-configuration": "Configuració del transport", + "wizard": { + "device-wizard": "Assistent de dispositiu", + "device-details": "Detalls del dispositiu", + "new-device-profile": "Crear un nou perfil de dispositiu", + "existing-device-profile": "Seleccionar un perfil existent", + "specific-configuration": "Configuració específica", + "customer-to-assign-device": "Client al que assignar el dispositiu", + "add-credentials": "Afegir credencial" + }, + "unassign-devices-from-edge-title": "Està segur de que desitja desassignar {count, plural, 1 {1 dispositivo} other {# dispositivos} }?", + "unassign-devices-from-edge-text": "Després de la confirmació, tots els dispositius seleccionats quedaran sense assignar i la vora no podrà accedir a ells." + }, + "device-profile": { + "device-profile": "Perfil de dispositiu", + "device-profiles": "Perfils de dispositiu", + "all-device-profiles": "Tots", + "add": "Afegir perfil de dispositiu", + "edit": "Editar perfil de dispositiu", + "device-profile-details": "Detalls de perfil de dispositiu", + "no-device-profiles-text": "No s'han trobat perfils", + "search": "Buscar perfils", + "selected-device-profiles": "{ count, plural, 1 {1 perfil} other {# perfiles} } seleccionats", + "no-device-profiles-matching": "No existeix perfil que coincideixi amb '{{entity}}'.", + "device-profile-required": "Cal perfil de dispositiu", + "idCopiedMessage": "S'ha copiat el ID de perfil al porta-retalls", + "set-default": "Fer perfil per defecte", + "delete": "Esborrar perfil de dispositiu", + "copyId": "Copiar ID de perfil", + "name-max-length": "El nom ha de ser inferior a 256", + "new-device-profile-name": "Nom del perfil", + "new-device-profile-name-required": "Cal nom de perfil.", + "name": "Nom", + "name-required": "Cal nom.", + "type": "Tipus de perfil", + "type-required": "Cal tipus de perfil.", + "type-default": "Per defecte", + "image": "Imatge del perfil del dispositiu", + "transport-type": "Tipus de transport", + "transport-type-required": "Cal tipus de transport.", + "transport-type-default": "Per defecte", + "transport-type-default-hint": "Suporta transports per MQTT bàsic, HTTP i CoAP", + "transport-type-mqtt": "MQTT", + "transport-type-mqtt-hint": "Activa configuracions avançats de transport MQTT", + "transport-type-coap": "CoAP", + "transport-type-coap-hint": "Activa la configuració avançada de transport CoAP", + "transport-type-lwm2m": "LWM2M", + "transport-type-lwm2m-hint": "Transport LWM2M", + "transport-type-snmp": "SNMP", + "transport-type-snmp-hint": "Especifiqueu la configuració de transport SNMP", + "description": "Descripció", + "default": "Defecte", + "profile-configuration": "Configuració de perfil", + "transport-configuration": "Configuració de transport", + "default-rule-chain": "Cadena de regles per defecte", + "mobile-dashboard": "Tauler de control mòbil", + "mobile-dashboard-hint": "Utilitzat per l'aplicació mòbil com a tauler de detalls del dispositiu", + "select-queue-hint": "Selecciona des de el desplegable o afegir un nom personalitzat.", + "delete-device-profile-title": "Eliminar el perfil '{{deviceProfileName}}'?", + "delete-device-profile-text": "Atenció, després de la confirmació el perfil i totes les vostres dades seran esborrades i irrecuperables.", + "delete-device-profiles-title": "EEliminar { count, plural, 1 {1 perfil} other {# perfiles} }?", + "delete-device-profiles-text": "Atenció, després de la confirmació els perfils seleccionats i totes les vostres dades seran esborrades i irrecuperables.", + "set-default-device-profile-title": "Establir el perfil '{{deviceProfileName}}' com perfil per defecte?", + "set-default-device-profile-text": "Després de la confirmació, el perfil serà marcat com a defecte i serà utilitzat per tots els nous dispositius que no tinguin perfil especificat.", + "no-device-profiles-found": "No s'han trobar perfils.", + "create-new-device-profile": "Crear un nou perfil!", + "mqtt-device-topic-filters": "Filtres de topic MQTT", + "mqtt-device-topic-filters-unique": "Els filtres de topic de dispositiu MQTT han de ser únics.", + "mqtt-device-payload-type": "Payload de dispositiu MQTT", + "mqtt-device-payload-type-json": "JSON", + "mqtt-device-payload-type-proto": "Protobuf", + "mqtt-enable-compatibility-with-json-payload-format": "Activa la compatibilitat amb altres formats de càrrega útil.", + "mqtt-enable-compatibility-with-json-payload-format-hint": "Quan està habilitada, la plataforma utilitzarà un format de càrrega útil Protobuf de manera predeterminada. Si l'anàlisi falla, la plataforma intentarà utilitzar el format de càrrega útil JSON. Útil per a la compatibilitat enrere durant les actualitzacions de firmware. Per exemple, la versió inicial del firmware utilitza Json, mentre que la nova versió utilitza Protobuf. Durant el procés d'actualització del microprogramari per a la flota de dispositius, cal que admeti Protobuf i JSON simultàniament. El mode de compatibilitat introdueix una lleugera degradació del rendiment, per la qual cosa es recomana desactivar aquest mode un cop s'actualitzin tots els dispositius.", + "mqtt-use-json-format-for-default-downlink-topics": "Utilitzeu el format Json per als temes d'enllaç descendent predeterminat", + "mqtt-use-json-format-for-default-downlink-topics-hint": "Quan estigui habilitada, la plataforma utilitzarà el format de càrrega útil Json per enviar atributs i RPC mitjançant els temes següents: v1/devices/me/attributes/response/$request_id, v1/devices/me/attributes , v1/devices/me/rpc/request/$request_id, v1/devices/me/rpc/response/$request_id. Aquesta configuració no afecta les subscripcions d'atributs i rpc enviades amb temes nous (v2): v2/a/res/$request_id, v2/a, v2/r /req/$request_id, v2/r/res/$request_id. On $request_id és un identificador de sol·licitud d'enter.", + "mqtt-send-ack-on-validation-exception": "Enviar PUBACK en error de validació a la publicació (PUBLICAR)", + "mqtt-send-ack-on-validation-exception-hint": "Per defecte, la plataforma tancarà la sessió MQTT en cas d'error de validació del missatge. Quan estigui activat, la plataforma enviarà un reconeixement de publicació en lloc de tancar la sessió.", + "snmp-add-mapping": "Afegeix mapeig SNMP", + "snmp-mapping-not-configured": "No s'ha configurat cap mapeig per a OID a sèries temporals/telemetria", + "snmp-timseries-or-attribute-name": "Sèries temporals/nom d'atribut per al mapeig", + "snmp-timseries-or-attribute-type": "Sèries temporals/tipus d'atribut per al mapeig", + "snmp-method-pdu-type-get-request": "Obteniu la sol·licitud", + "snmp-method-pdu-type-get-next-request": "Obteniu la següent sol·licitud", + "snmp-oid": "OID", + "transport-device-payload-type-json": "JSON", + "transport-device-payload-type-proto": "Protobuf", + "mqtt-payload-type-required": "Cal tipus de Payload.", + "coap-device-type": "Tipus de dispositiu CoAP", + "coap-device-payload-type": "Càrrega útil del dispositiu CoAP", + "coap-device-type-required": "Cal tipus de dispositiu CoAP.", + "coap-device-type-default": "Per defecte", + "coap-device-type-efento": "Efento NB-IoT", + "support-level-wildcards": "Se suporten els wildcards únics [+] i multinivell [#].", + "telemetry-topic-filter": "Filtre de topic en telemetria", + "telemetry-topic-filter-required": "Cal filtre de topic (telemetria).", + "attributes-topic-filter": "Filtre de topic en atributs", + "attributes-topic-filter-required": "Cal filtre de topic (atributs).", + "telemetry-proto-schema": "Proto esquema de telemetria", + "telemetry-proto-schema-required": "Cal proto esquema de telemetria.", + "attributes-proto-schema": "Proto esquema de atributs", + "attributes-proto-schema-required": "Cal proto esquema de atributs.", + "rpc-response-proto-schema": "RPC resposta proto esquema", + "rpc-response-proto-schema-required": "Cal RPC resposta proto esquema.", + "rpc-response-topic-filter": "Filtre de topic de respuesta RPC", + "rpc-response-topic-filter-required": "Cal fitro de respuesta RPC.", + "rpc-request-proto-schema": "RPC petició proto esquema", + "rpc-request-proto-schema-required": "Cal RPC petició proto esquema.", + "rpc-request-proto-schema-hint": "RPC petició el missatge ha de tenir sempre camps: mètode de cadena = 1; int32 requestId = 2; i paràmetres = 3 de qualsevol tipus de dades.", + "not-valid-pattern-topic-filter": "No és un patró de filtre vàlid", + "not-valid-single-character": "Ús invàlid de wildcard únic", + "not-valid-multi-character": "Ús invàlid de wildcard multi-nivell", + "single-level-wildcards-hint": "[+] és adequat per qualsevol nivell. Ej.: v1/devices/+/telemetry o +/devices/+/attributes.", + "multi-level-wildcards-hint": "[#] pot reemplaçar el mateix filtre i ha de ser l'últim símbol del topic. Ej.: # o v1/devices/me/#.", + "alarm-rules": "Regles d'alarma", + "alarm-rules-with-count": "Regles d'alarma ({{count}})", + "no-alarm-rules": "No hi ha cap regla d'alarma configurada", + "add-alarm-rule": "Afegir regla d'alarma", + "edit-alarm-rule": "Editar regla d'alarma", + "alarm-type": "Tipus d'alarma", + "alarm-type-required": "Cal tipus d'alarma.", + "alarm-type-unique": "El tipus d'alarma, ha de ser únic dins de les regles d'alarma del perfil de dispositiu.", + "alarm-type-max-length": "El tipus d'alarma ha de ser inferior a 256", + "create-alarm-pattern": "Crear alarma {{alarmType}}", + "create-alarm-rules": "Crear regles d'alarma", + "no-create-alarm-rules": "No hi ha condicions de creació d'alarma configurades", + "add-create-alarm-rule-prompt": "Per favor, afegiu una regla d'alarma", + "clear-alarm-rule": "Esborrar regla d'alarma", + "no-clear-alarm-rule": "No hi ha condicions de esborrat d'alarma configurades", + "add-create-alarm-rule": "Afegir crear condició (activar alarma)", + "add-clear-alarm-rule": "Afegir esborrar condició (limpiar alarma)", + "select-alarm-severity": "Seleccioneu severitat d'alarma", + "alarm-severity-required": "Cal especificar severitat d'alarma.", + "condition-duration": "Duració de condició", + "condition-duration-value": "Valor de duració", + "condition-duration-time-unit": "Unitat del temps", + "condition-duration-value-range": "El valor ha d'estar en un rang des de 1 a 2147483647.", + "condition-duration-value-pattern": "El valor de duració ha de ser un número enter.", + "condition-duration-value-required": "Cal valor de duració.", + "condition-duration-time-unit-required": "Cal una unidad del temps.", + "advanced-settings": "Configuració avançada", + "alarm-rule-details": "Detalls", + "alarm-rule-details-hint": "Suggeriment: utilitzeu ${keyName} per substituir els valors de les claus d'atribut o de telemetria que s'utilitzen en condicions de regla d'alarma.", + "add-alarm-rule-details": "Afegir detalls", + "alarm-rule-mobile-dashboard": "Tauler de control mòbil", + "alarm-rule-mobile-dashboard-hint": "Utilitzat per l'aplicació mòbil com a quadre de comandament de detalls d'alarma", + "alarm-rule-no-mobile-dashboard": "No s'ha seleccionat cap tauler", + "propagate-alarm": "Propagar alarma", + "alarm-rule-relation-types-list": "Tipus de relació per propagar", + "alarm-rule-relation-types-list-hint": "Si no está seleccionat 'propagar relacions', les alarmes seran propagadas sense filtrar per relació.", + "propagate-alarm-to-owner": "Propaga l'alarma al propietari de l'entitat (Customer or Tenant)", + "propagate-alarm-to-owner-hierarchy": "Propaga l'alarma a la jerarquia dels propietaris de l'entitat", + "propagate-alarm-to-tenant": "Propaga l'alarma al llogater", + "alarm-details": "Detalls d'alarma", + "alarm-rule-condition": "Condicions de regla d'alarma", + "enter-alarm-rule-condition-prompt": "Per favor, afegir una condició d'alarma", + "edit-alarm-rule-condition": "Editar condició d'alarma", + "device-provisioning": "Aprovisionament de dispositius", + "provision-strategy": "Estrategia d'aprovisionament", + "provision-strategy-required": "Cal estrategia d'aprovisionament.", + "provision-strategy-disabled": "Desactivat", + "provision-strategy-created-new": "Permetre crear nous dispositius", + "provision-strategy-check-pre-provisioned": "Revisar dispositius pre-aprovisionats", + "provision-device-key": "Clau d'aprovisionament", + "provision-device-key-required": "Cal clau d'aprovisionament.", + "copy-provision-key": "Copiar clau d'aprovisionament", + "provision-key-copied-message": "La clau d'aprovisionament s'ha copiat al porta-retalls", + "provision-device-secret": "Secret d'aprovisionament", + "provision-device-secret-required": "Cal secret d'aprovisionament.", + "copy-provision-secret": "Copiar secret d'aprovisionament", + "provision-secret-copied-message": "S'ha copiat el secret d'aprovisionament al porta-retalls", + "condition": "Condició", + "condition-type": "Tipus de condició", + "condition-type-simple": "Simple", + "condition-type-duration": "Duració", + "condition-during": "Durant {{during}}", + "condition-during-dynamic": "Durant \"{{ attribute }}\" ({{during}})", + "condition-type-repeating": "Repetitiva", + "condition-type-required": "Cal tipus de condició.", + "condition-repeating-value": "Nº de esdeveniments", + "condition-repeating-value-range": "El Nº de esdeveniments ha d'estar en un rang de 1 to 2147483647.", + "condition-repeating-value-pattern": "Nº de esdeveniments ha de ser un número enter.", + "condition-repeating-value-required": "Cal Nº de esdeveniments.", + "condition-repeat-times": "Repetició { count, plural, 1 {1 vez} other {# veces} }", + "condition-repeat-times-dynamic": "Repeats \"{ attribute }\" ({ count, plural, 1 {1 time} other {# times} })", + "schedule-type": "Tipus d'horari", + "schedule-type-required": "Cal tipus d'horari.", + "schedule": "Horari", + "edit-schedule": "Editar horari d'alarma", + "schedule-any-time": "Sempre actiu", + "schedule-specific-time": "Actiu en una hora específica", + "schedule-custom": "Personalitzat", + "schedule-day": { + "monday": "Dilluns", + "tuesday": "Dimarts", + "wednesday": "Dimecres", + "thursday": "Dijous", + "friday": "Divendres", + "saturday": "Dissabte", + "sunday": "Diumenge" + }, + "schedule-days": "Dies", + "schedule-time": "Hora", + "schedule-time-from": "De", + "schedule-time-to": "Fins a", + "schedule-days-of-week-required": "Ha de ser seleccionat almenys un dia de la setmana.", + "create-device-profile": "Crea un perfil nou de dispositiu", + "import": "Importa el perfil del dispositiu", + "export": "Exporta el perfil del dispositiu", + "export-failed-error": "No s'ha pogut exportar el perfil del dispositiu: {{error}}", + "device-profile-file": "Fitxer de perfil del dispositiu", + "invalid-device-profile-file-error": "No s'ha pogut importar el perfil del dispositiu: l'estructura de dades del perfil del dispositiu no és vàlida.", + "power-saving-mode": "Mode d'estalvi d'energia", + "power-saving-mode-type": { + "default": "Utilitza el mode d'estalvi d'energia del perfil del dispositiu", + "psm": "Mode d'estalvi d'energia (PSM)", + "drx": "Recepció contínua (DRX)", + "edrx": "Recepció discontinu ampliada (eDRX)" + }, + "edrx-cycle": "Cicle eDRX", + "edrx-cycle-required": "Cal cicle eDRX.", + "edrx-cycle-pattern": "El cicle eDRX ha de ser un nombre enter positiu.", + "edrx-cycle-min": "El nombre mínim de cicles eDRX és {{ min }} segons.", + "paging-transmission-window": "Paging Transmission Window", + "paging-transmission-window-required": "Cal finestra de transmissió de paginació.", + "paging-transmission-window-pattern": "La finestra de transmissió de paginació ha de ser un nombre enter positiu.", + "paging-transmission-window-min": "El nombre mínim de finestra de transmissió de paginació és {{ min }} segons.", + "psm-activity-timer": "Temporitzador d'activitats PSM", + "psm-activity-timer-required": "Cal temporitzador d'activitats PSM.", + "psm-activity-timer-pattern": "El temporitzador d'activitat PSM ha de ser un nombre enter positiu.", + "psm-activity-timer-min": "El nombre mínim de temporitzadors d'activitat PSM és {{ min }} segons.", + "lwm2m": { + "object-list": "Llista d'objectes", + "object-list-empty": "No s'ha seleccionat cap objecte.", + "no-objects-found": "No s'ha trobat cap objecte.", + "no-objects-matching": "No hi ha cap objecte que coincideixi '{{object}}' els van trobar.", + "model-tab": "Model LWM2M", + "add-new-instances": "Afegeix noves instàncies", + "instances-list": "Llista d'instàncies", + "instances-list-required": "Cal llista d'instàncies.", + "instance-id-pattern": "L'identificador de la instància ha de ser un nombre enter positiu.", + "instance-id-max": "Valor màxim d'identificador de la instància {{max}}.", + "instance": "Instància", + "resource-label": "#ID Nom del recurs", + "observe-label": "Observa", + "attribute-label": "Atribut", + "telemetry-label": "Telemetria", + "edit-observe-select": "Per editar, seleccioneu la telemetria o l'atribut", + "edit-attributes-select": "Per editar els atributs, seleccioneu la telemetria o l'atribut", + "no-attributes-set": "No s'han establert atributs", + "key-name": "Nom clau", + "key-name-required": "Cal nom clau", + "attribute-name": "Atribut de nom", + "attribute-name-required": "Cal atribut de nom.", + "attribute-value": "Valor de l'atribut", + "attribute-value-required": "Cal valor de l'atribut.", + "attribute-value-pattern": "El valor de l'atribut ha de ser un nombre enter positiu.", + "edit-attributes": "Edita els atributs: {{ name }}", + "view-attributes": "Mostra els atributs: {{ name }}", + "add-attribute": "Afegeix un atribut", + "edit-attribute": "Edita l'atribut", + "view-attribute": "Veure l'atribut", + "remove-attribute": "Eliminar l'atribut", + "delete-server-text": "Aneu amb compte, després de la confirmació, la configuració del servidor es tornarà irrecuperable.", + "delete-server-title": "Esteu segur que voleu suprimir el servidor?", + "mode": "Mode de configuració de seguretat", + "bootstrap-tab": "Bootstrap", + "bootstrap-server-legend": "Servidor Bootstrap (ShortId...)", + "lwm2m-server-legend": "Servidor LwM2M (ShortId...)", + "server": "Servidor", + "short-id": "Identificador curt del servidor", + "short-id-tooltip": "Identificador curt del servidor. S'utilitza com a enllaç a la instància d'objecte del servidor associada.\nAquest identificador identifica de manera única cada servidor LwM2M configurat per al client LwM2M.\nEl recurs S'HA d'establir quan el recurs del servidor Bootstrap té un valor 'fals'.\nEls valors ID:0 i ID :65535 NO S'HAN d'utilitzar per identificar el servidor LwM2M.", + "short-id-required": "Cal identificador curt del servidor.", + "short-id-range": "L'identificador de servidor curt hauria d'estar entre 1 i 65534.", + "short-id-pattern": "L'identificador de servidor curt ha de ser un nombre enter positiu.", + "lifetime": "Vida útil del registre del client", + "lifetime-required": "Cal vida útil del registre del client.", + "lifetime-pattern": "La vida útil del registre del client ha de ser un nombre enter positiu.", + "default-min-period": "Període mínim entre dues notificacions (s)", + "default-min-period-tooltip": "El valor predeterminat que el client LwM2M hauria d'utilitzar durant el període mínim d'una observació si no s'inclou aquest paràmetre en una observació..", + "default-min-period-required": "Cal període mínim.", + "default-min-period-pattern": "El període mínim ha de ser un nombre enter positiu.", + "notification-storing": "Emmagatzematge de notificacions quan està desactivat o fora de línia", + "binding": "Enquadernació", + "binding-type": { + "u": "U: Es pot contactar amb el client mitjançant l'enllaç UDP en qualsevol moment.", + "m": "M: Es pot contactar amb el client mitjançant l'enllaç MQTT en qualsevol moment.", + "h": "H: El client és accessible mitjançant l'enllaç HTTP en qualsevol moment.", + "t": "T: Es pot contactar amb el client mitjançant l'enllaç TCP en qualsevol moment.", + "s": "S: Es pot contactar amb el client mitjançant l'enllaç SMS en qualsevol moment.", + "n": "N: El client HA d'enviar la resposta a aquesta sol·licitud mitjançant l'enllaç no IP (s'admet des de LWM2M 1.1).", + "uq": "UQ: Connexió UDP en mode de cua (no s'admet des de LWM2M 1.1)", + "uqs": "UQS: les connexions UDP i SMS actives; UDP en mode de cua, SMS en mode estàndard (no s'admet des de LWM2M 1.1)", + "tq": "TQ: Connexió TCP en mode de cua (no s'admet des de LWM2M 1.1)", + "tqs": "TQS: les connexions TCP i SMS actives; TCP en mode de cua, SMS en mode estàndard (no s'admet des de LWM2M 1.1)", + "sq": "SQ: Connexió d'SMS en mode de cua (no s'admet des de LWM2M 1.1)" + }, + "binding-tooltip": "Aquesta és la llista del recurs\"enllaç\" de l'objecte del servidor LwM2M - /1/x/7.\nIndica els modes d'enllaç admesos al client LwM2M.\nAquest valor HA de ser el mateix que el valor de 'Supported'. Enllaç i modes” a l'objecte del dispositiu (/3/0/16).\nTot i que s'admeten diversos transports, només es pot utilitzar una vinculació de transport durant tota la sessió de transport.\nPer exemple, quan s'admeten UDP i SMS. , el client LwM2M i el servidor LwM2M poden triar comunicar-se mitjançant UDP o SMS durant tota la sessió de transport.", + "bootstrap-server": "Servidor Bootstrap", + "lwm2m-server": "Servidor LwM2M", + "include-bootstrap-server": "Inclou actualitzacions del servidor Bootstrap", + "bootstrap-update-title": "Ja heu configurat el servidor Bootstrap. Esteu segur que voleu excloure les actualitzacions?", + "bootstrap-update-text": "Aneu amb compte, després de la confirmació, les dades de configuració del servidor Bootstrap seran irrecuperables.", + "server-host": "Host", + "server-host-required": "Cal host.", + "server-port": "Port", + "server-port-required": "Cal port.", + "server-port-pattern": "El port ha de ser un nombre enter positiu.", + "server-port-range": "El port ha d'estar en un rang d'1 a 65535.", + "server-public-key": "Clau pública del servidor", + "server-public-key-required": "Cal clau pública del servidor.", + "client-hold-off-time": "Temps d'espera", + "client-hold-off-time-required": "cal temps d'espera.", + "client-hold-off-time-pattern": "El temps d'espera ha de ser un enter positiu.", + "client-hold-off-time-tooltip": "El client manté el temps d'espera per al seu ús només amb un servidor Bootstrap", + "account-after-timeout": "Compte després del temps d'espera", + "account-after-timeout-required": "El compte després del temps d'espera és obligatori.", + "account-after-timeout-pattern": "El compte després del temps d'espera ha de ser un enter positiu.", + "account-after-timeout-tooltip": "Compte de servidor d'arrencada després del valor de temps d'espera donat per aquest recurs.", + "server-type": "Tipus de servidor", + "add-new-server-title": "Afegeix una nova configuració del servidor", + "add-server-config": "Afegeix la configuració del servidor", + "add-lwm2m-server-config": "Afegeix el servidor LwM2M", + "no-config-servers": "No s'ha configurat cap servidor", + "others-tab": "Altres configuracions", + "client-strategy": "Estratègia del client a l'hora de connectar-se", + "client-strategy-label": "Estratègia", + "client-strategy-only-observe": "Observeu només la sol·licitud al client després de la connexió inicial", + "client-strategy-read-all": "Llegiu tots els recursos i observeu la sol·licitud al client després del registre", + "fw-update": "Actualització del firmware", + "fw-update-strategy": "Estratègia d'actualització del firmware", + "fw-update-strategy-data": "Envieu l'actualització del microprogramari com a fitxer binari mitjançant l'objecte 19 i el recurs 0 (dades)", + "fw-update-strategy-package": "Envieu l'actualització del microprogramari com a fitxer binari mitjançant l'objecte 5 i el recurs 0 (paquet)", + "fw-update-strategy-package-uri": "Genera automàticament un URL CoAP únic per descarregar el paquet i enviar l'actualització del microprogramari com a Objecte 5 i Recurs 1 (URI del paquet)", + "sw-update": "Actualització de software", + "sw-update-strategy": "Estratègia d'actualització de programari", + "sw-update-strategy-package": "Envieu el fitxer binari mitjançant l'objecte 9 i el recurs 2 (paquet)", + "sw-update-strategy-package-uri": "Genera automàticament un URL CoAP únic per descarregar el paquet i impulsar l'actualització del programari mitjançant Objecte 9 i Recurs 3 (URI del paquet)", + "fw-update-resource": "Recurs CoAP d'actualització del firmware", + "fw-update-resource-required": "Cal recurs CoAP d'actualització del firmware.", + "sw-update-resource": "Recurs CoAP d'actualització de programari", + "sw-update-resource-required": "Cal Recurs CoAP d'actualització de programari.", + "config-json-tab": "Dispositiu de perfil de configuració Json", + "attributes-name": { + "min-period": "Període mínim", + "max-period": "Període màxim", + "greater-than": "Més gran que", + "less-than": "Menys que", + "step": "Pas", + "min-evaluation-period": "Període mínim d'avaluació", + "max-evaluation-period": "Període màxim d'avaluació" + }, + "composite-operations-support": "Admet operacions compostes de lectura/escriptura/observació" + }, + "snmp": { + "add-communication-config": "Afegeix la configuració de comunicació", + "add-mapping": "Afegeix mapeig", + "authentication-passphrase": "Frase de contrasenya d'autenticació", + "authentication-passphrase-required": "Cal frase de contrasenya d'autenticació.", + "authentication-protocol": "Protocol d'autenticació", + "authentication-protocol-required": "Cal protocol d'autenticació.", + "communication-configs": "Configuracions de comunicació", + "community": "Cadena comunitària", + "community-required": "Cal cadena comunitària.", + "context-name": "Nom del context", + "data-key": "Clau de dades", + "data-key-required": "Cal clau de dades.", + "data-type": "Tipus de dades", + "data-type-required": "Cal tipus de dades.", + "engine-id": "ID del motor", + "host": "Host", + "host-required": "Cal host.", + "oid": "OID", + "oid-pattern": "Format OID no vàlid.", + "oid-required": "Cal OID.", + "please-add-communication-config": "Si us plau, afegiu la configuració de comunicació", + "please-add-mapping-config": "Si us plau, afegiu la configuració de mapes", + "port": "Port", + "port-format": "Format de port no vàlid.", + "port-required": "Cal port.", + "privacy-passphrase": "Frase de contrasenya de privadesa", + "privacy-passphrase-required": "Cal frase de contrasenya de privadesa.", + "privacy-protocol": "Protocol de privadesa", + "privacy-protocol-required": "Cal protocol de privadesa.", + "protocol-version": "Versió del protocol", + "protocol-version-required": "Cal versió del protocol.", + "querying-frequency": "Freqüència de consulta, ms", + "querying-frequency-invalid-format": "La freqüència de consulta ha de ser un nombre enter positiu.", + "querying-frequency-required": "Cal freqüència de consulta.", + "retries": "Reintents", + "retries-invalid-format": "Els reintents han de ser un nombre enter positiu.", + "retries-required": "Cal reintents.", + "scope": "Àmbit", + "scope-required": "Cal àmbit.", + "security-name": "Nom de seguretat", + "security-name-required": "Cal nom de seguretat.", + "timeout-ms": "Temps d'espera, ms", + "timeout-ms-invalid-format": "El temps d'espera ha de ser un nombre enter positiu.", + "timeout-ms-required": "Cal temps d'espera.", + "user-name": "Nom d'usuari", + "user-name-required": "Cal nom d'usuari." + } + }, + "dialog": { + "close": "Tancar diàleg" + }, + "direction": { + "column": "Columna", + "row": "Fila" + }, + "edge": { + "edge": "Vora", + "edge-instances": "Instàncies de Vora", + "edge-file": "Arxiu de vora", + "name-max-length": "El nom ha de ser inferior a 256", + "label-max-length": "L'etiqueta ha de ser inferior a 256", + "type-max-length": "El tipus ha de ser inferior a 256", + "management": "Gestió de vores", + "no-edges-matching": "No s'han trobar vores que coincideixin amb '{{entity}}'", + "add": "Afegir vora", + "no-edges-text": "No s'han trobar vores", + "edge-details": "Detalls del vora", + "add-edge-text": "Afegir nova vora", + "delete": "Eliminar vora", + "delete-edge-title": "Està segur de que desitja eliminar la vora '{{edgeName}}'?", + "delete-edge-text": "Aneu amb compte, després de la confirmació, la vora i totes les dades relacionades seran irrecuperables", + "delete-edges-title": "Està segur de que desitja edge {count, plural, 1 {1 borde} other {# bordes} }?", + "delete-edges-text": "Aneu amb compte, després de la confirmació s'eliminaran totes les vores seleccionades i totes les dades relacionades seran irrecuperables", + "name": "Nom", + "name-starts-with": "El nom de la vora comença per", + "name-required": "Cal nom", + "edge-license-key": "Clau de llicència Edge", + "edge-license-key-required": "Necessiteu la clau de llicència Edge", + "edge-license-key-max-length": "La clau de llicència Edge ha de ser inferior a 31", + "edge-license-key-hint": "Per obtenir la seva llicència, aneu a la pàgina de preus i seleccioneu la millor opció de llicència si escau.", + "cloud-endpoint": "Punt final del núvol", + "cloud-endpoint-required": "Cal punt final del núvol", + "cloud-endpoint-max-length": "Cloud Endpoint hauria de ser inferior a 256", + "cloud-endpoint-hint": "Edge requereix l'accés HTTP (s) a la nube (ThingsBoard CE / PE) per verificar la clau de llicència. Especifiqueu l'URL de la núvol a la que Edge pot connectar-se.", + "description": "Descripció", + "details": "Detalls", + "events": "Esdeveniments", + "copy-id": "Copiar ID de vora", + "id-copied-message": "El ID de vora s'ha copiat al porta-retalls", + "sync": "Sinc Edge", + "edge-required": "Cal Edge", + "edge-type": "Tipus de vora", + "edge-type-required": "Cal tipus de vora.", + "event-action": "Informació de l'entitat", + "entity-id": "ID d'entitat", + "select-edge-type": "Seleccionar tipus de vora", + "assign-to-customer": "Assignar al client", + "assign-to-customer-text": "Selecciona el client per assignar les vores", + "assign-edge-to-customer": "Assignar vora(s) al client", + "assign-edge-to-customer-text": "Selecciona les vores per assignar al client", + "assignedToCustomer": "Asignada a la client", + "edge-public": "Edge és pública", + "assigned-to-customer": "Assignat al client", + "unassign-from-customer": "Anul·lar assignació del client", + "unassign-edge-title": "Està segur de que desitja desassignar la vora '{{edgeName}}'?", + "unassign-edge-text": "Després de la confirmació, la vora quedará sense assignar i el client no podrà accedir a ell", + "unassign-edges-title": "Està segur de que desitja anul·lar l'assignació de {count, plural, 1 {1 borde} other {# bordes} }?", + "unassign-edges-text": "Després de la confirmació de totes les vores seleccionades, s'anul·larà l'assignació i el client no podrà accedir a ells.", + "make-public": "Fer públic la vora", + "make-public-edge-title": "Estás segur de que quieres fer públic el edge '{{edgeName}}'?", + "make-public-edge-text": "Després de la confirmació, la vora i totes les seves dades seran públiques i accessibles per altres", + "make-private": "Fer que edge sigui privat", + "public": "Públic", + "make-private-edge-title": "Està segur de que desitja que la vora '{{edgeName}}' sigui privat?", + "make-private-edge-text": "Després de la confirmació, la vora i totes les seves dades seran privades i altres no podrán accedir a ells", + "import": "Importar vora", + "label": "Etiqueta", + "load-entity-error": "Entitat no trobada. No s'ha pogut carregar la informació", + "assign-new-edge": "Assignar nova vora", + "unassign-from-edge": "Anul·lar assignació de vora", + "edge-key": "Clau de vora", + "copy-edge-key": "Copiar clau de vora", + "edge-key-copied-message": "La clau de vora s'ha copiat al porta-retalls", + "edge-secret": "Vora secret", + "copy-edge-secret": "Copiar vora secret", + "edge-secret-copied-message": "El secret de vora s'ha copiat al porta-retalls", + "edge-assets": "Gestionar actius de vores", + "edge-devices": "Gestionar dispositius de vora", + "edge-entity-views": "Gestionar vistes d'entitat de vora", + "edge-dashboards": "Administrar panells de vora", + "edge-rulechains": "Cadenes de regles de vora", + "assets": "Actius de vora", + "devices": "Dispositius de vora", + "entity-views": "Vistes d'entitat de vora", + "dashboard": "Panell de control Edge", + "dashboards": "Panells de vora", + "rulechain-templates": "Plantilles, de cadena de regles", + "rulechains": "Cadenes de regla de vora", + "search": "Vores de cerca", + "selected-edges": "{count, plural, 1 {1 borde} other {# bordes} } seleccionades", + "any-edge": "Qualsevol vora", + "no-edge-types-matching": "No s'han trobar tipus de aristas que coincideixin amb '{{entitySubtype}}'.", + "edge-type-list-empty": "No s'ha seleccionat cap tipus de vora.", + "edge-types": "Tipus de vores", + "enter-edge-type": "Introduïu el tipus de vora", + "deployed": "Desplegada", + "pending": "Pendent", + "downlinks": "Enllaços descendents", + "no-downlinks-prompt": "No s'han trobar enllaços descendents", + "sync-process-started-successfully": "El procés de sincronització és va iniciar correctament!", + "missing-related-rule-chains-title": "A la vora li falten cadenes de regles relacionades", + "missing-related-rule-chains-text": "Assignat a la (es) cadena (es) de regles de vora utilitza nodes de regles que reenvien missatges a cadenes de regles que no están assignades a aquest vora.

                Llista de cadenes de regles que manca:
                {{missingRuleChains}}", + "widget-datasource-error": "Aquest widget només admet la font de dades de l'entitat EDGE", + "assign-to-edge": "Assignar la vora", + "assign-to-edge-title": "Assignar grup (s) d'entitat a Edge", + "manage-edge-user-groups": "Administrar grups d'usuaris perimetrals", + "manage-edge-asset-groups": "Administrar grups d'actius perimetrals", + "manage-edge-device-groups": "Administrar grups de dispositius perimetrals", + "manage-edge-entity-view-groups": "Administrar grups de vistes d'entitats perimetrals", + "manage-edge-dashboard-groups": "Administrar grups de panells de vora", + "manage-edge-rule-chains": "Administrar cadenes de regles de vora", + "manage-edge-scheduler-events": "Administrar esdeveniments del programador de vora", + "select-group-to-add": "Selecciona el grup de destí per afegir les vores seleccionades", + "select-group-to-move": "Selecciona el grup de destí per moure les vores seleccionades", + "remove-edges-from-group": "Està segur de que desitja eliminar {count, plural, 1 {1 borde} other {# bordes} } del grup '{entityGroup}'?", + "group": "Grup de vores", + "list-of-groups": "{count, plural, 1 {Un grupo de bodre} other {Lista de # grupos de bordes} }", + "group-name-starts-with": "Grups de vora els quals comencen amb '{{prefix}}'", + "unassign-entity-group-from-edge-title": "Està segur de que desitja anul·lar l'assignació del grup d'entitat '{{entityGroupName}}'?", + "unassign-entity-group-from-edge-text": "Després de la confirmació, s'anul·larà l'assignació del grup d'entitats i no es podrà accedir a ell des de la vora.", + "unassign-entity-group-from-edge": "Desassignar grup d'entitats des de la vora", + "unassign-entity-groups-from-edge-title": "Està segur de que desitja anul·lar l'assignació de {{count}} grups d'entitats?", + "unassign-entity-groups-from-edge-text": "Després de la confirmació, els grups d'entitats seran desassignat i no seran accessibles des de la vora.", + "unassign-entity-groups-from-edge": "Anul·lar l'assignació de grups d'entitats des de la vora", + "unassign-scheduler-events-from-edge": "Anul·lar l'assignació de esdeveniments del programador des de la vora", + "unassign-scheduler-event-from-edge-title": "Està segur de que desitja anul·lar l'assignació del esdeveniment del programador '{{schedulerEventName}}'?", + "unassign-scheduler-event-from-edge-text": "Després de la confirmació, s'anul·larà l'assignació del esdeveniment del programador i la vora no podrà accedir a ell.", + "unassign-scheduler-events-from-edge-title": "Està segur de que desitja anul·lar l'assignació {{count}} esdeveniments del programador?", + "unassign-scheduler-events-from-edge-text": "Després de la confirmació, tots els esdeveniments del programador seleccionat seran desassignats i no seran accessibles per la vora.", + "manage-user-groups": "Administrar grups d'usuaris", + "manage-asset-groups": "Administrar grups d'actius", + "manage-device-groups": "Administrar grups de dispositius", + "manage-dashboard-groups": "Administrar grups de panells", + "manage-entity-view-groups": "Administrar grups de vista d'entitat", + "manage-scheduler-events": "Administrar esdeveniments del planificador", + "manage-rulechains": "Administrar cadenes de regles", + "assign-scheduler-event-to-edge-title": "Assignar esdeveniments del programador a Edge", + "assign-scheduler-event-to-edge-text": "Selecciona els esdeveniments del programador per assignar a la vora", + "assign-entity-groups-to-edge-text": "Selecciona els grups d'entitats per assignar a la vora" + }, + "edge-event": { + "type-dashboard": "Panell", + "type-asset": "Actiu", + "type-device": "Dispositiu", + "type-device-profile": "Perfil del dispositiu", + "type-entity-view": "Vista d'entitat", + "type-alarm": "Alarma", + "type-entity-group": "Grup d'Entitats", + "type-rule-chain": "Cadena de regles", + "type-rule-chain-metadata": "Metadades de la cadena de regles", + "type-edge": "Edge", + "type-user": "Usuari", + "type-customer": "Client", + "type-relation": "Relació", + "type-widgets-bundle": "Paquet de widgets", + "type-widgets-type": "Tipus de widgets", + "type-admin-settings": "Configuració d'administrador", + "type-scheduler-event": "Agenda d'esdeveniments", + "type-white-labeling": "Etiquetat blanc", + "type-login-white-labeling": "Inici de sessió Etiqueta blanca", + "type-custom-translation": "Traducció personalitzada", + "type-role": "Rol", + "type-group-permission": "Permís de grup", + "action-type-added": "Afegit", + "action-type-deleted": "S'ha suprimit", + "action-type-updated": "Actualitzat", + "action-type-post-attributes": "Atributs de publicació", + "action-type-attributes-updated": "Atributs actualitzats", + "action-type-attributes-deleted": "Atributs suprimits", + "action-type-timeseries-updated": "Sèries temporals actualitzades", + "action-type-credentials-updated": "Credencials actualitzades", + "action-type-assigned-to-customer": "Assignat al client", + "action-type-unassigned-from-customer": "No assignat del client", + "action-type-relation-add-or-update": "Afegir o actualitzar la relació", + "action-type-relation-deleted": "S'ha eliminat la relació", + "action-type-rpc-call": "Trucada RPC", + "action-type-alarm-ack": "Ack d'alarma", + "action-type-alarm-clear": "Alarma esborrada", + "action-type-assigned-to-edge": "Assignat a Edge", + "action-type-unassigned-from-edge": "Sense assignar des d'Edge", + "action-type-credentials-request": "Sol·licitud de credencials", + "action-type-entity-merge-request": "Sol·licitud de fusió d'entitats", + "action-type-added-to-entity-group": "S'ha afegit al grup d'entitats", + "action-type-removed-from-entity-group": "S'ha eliminat del grup d'entitats", + "action-type-change-owner": "Canvia de propietari", + "action-type-relations-deleted": "S'han suprimit les relacions" + }, + "error": { + "unable-to-connect": "Impossible connectar amb el servidor! Per favor, revisi la seva connexió a internet.", + "unhandled-error-code": "Codi d'error no controlat: {{errorCode}}", + "unknown-error": "Error desconegut" + }, + "entity": { + "entity": "Entitat", + "entities": "Entitats", + "entities-count": "Les entitats compten", + "aliases": "Àlies d'entitat", + "entity-alias": "Àlies d'entitat", + "unable-delete-entity-alias-title": "No ha estat possible eliminar l'àlies d'entitat", + "unable-delete-entity-alias-text": "L'àlies d'entitat '{{entityAlias}}' no pot ser eliminat, ja que s'està usant per als següents widgets:
                {{widgetsList}}", + "duplicate-alias-error": "Trobat un àlies duplicat '{{alias}}'.
                Els àlies d'entitat han de ser únics per a cada panell.", + "missing-entity-filter-error": "Falta el filtre per l'àlies '{{alias}}'.", + "configure-alias": "Configurar àlies '{{alias}}' ", + "alias": "Àlies", + "alias-required": "Cal àlies d'entitat.", + "remove-alias": "Eliminar àlies d'entitat", + "add-alias": "Afegir àlies d'entitat", + "entity-list": "Llista d'entitats", + "entity-type": "Tipus d'entitat", + "entity-types": "Tipus d'entitats", + "entity-type-list": "Llista de tipus d'entitat", + "any-entity": "Qualsevol entitat", + "enter-entity-type": "Introduir tipus d'entitat", + "no-entities-matching": "No s'han trobat entitats que coincideixin amb '{{entity}}' .", + "no-entity-types-matching": "No s'han trobat tipus d'entitat que coincideixin amb '{{entityType}}' .", + "name-starts-with": "Nom comença amb", + "help-text": "Utilitzeu '%' segons les necessitats: '%entity_name_contains%', '%entity_name_ends', 'entity_starts_with'.", + "use-entity-name-filter": "Utilitza filtre", + "entity-list-empty": "No hi ha entitats seleccionades.", + "entity-type-list-empty": "No hi ha tipus d'entitat seleccionats.", + "entity-name-filter-required": "Cal filtre de nom d'entitat.", + "entity-name-filter-no-entity-matched": "No hi ha entitats que comencen amb '{{entity}}' .", + "all-subtypes": "Tots", + "select-entities": "Seleccionar entitats", + "no-aliases-found": "No s'han trobat àlies.", + "no-alias-matching": "'{{alias}}' no trobat.", + "create-new-alias": "Crear nou àlies!", + "key": "Clau", + "key-name": "Nom de clau", + "no-keys-found": "No s'han trobat claus.", + "no-key-matching": "'{{key}}' no trobada.", + "create-new-key": "Crear nova clau!", + "type": "Tipus", + "type-required": "Cal tipus d'entitat.", + "type-device": "Dispositiu", + "type-devices": "Dispositius", + "list-of-devices": "{ count, plural, 1 {Un dispositivo} other {Lista de # Dispositivos} }", + "device-name-starts-with": "Dispositius els quals comencen per '{{prefix}}'", + "type-device-profile": "Perfil de dispositiu", + "type-device-profiles": "Perfils de dispositiu", + "list-of-device-profiles": "{ count, plural, 1 {un perfil} other {Lista de # perfiles} }", + "device-profile-name-starts-with": "Perfils els quals comenci per '{{prefix}}'", + "type-asset": "Actiu", + "type-assets": "Actius", + "list-of-assets": "{ count, plural, 1 {Un activo} other {Lista de # activos} }", + "asset-name-starts-with": "Actius els quals comencen per '{{prefix}}'", + "type-entity-view": "Vista Entitat", + "type-entity-views": "Vistes Entitats", + "list-of-entity-views": "{ count, plural, 1 {Una vista de entidad} other {Lista de # Vistas de Entitats} }", + "entity-view-name-starts-with": "Vistes de Entitats els quals comencen per '{{prefix}}'", + "type-rule": "Regla", + "type-rules": "Regles", + "list-of-rules": "{ count, plural, 1 {Una regla} other {Lista de # regles} }", + "rule-name-starts-with": "Regles les quals comencen per '{{prefix}}'", + "type-plugin": "Plugin", + "type-plugins": "Plugins", + "list-of-plugins": "{ count, plural, 1 {Un plugin} other {Lista de # plugins} }", + "plugin-name-starts-with": "Plugins els quals comencen per '{{prefix}}'", + "type-tenant": "Propietari", + "type-tenants": "Propietaris", + "list-of-tenants": "{ count, plural, 1 {Un propietario} other {Lista de # propietarios} }", + "tenant-name-starts-with": "Propietaris els quals comencen per '{{prefix}}'", + "type-tenant-profile": "Perfil de Propietari", + "type-tenant-profiles": "Perfils de propietari", + "list-of-tenant-profiles": "{ count, plural, 1 {Un perfil de propietario} other {Lista de # perfils de propietario} }", + "tenant-profile-name-starts-with": "Pefils de propietari els quals comencen per '{{prefix}}'", + "type-customer": "Client", + "type-customers": "Clients", + "list-of-customers": "{ count, plural, 1 {Un cliente} other {Lista de # clientes} }", + "customer-name-starts-with": "Clients els quals comencen per '{{prefix}}'", + "type-user": "Usuari", + "type-users": "Usuaris", + "list-of-users": "{ count, plural, 1 {Un usuario} other {Lista de # usuaris} }", + "user-name-starts-with": "Usuaris els quals comencen per '{{prefix}}'", + "type-dashboard": "Panell", + "type-dashboards": "Panells", + "list-of-dashboards": "{ count, plural, 1 {Un panel} other {Lista de # paneles} }", + "dashboard-name-starts-with": "Panells els quals comencen per '{{prefix}}'", + "type-alarm": "Alarma", + "type-alarms": "Alarmes", + "list-of-alarms": "{ count, plural, 1 {Una alarma} other {Lista de # alarmas} }", + "alarm-name-starts-with": "Alarmes les quals comencen amb '{{prefix}}'", + "type-rulechain": "Cadena de regles", + "type-rulechains": "Cadenes de regles", + "list-of-rulechains": "{ count, plural, 1 {Una cadena de regles} other {Lista de # cadenes de regles} }", + "rulechain-name-starts-with": "Cadenes de regles les quals comencen amb '{{prefix}}'", + "type-scheduler-event": "Planificador d'esdeveniment", + "type-scheduler-events": "Planificador d'esdeveniment", + "list-of-scheduler-events": "{ count, plural, 1 {One scheduler event} other {List of # scheduler events} }", + "scheduler-event-name-starts-with": "Planificador d'esdeveniment els quals comencen amb '{{prefix}}'", + "type-blob-entity": "Entitat blob", + "type-blob-entities": "Entitats blob", + "list-of-blob-entities": "{ count, plural, 1 {One blob entity} other {List of # blob entities} }", + "blob-entity-name-starts-with": "Entitats blob els quals comencen amb '{{prefix}}'", + "type-rulenode": "Node de regles", + "type-rulenodes": "Nodes de regles", + "list-of-rulenodes": "{ count, plural, 1 {Un nodo de regles} other {Lista de # nodes de regles} }", + "rulenode-name-starts-with": "Nodes de regles els quals comencen amb '{{prefix}}'", + "type-current-customer": "Client Actual", + "type-current-tenant": "Propietari Actual", + "type-current-user": "Usuari Actual", + "type-current-user-owner": "Usuari Propietari Actual", + "search": "Buscar entitats", + "selected-entities": "{ count, plural, 1 {1 entitat} other {# entitats} } seleccionades", + "entity-name": "Nom d'entitat", + "entity-label": "Etiqueta d'entitat", + "details": "Detalls d'entitat", + "no-entities-prompt": "No s'han trobat entitats", + "no-data": "No hi ha dades que mostrar", + "columns-to-display": "Columnes a Mostrar", + "type-api-usage-state": "Estat d'ús de la API", + "type-entity-group": "Grup d'entitats", + "type-converter": "Convertidor de dades", + "type-converters": "Convertidors de dades", + "list-of-converters": "{ count, plural, 1 {One data converter} other {List of # data converters} }", + "converter-name-starts-with": "Convertidors de dades els quals comencen amb '{{prefix}}'", + "type-integration": "Integració", + "type-integrations": "Integracions", + "list-of-integrations": "{ count, plural, 1 {One integration} other {List of # integrations} }", + "integration-name-starts-with": "Integracions les quals comencen amb '{{prefix}}'", + "type-role": "Rol", + "type-roles": "Rols", + "list-of-roles": "{ count, plural, 1 {One role} other {List of # roles} }", + "role-name-starts-with": "Rols els noms dels quals comencen per '{{prefix}}'", + "type-group-permission": "Permís de grup", + "type-edge": "Vora", + "type-edges": "Vores", + "list-of-edges": "{count, plural, 1 {Un borde} other {Lista de # bordes} }", + "edge-name-starts-with": "Vores les quals comencen amb '{{prefijo}}'", + "type-tb-resource": "Recurs", + "type-ota-package": "Paquet OTA" + }, + "entity-group": { + "entity-group": "Grup d'entitats", + "details": "Detalls", + "columns": "Columnes", + "add-column": "Afegir columna", + "column-value": "Valor", + "column-value-required": "Cal valor de la columna.", + "column-title": "Títol", + "default-sort-order": "Orde de clasificació predeterminat", + "default-sort-order-required": "Cal orde de clasificació predeterminat.", + "hide-in-mobile-view": "Móvil ocult", + "use-cell-style-function": "Utilizar funció d'estil de cel·la", + "use-cell-content-function": "Utilizar funció de contingut de cel·la", + "edit-column": "Editar columna", + "column-details": "Detalls de la columna", + "actions": "Accions", + "settings": "Configuració", + "search": "Cerca grups d'entitats", + "delete": "Eliminar grup d'entitats", + "name": "Nom", + "name-required": "Cal el nom.", + "name-max-length": "El nom ha de ser inferior a 256", + "description": "Descripció", + "add": "Afegir grup d'entitats", + "open-entity-group": "Grup d'entitats obert", + "add-entity-group-text": "Afegir nou grup d'entitats", + "no-entity-groups-text": "Grups d'entitats no trobats", + "entity-group-details": "Detalls del grup d'entitats", + "selected-entity-groups": "{ count, plural, 1 {1 entity group} other {# entity groups} } seleccionat", + "delete-entity-groups": "Eliminar grups d'entitats", + "delete-entity-group-title": "Està segur de que desitja eliminar el grup d'entitats '{{entityGroupName}}'?", + "delete-entity-group-text": "Aneu amb compte, després de la confirmació, el grup d'entitats i totes les dades relacionades seran irrecuperables.", + "delete-entity-groups-title": "Està segur de que desitja eliminar { count, plural, 1 {1 entity group} other {# entity groups} }?", + "delete-entity-groups-action-title": "Eliminar { count, plural, 1 {1 entity group} other {# entity groups} }", + "delete-entity-groups-text": "Aneu amb compte, després de la confirmació s'eliminaran tots els grups d'entitats seleccionats i totes les dades relacionades seran irrecuperables.", + "device-groups": "Grups de dispositius", + "asset-groups": "Grups d'actius", + "customer-groups": "Grups de clients", + "device-group": "Grup de dispositius", + "asset-group": "Grup d'actius", + "user-group": "Grup d'usuaris", + "user-groups": "Grups d'usuaris", + "customer-group": "Grup de clients", + "entity-view-groups": "Grups de vista d'entitat", + "entity-view-group": "Grup de vista d'entitat", + "edge-groups": "Grups de vores", + "edge-group": "Grup de vora", + "dashboard-groups": "Grups de panells", + "dashboard-group": "Grup de quadres de comandament", + "fetch-more": "Portar més", + "column-type": { + "column-type": "Tipus de columna", + "client-attribute": "Atribut del client", + "shared-attribute": "Atribut compartit", + "server-attribute": "Atribut del servidor", + "timeseries": "Series temporals", + "entity-field": "Camp d'entitat" + }, + "column-type-required": "Cal el tipus de columna.", + "entity-field": { + "created-time": "Temps de creació", + "name": "Nom", + "type": "Tipus", + "device_profile": "Perfil del dispositiu", + "assigned_customer": "Client Assignat", + "authority": "Autoritat", + "first_name": "Nom", + "last_name": "Cognom", + "email": "Correu electrònic", + "title": "Títol", + "country": "País", + "state": "Estat", + "city": "Ciutat", + "address": "Direcció", + "address2": "Direcció 2", + "zip": "Codi postal", + "phone": "Telèfon", + "label": "Etiqueta" + }, + "sort-order": { + "asc": "Ascendent", + "desc": "Descendent", + "none": "Ningú" + }, + "details-mode": { + "on-row-click": "Clic en la fila", + "on-action-button-click": "Clic en el botó de detalls", + "disabled": "Inhabilitat" + }, + "change-owner": "Change owner", + "select-target-owner": "Seleccioneu el propietari objectiu", + "no-owners-matching": "No hi ha cap propietari que coincideixi '{{owner}}' els van trobar.", + "target-owner-required": "Cal propietari objectiu.", + "confirm-change-owner-title": "Esteu segur que voleu canviar de propietari per { count, plural, 1 {1 selected entity} other {# selected entities} }?", + "confirm-change-owner-text": "Aneu amb compte, després de la confirmació, totes les entitats seleccionades s'eliminaran del propietari actual i es col·locaran al grup 'Tots' del propietari objectiu..", + "add-to-group": "Afegir al grup", + "move-to-group": "Moure al grup", + "select-entity-group": "Seleccionar grup d'entitats", + "no-entity-groups-matching": "Grups d'entitats que coincideixin amb '{{entityGroup}}' no han estat trobats.", + "target-entity-group-required": "Cal grup d'entitats objetiu.", + "select-user-group": "Seleccioneu el grup d'usuaris", + "no-user-groups-matching": "No hi ha cap grup d'usuaris que coincideixi '{{entityGroup}}' els van trobar.", + "target-user-group-required": "Cal grup d'usuaris objectiu.", + "remove-from-group": "Eliminar des de grup", + "group-table-title": "Títol de panell de grup", + "enable-search": "Habilitar cerca d'entitats", + "enable-add": "Habilitar agregació d'entitats", + "enable-delete": "Habilitar eliminació d'entitats", + "enable-selection": "Habilitar selecció d'entitats", + "enable-group-transfer": "Habilitar accions de transferencia de grup", + "display-pagination": "Mostrar paginació", + "default-page-size": "Mida de pàgina predeterminat", + "enable-assignment-actions": "Habilitar accions de assignació", + "enable-credentials-management": "Habilitar gestió de credencials", + "enable-login-as-user": "Activa l'inici de sessió com a acció de l'usuari", + "enable-users-management": "Habilitar gestió d'usuaris", + "enable-customers-management": "Habilitar la gestió de clients", + "enable-assets-management": "Habilitar gestió d'actius", + "enable-devices-management": "Habilitar gestió de dispositius", + "enable-entity-views-management": "Habilitar la gestió de vistes d'entitats", + "enable-edges-management": "Habilitar la gestió de vores", + "enable-dashboards-management": "Habilitar gestió de panells", + "enable-scheduler-events-management": "Habilitar la gestió de esdeveniments del programador", + "open-details-on": "Obrir detalls de l'entitat en", + "select-existing": "Seleccionar grup d'entitats existent", + "create-new": "Crear nou grup d'entitats", + "new-entity-group-name": "Nou nom del grup d'entitats", + "entity-group-list": "Llista de grup d'entitats", + "entity-group-list-empty": "Grups d'entitats no seleccionats.", + "name-starts-with": "El nom de l'entitat de grup comença amb", + "entity-group-name-filter-required": "Cal el nom de filtre per entitat de grup.", + "roles": "Rols", + "permissions": "Permissions", + "public": "Públic", + "entity-group-public": "El grup d'entitats és públic", + "make-public": "Fer públic el grup d'entitats", + "make-private": "Fes que el grup d'entitats sigui privat", + "make-public-entity-group-title": "Esteu segur que voleu fer el grup d'entitats '{{entityGroupName}}' públic?", + "make-public-entity-group-text": "Després de la confirmació, el grup d'entitats i totes les seves entitats es faran públiques i es faran accessibles per altres.", + "make-private-entity-group-title": "Esteu segur que voleu fer el grup d'entitats '{{entityGroupName}}' private?", + "make-private-entity-group-text": "Després de la confirmació, el grup d'entitats i totes les seves entitats es convertiran en privats i no seran accessibles per altres.", + "share": "Comparteix el grup d'entitats", + "copyId": "Copia l'identificador del grup d'entitats", + "idCopiedMessage": "L'identificador del grup d'entitats s'ha copiat al porta-retalls", + "entity-group-name": "Nom del grup d'entitats", + "all-users": "Tots els usuaris" + }, + "entity-field": { + "created-time": "Hora de creació", + "name": "Nom", + "type": "Tipus", + "first-name": "Nom", + "last-name": "Cognom", + "email": "Correu electrònic", + "title": "Títol", + "country": "País", + "state": "Estat", + "city": "Ciudad", + "address": "Direcció", + "address2": "Direcció 2", + "zip": "Códi postal", + "phone": "Telèfon", + "label": "Etiqueta" + }, + "entity-view": { + "entity-view": "Vista d'entitat", + "entity-view-required": "Cal vista d'entitat.", + "entity-views": "Vistes d'entitat", + "management": "Gestió de vistes d'entitat", + "view-entity-views": "Veure vista d'entitat", + "entity-view-alias": "Àlies de vista d'entitat", + "aliases": "Àlies de vista d'entitat", + "no-alias-matching": "'{{alias}}' no trobat.", + "no-aliases-found": "No s'han trobar àlies.", + "no-key-matching": "'{{key}}' no trobada.", + "no-keys-found": "No s'han trobar claus.", + "create-new-alias": "¡Crear un nou!", + "create-new-key": "¡Crear una nova!", + "duplicate-alias-error": "Àlies duplicat'{{alias}}'.
                Els àlies de Entity View han de ser els únics en el panell.", + "configure-alias": "Configurar àlies '{{alias}}'", + "no-entity-views-matching": "No s'han trobar vistes que coincideixin amb '{{entity}}'.", + "public": "Públic", + "alias": "Àlies", + "alias-required": "Cal àlies de vista d'entitat.", + "remove-alias": "Esborrar àlies de la vista d'entitat", + "add-alias": "Afegir àlies a la vista d'entitat", + "name-starts-with": "Nom de vista d'entitat comença amb", + "help-text": "Utilitzeu '%' segons les necessitats: '%entity-view_name_contains%', '%entity-view_name_ends', 'entity-view_starts_with'.", + "entity-view-list": "Llista de vistes d'entitat", + "use-entity-view-name-filter": "Utilitzar el filtre", + "entity-view-list-empty": "No hi ha vistes d'entitat seleccionades.", + "entity-view-name-filter-required": "Cal nom del filtre de vista d'entitat.", + "entity-view-name-filter-no-entity-view-matched": "No s'han trobar vistes d'entitat que comencen amb '{{entityView}}'.", + "add": "Afegir vista d'entitat", + "entity-view-public": "Vista d'entitat és pública", + "assign-to-customer": "Assignar a client", + "assign-entity-view-to-customer": "Assignar vista d'entitat a client", + "assign-entity-view-to-customer-text": "Per favor, selecciona les vistes d'entitat per assignar al client", + "no-entity-views-text": "No s'han trobar vistas d'entitat", + "assign-to-customer-text": "Per favor, selecciona el client per assignar la vista d'entitat", + "entity-view-details": "Detalls de la vista d'entitat", + "add-entity-view-text": "Afegir nova vista d'entitat", + "delete": "Esborrar vista d'entitat", + "assign-entity-views": "Assignar vistes d'entitat", + "assign-entity-views-text": "Assignar { count, plural, 1 {1 vista de entidad} other {# vistas de entidad} } a client", + "delete-entity-views": "Esborrar vistes d'entitat", + "make-public": "Fer pública la vista d'entitat", + "make-private": "Fer que la vista d'entitat sigui privada", + "unassign-from-customer": "Anul·lar assignació a client", + "unassign-entity-views": "Anul·lar assignació de vistes d'entitat", + "unassign-entity-views-action-title": "Anul·lar assignació { count, plural, 1 {1 vista de entidad} other {# vistas de entidad} } al client", + "assign-new-entity-view": "Assignar nova vista d'entitat", + "delete-entity-view-title": "Esteu segur que voleu esborrar la vista entitat '{{entityViewName}}'?", + "delete-entity-view-text": "Compte! Després de la confirmació, la vista de l'entitat i totes les dades relacionades seran irrecuperables.", + "delete-entity-views-title": "Esteu segur que voleu esborrar les vistes d'entitat { count, plural, 1 {1 entityView} other {# entityViews} }?", + "delete-entity-views-action-title": "Esborrar { count, plural, 1 {1 vista de entidad} other {# vistas de entidad} }", + "delete-entity-views-text": "Compte! Després de la confirmació, totes les vistes d'entitats seleccionades s'eliminaran i totes les dades relacionades seran irrecuperables.", + "make-public-entity-view-title": "Està segur de que desitja que la vista d'entitat '{{entityViewName}}' sigui pública?", + "make-public-entity-view-text": "Després de la confirmació, la vista de l'entitat i totes les dades seran públiques i accessibles per altres.", + "make-private-entity-view-title": "Està segur de que desitja que la vista d'entitat '{{entityViewName}}' sigui privada?", + "make-private-entity-view-text": "Després de la confirmació, la vista de l'entitat i totes les dades seran privats i no seran accessibles per altres.", + "unassign-entity-view-title": "Esteu segur que voleu anul·lar l'assignació de la vista d'entitat '{{entityViewName}}'?", + "unassign-entity-view-text": "Després de la confirmació, la vista de l'entitat quedarà sense assignar i el client no hi podrà accedir.", + "unassign-entity-view": "Anul·lar assignació de la vista d'entitat", + "unassign-entity-views-title": "Esteu segur que voleu anul·lar l'assignació de { count, plural, 1 {1 vista de entidad} other {# vistas de entidad} }?", + "unassign-entity-views-text": "Després de la confirmació, totes les vistes d'entitats seleccionades quedaran sense assignar i el client no podrà accedir-hi.", + "entity-view-type": "Tipus de vista d'entitat", + "entity-view-type-required": "Cal tipus de vista d'entitat.", + "select-entity-view-type": "Selecciona el tipus de vista d'entitat", + "enter-entity-view-type": "Teclegeu el tipus de vista d'entitat", + "any-entity-view": "Qualsevol vista d'entitat", + "no-entity-view-types-matching": "No s'han trobar tipus de vista d'entitat que coincideixin amb '{{entitySubtype}}'.", + "entity-view-type-list-empty": "No hi ha tipus de vista d'entitat seleccionada.", + "entity-view-types": "Tipus de vista d'entitat", + "created-time": "Data de creació", + "name": "Nom", + "name-required": "Cal nom.", + "name-max-length": "El nom ha de ser inferior a 256", + "type-max-length": "El tipus de visualització d'entitat ha de ser inferior a 256", + "description": "Descripció", + "events": "Esdeveniments", + "details": "Detalls", + "copyId": "Copiar el Id de la vista d'entitat", + "idCopiedMessage": "El Id de la vista d'entitat s'ha copiat al porta-retalls", + "assignedToCustomer": "Assignat a client", + "unable-entity-view-device-alias-title": "No es pot eliminar l'àlies de vista d'entitat", + "unable-entity-view-device-alias-text": "L'àlies del dispositiu '{{entityViewAlias}}' no es pot borrar perquè està sent usat pel widget(s):
                {{widgetsList}}", + "select-entity-view": "Seleccionar vista d'entitat", + "start-date": "Data d'inici", + "start-ts": "Temps d'inici", + "end-date": "Data de finalització", + "end-ts": "Temps de finalització", + "date-limits": "Limits de data", + "client-attributes": "Atributs de client", + "shared-attributes": "Atributs compartits", + "server-attributes": "Atributs de servidor", + "timeseries": "Series temporals", + "client-attributes-placeholder": "Atributs de client", + "shared-attributes-placeholder": "Atributs compartits", + "server-attributes-placeholder": "Atributs de servidor", + "timeseries-placeholder": "Series temporals", + "target-entity": "Entitat objetiu", + "attributes-propagation": "Propagació de atributs", + "attributes-propagation-hint": "La vista d'entitat copiarà automàticament els atributs especificats de l'entitat de destí cada vegada que guardi o actualitzi esta vista d'entitat. Per razones de rendimiento, els atributs d'entitat objetiu no es propagan a la vista d'entitat en cada cambio d'atribut. Puede habilitar la propagación automàtica configurando el node de la regla \"copiar a la vista\" en la seva cadena de regles i vincular els missatges \"Atributs de la publicación\" i \"Atributs actualizados\" al nou node de la regla.", + "timeseries-data": "Dades de series temporals", + "timeseries-data-hint": "Configura les claus de les dades de les series temporals de l'entitat de destí que seran accessibles per la vista de l'entitat. Les dades d'aquesta sèrie temporal són de només lectura.", + "selected-entity-views": "{ count, plural, 1 {1 vista de entidad} other {# vistas de entidad} } seleccionades", + "search": "Buscar vistes d'entitat", + "select-group-to-add": "Seleccioneu el grup objectiu per afegir visualitzacions d'entitat seleccionades", + "select-group-to-move": "Seleccioneu el grup objectiu per moure les vistes d'entitat seleccionades", + "remove-entity-views-from-group": "Esteu segur que voleu eliminar-lo { count, plural, 1 {1 entity view} other {# entity views} } del grup '{{entityGroup}}'?", + "group": "Grup de vistes d'entitat", + "list-of-groups": "{ count, plural, 1 {One entity view group} other {List of # entity view groups} }", + "group-name-starts-with": "Grups de visualització d'entitats els noms dels quals comencen per '{{prefix}}'", + "assign-entity-view-to-edge": "Assignar vista (es) d'entitat a vora", + "assign-entity-view-to-edge-text": "Selecciona les vistes d'entitat per assignar a la vora", + "unassign-entity-view-from-edge-title": "Està segur de que desitja anul·lar l'assignació de la vista d'entitat '{{entityViewName}}'?", + "unassign-entity-view-from-edge-text": "Després de la confirmació, la vista d'entitat quedará sense assignar i la vora no podrà accedir a ella", + "unassign-entity-views-from-edge-action-title": "Anul·lar assignació {count, plural, 1 {1 vista de entidad} other {# vistas de entidad} } del vora", + "unassign-entity-view-from-edge": "Anul·lar assignació de vista d'entitat", + "unassign-entity-views-from-edge-title": "Està segur de que desitja desassignar {count, plural, 1 {1 vista de entidad} other {# vistas de entidad} }?", + "unassign-entity-views-from-edge-text": "Després de la confirmació, totes les vistas d'entitat seleccionades no seran assignades i la vora no podrà accedir a ellas" + }, + "event": { + "events": "Esdeveniments", + "event-type": "Tipus d'esdeveniment", + "events-filter": "Filtre d'esdeveniments", + "clean-events": "Esborrar esdeveniments", + "type-error": "Error", + "type-lc-event": "Cicle de vida de l'esdeveniment", + "type-stats": "Estadístiques", + "type-debug-converter": "Depurar", + "type-debug-integration": "Depurar", + "type-debug-rule-node": "Depuració", + "type-debug-rule-chain": "Depuració", + "no-events-prompt": "Cap esdeveniment trobat.", + "error": "Error", + "alarm": "Alarma", + "event-time": "Hora de l'esdeveniment", + "server": "Servidor", + "body": "Cos", + "method": "Mètode", + "type": "Tipus", + "in": "Dins", + "out": "Fora", + "metadata": "Metadades", + "message": "Missatge", + "entity": "Entitat", + "message-id": "Id Missatge", + "message-type": "Tipus Missatge", + "data-type": "Tipus de Dades", + "relation-type": "Tipus de relació", + "data": "Dades", + "event": "Esdeveniment", + "status": "Estat", + "success": "Èxit", + "failed": "Fallada", + "messages-processed": "Missatges processats", + "min-messages-processed": "Mínim missatges processats", + "errors-occurred": "Van passar errors", + "min-errors-occurred": "S'han produït errors mínims", + "min-value": "S'han produït errors mínims.", + "all-events": "Tots", + "has-error": "Té error", + "entity-id": "Identificador de l'entitat", + "entity-type": "Tipus d'entitat", + "clear-filter": "Esborra el filtre", + "clear-request-title": "Esborra tots els esdeveniments", + "clear-request-text": "Esteu segur que voleu esborrar tots els esdeveniments?", + "type-edge-event": "Enllaç descendent" + }, + "extension": { + "extensions": "Extensions", + "selected-extensions": "{ count, plural, 1 {1 extensión} other {# extensiones} } seleccionades", + "type": "Tipus", + "key": "Clau", + "value": "Valor", + "id": "ID", + "extension-id": "ID d'extensió", + "extension-type": "Tipus d'extensió", + "transformer-json": "JSON *", + "unique-id-required": "El id d'extensió ja existeix.", + "delete": "Esborrar Extensió", + "add": "Afegir Extensió", + "edit": "Editar Extensió", + "view": "Mostra l'extensió", + "delete-extension-title": "Eliminar la extensió '{{extensionId}}'?", + "delete-extension-text": "Atenció, després de la confirmació, l'extensió i els seus dades seran esborrades i irrecuperables.", + "delete-extensions-title": "Eliminar les extensions { count, plural, 1 {1 extensión} other {# extensiones} }?", + "delete-extensions-text": "Atenció, després de la confirmació totes les extensions seleccionades i els seus dades seran esborrats i irrecuperables.", + "converters": "Convertidors", + "converter-id": "Id convertidor", + "configuration": "Configuració", + "converter-configurations": "Configuració de convertidor", + "token": "Tóken de seguretat", + "add-converter": "Afegir convertidor", + "add-config": "Afegir configuració de convertidor", + "device-name-expression": "Expressió del nom de dispositiu", + "device-type-expression": "Expressió del tipus de dispositiu", + "custom": "Personalitzat", + "to-double": "Per a duplicar", + "transformer": "Transformador", + "json-required": "Cal el JSON del transformador.", + "json-parse": "No ha estat possible analitzar el JSON del transformador.", + "attributes": "Atributs", + "add-attribute": "Afegir Atribut", + "add-map": "Afegir element de mapejat", + "timeseries": "Series del temps", + "add-timeseries": "Afegir series del temps", + "field-required": "Cal Camp", + "brokers": "Corredors", + "add-broker": "Afegir corredor", + "host": "Host", + "port": "Port", + "port-range": "El port ha d'estar en un rang de 1 a 65535.", + "ssl": "SSL", + "credentials": "Credencials", + "username": "Usuari", + "password": "Contrasenya", + "retry-interval": "Interval de reintent en mil·lisegons", + "sas": "Signatura d'accés compartit", + "anonymous": "Anònim", + "basic": "Bàsic", + "pem": "PEM", + "ca-cert": "Arxiu de certificat CA *", + "private-key": "Arxiu de clau privat *", + "cert": "Arxiu certificat *", + "no-file": "Cap arxiu seleccionat.", + "drop-file": "Colocar un arxiu o fer clic per seleccionar un arxiu per carregar .", + "mapping": "Mapeig", + "topic-filter": "Filtre de tema", + "converter-type": "Tipus de convertidor", + "converter-json": "Json", + "json-name-expression": "Expressió json per nom del dispositiu", + "topic-name-expression": "Expressió temàtica per nom del dispositiu", + "json-type-expression": "Expressió json per tipus de dispositiu", + "topic-type-expression": "Expressió temàtica per tipus de dispositiu", + "attribute-key-expression": "Expressió per clau d'atribut", + "attr-json-key-expression": "Expressió json per clau d'atribut", + "attr-topic-key-expression": "Expressió temàtica per clau d'atribut", + "request-id-expression": "Expressió per sol·licitud d'ID", + "request-id-json-expression": "Expressió json per sol·licitud d'ID", + "request-id-topic-expression": "Expressió temàtica per sol·licitud d'ID", + "response-topic-expression": "Expressió temàtica per respuesta", + "value-expression": "Expressió per valor", + "topic": "Tema", + "timeout": "Temps d'espera en mil·lisegons", + "converter-json-required": "Cal convertidor json.", + "converter-json-parse": "No es pot analitzar el convertidor json.", + "filter-expression": "Expressió per filtre", + "connect-requests": "Sol·licituds de connexió", + "add-connect-request": "Afegir solicituts de connexió", + "disconnect-requests": "Sol·licituds de desconexió", + "add-disconnect-request": "Afegir sol·licitud de desconexió", + "attribute-requests": "Sol·licituds d'atribut", + "add-attribute-request": "Afegir solicituts d'atribut", + "attribute-updates": "Actualizaciones d'atribut", + "add-attribute-update": "Afegir actualitzacions d'atribut", + "server-side-rpc": "RPC costat servidor", + "add-server-side-rpc-request": "Afegir sol·licitud RPC costat servidor", + "device-name-filter": "Filtre de nom de dispositiu", + "attribute-filter": "Filtre d'atribut", + "method-filter": "Filtre de mètode", + "request-topic-expression": "Expressió temàtica per sol·licitud", + "response-timeout": "Temps d'espera de respuesta en mil·lisegons", + "topic-expression": "Expressió temàtica", + "client-scope": "Alcance del client", + "add-device": "Afegir dispositiu", + "opc-server": "Servidors", + "opc-add-server": "Afegir servidor", + "opc-add-server-prompt": "Per favor afegir servidor", + "opc-application-name": "Nom d'aplicació", + "opc-application-uri": "Aplicació URI", + "opc-scan-period-in-seconds": "Període d'exploració en segons", + "opc-security": "Seguretat", + "opc-identity": "Identitat", + "opc-keystore": "Magatzem de claus", + "opc-type": "Tipus", + "opc-keystore-type": "Tipus", + "opc-keystore-location": "Ubicació *", + "opc-keystore-password": "Contrasenya", + "opc-keystore-alias": "Àlies", + "opc-keystore-key-password": "Clau de contrasenya", + "opc-device-node-pattern": "Patró de node de dispositiu", + "opc-device-name-pattern": "Patró de nom de dispositiu", + "modbus-server": "Servidors/esclaus", + "modbus-add-server": "Afegir servidor/esclau", + "modbus-add-server-prompt": "Per favor afegir servidor/esclau", + "modbus-transport": "Transport", + "modbus-tcp-reconnect": "Reconexió automàtica", + "modbus-rtu-over-tcp": "RTU sobre TCP", + "modbus-port-name": "Nom del port serial", + "modbus-encoding": "Codificació", + "modbus-parity": "Paritat", + "modbus-baudrate": "Taxa de bauds", + "modbus-databits": "Bits de dades", + "modbus-stopbits": "Bits de parada", + "modbus-databits-range": "Bits de dades han d'estar en un rang entre 7 i 8.", + "modbus-stopbits-range": "Bits de parada han d'estar en un rang entre 1 a 2.", + "modbus-unit-id": "ID de unidad", + "modbus-unit-id-range": "ID de unidad ha d'estar en un rang entre 1 a 247.", + "modbus-device-name": "Nom del dispositiu", + "modbus-poll-period": "Període de sondeig (ms)", + "modbus-attributes-poll-period": "Atributs del període de sondeig (ms)", + "modbus-timeseries-poll-period": "Període de sondeig de les series temporals (ms)", + "modbus-poll-period-range": "El període de sondeig ha de ser una valor positiu.", + "modbus-tag": "Etiqueta", + "modbus-function": "Funció", + "modbus-register-address": "Direcció del registre", + "modbus-register-address-range": "Direcció del registre ha d'estar en un rang entre 0 i 65535.", + "modbus-register-bit-index": "Índex de bit", + "modbus-register-bit-index-range": "Índex de bit ha d'estar en un rang entre 0 i 15.", + "modbus-register-count": "Contador del registre", + "modbus-register-count-range": "Contador del registre ha de ser un valor positiu.", + "modbus-byte-order": "Orde del byte", + "sync": { + "status": "Estat", + "sync": "Sincronitzat", + "not-sync": "No Sincronitzat", + "last-sync-time": "Hora d'última sincronització", + "not-available": "No disponible" + }, + "export-extensions-configuration": "Exportar configuració d'extensions", + "import-extensions-configuration": "Importar configuració d'extensions", + "import-extensions": "Importar extensions", + "import-extension": "Importar extensió", + "export-extension": "Exportar extensió", + "file": "Fitxer d'extensions", + "invalid-file-error": "Fitxer d'extensions invàlid", + "text": "TEXT", + "json": "JSON", + "binary": "BINARY", + "hex": "HEX" + }, + "filter": { + "add": "Afegir filtre", + "edit": "Editar filtre", + "name": "Nom de filtre", + "name-required": "Cal nom de filtre.", + "duplicate-filter": "Ja existeix un filtre amb el mateix nom.", + "filters": "Filtres", + "unable-delete-filter-title": "Error esborrant filtre", + "unable-delete-filter-text": "El filtre '{{filter}}' no pot ser borrat pel fet que està sent usat actualment pels següents widgets:
                {{widgetsList}}", + "duplicate-filter-error": "S'ha trobat un filtre duplicat '{{filter}}'.
                Els filtres han de ser únics en el panell.", + "missing-key-filters-error": "No s'ha trobar la clau de filtres per el filtre '{{filter}}'.", + "filter": "Filtre", + "editable": "Editable", + "no-filters-found": "No s'han trobar filtres.", + "no-filter-text": "No s'ha especificat filtre", + "add-filter-prompt": "Per favor, añadir filtre", + "no-filter-matching": "'{{filter}}' no trobat.", + "create-new-filter": "Crear un filtre nou!", + "filter-required": "Cal filtre.", + "operation": { + "operation": "Operació", + "equal": "igual", + "not-equal": "no igual", + "starts-with": "comença amb", + "ends-with": "acaba amb", + "contains": "conté", + "not-contains": "no conté", + "greater": "més gran que", + "less": "menor que", + "greater-or-equal": "més gran o igual", + "less-or-equal": "menor o igual", + "and": "i", + "or": "o", + "in": "dins de ", + "not-in": "no dins" + }, + "ignore-case": "Ignorar majús/minus", + "value": "Valor", + "remove-filter": "Esborrar filtre", + "preview": "Vista previa de filtre", + "no-filters": "No hi ha filtres configurats", + "add-filter": "Afegir filtre", + "add-complex-filter": "Afegir filtre complex", + "add-complex": "Afegir filtre complex", + "complex-filter": "Filtre complex", + "edit-complex-filter": "Editar filtre complex", + "edit-filter-user-params": "Editar paràmetres d'usuari del filtre", + "filter-user-params": "Filtre de paràmetres d'usuari (predicado)", + "user-parameters": "Paràmetres d'usuari", + "display-label": "Etiqueta a mostrar", + "autogenerated-label": "Auto generar etiqueta", + "order-priority": "Prioritat orden de campos", + "key-filter": "Filtres (clau)", + "key-filters": "Filtres (claus)", + "key-name": "Nom de clau", + "key-name-required": "Cal nom de clau.", + "key-type": { + "key-type": "Tipus de clau", + "attribute": "Atribut", + "timeseries": "Sèries temporals", + "entity-field": "Camp d'entitat", + "constant": "Constant" + }, + "value-type": { + "value-type": "Tipus de valor", + "string": "Cadena", + "numeric": "Numèric", + "boolean": "Booleà", + "date-time": "Data/Hora" + }, + "value-type-required": "Cal tipus de valor.", + "key-value-type-change-title": "Canviar el tipus de valor de la clau?", + "key-value-type-change-message": "Si tu confirmes el nou tipus, tots els filtres s'esborraran.", + "no-key-filters": "No hi ha filtres claus configurats", + "add-key-filter": "Afegir filtre clau", + "remove-key-filter": "Esborrar filtre clau", + "edit-key-filter": "Editar filtre clau", + "date": "Data", + "time": "Hora", + "current-tenant": "Admin actual", + "current-customer": "Client actual", + "current-user": "Usuari actual", + "current-device": "Dispositiu actual", + "default-value": "Valor per defecte", + "dynamic-source-type": "Tipus d'origen dinàmic", + "dynamic-value": "Valor dinàmic", + "no-dynamic-value": "Sense valor dinàmic", + "source-attribute": "Atribut d'origen", + "switch-to-dynamic-value": "Canviar a valor dinàmic", + "switch-to-default-value": "Canviar a valor per defecte", + "inherit-owner": "Heretar del propietari", + "source-attribute-not-set": "Si l'atribut d'origen no està definit" + }, + "fullscreen": { + "expand": "Expandir a Pantalla Completa", + "exit": "Sortir de Pantalla Completa", + "toggle": "Canviar el modo de Pantalla Completa", + "fullscreen": "Pantalla Completa" + }, + "function": { + "function": "Funció" + }, + "gateway": { + "add-entry": "Afegir configuració", + "connector-add": "Afegir conector", + "connector-enabled": "Activar conector", + "connector-name": "Nom conector", + "connector-name-required": "Cal nom conector.", + "connector-type": "Tipus conector", + "connector-type-required": "Cal tipus conector.", + "connectors": "Configuració de conectors", + "create-new-gateway": "Crear un gateway nou", + "create-new-gateway-text": "Crear un nou gateway amb el nom: '{{gatewayName}}'?", + "delete": "Esborrar configuració", + "download-tip": "Descarregar fitxer de configuració", + "gateway": "Gateway", + "gateway-exists": "Ja existeix un dispositiu amb el mateix nom.", + "gateway-name": "Nom de Gateway", + "gateway-name-required": "Cal un nom de gateway.", + "gateway-saved": "Configuració de gateway gravada satisfactòriament.", + "json-parse": "JSON no vàlid.", + "json-required": "El camp no pot ser buit.", + "no-connectors": "No hi ha conectors", + "no-data": "No hi ha configuracions", + "no-gateway-found": "No s'ha trobat cap gateway.", + "no-gateway-matching": " '{{item}}' no trobat.", + "path-logs": "Ruta als fitxers de log", + "path-logs-required": "Cal ruta.", + "remote": "Configuració remota", + "remote-logging-level": "Nivel de logging", + "remove-entry": "Esborrar configuració", + "save-tip": "Gravar fitxer de configuració", + "security-type": "Tipus de seguretat", + "security-types": { + "access-token": "Token d'accés", + "tls": "TLS" + }, + "storage": "Grabació", + "storage-max-file-records": "Número màxim de registres en fitxer", + "storage-max-files": "Número màxim de fitxers", + "storage-max-files-min": "El número mínim és 1.", + "storage-max-files-pattern": "Número no vàlid.", + "storage-max-files-required": "Cal número.", + "storage-max-records": "Màxim de registres en el magatzem", + "storage-max-records-min": "El número mínim és 1.", + "storage-max-records-pattern": "Número no vàlid.", + "storage-max-records-required": "Cal número.", + "storage-pack-size": "Mida màxim de esdeveniments", + "storage-pack-size-min": "El número mínim és 1.", + "storage-pack-size-pattern": "Número no vàlid.", + "storage-pack-size-required": "Cal número.", + "storage-path": "Ruta de magatzem", + "storage-path-required": "Cal ruta de magatzem.", + "storage-type": "Tipus de magatzem", + "storage-types": { + "file-storage": "Magatzem fitxer", + "memory-storage": "Magatzem en memoria" + }, + "thingsboard": "ThingsBoard", + "thingsboard-host": "Host ThingsBoard", + "thingsboard-host-required": "Cal Host.", + "thingsboard-port": "Port ThingsBoard", + "thingsboard-port-max": "El port màxim és 65535.", + "thingsboard-port-min": "El port mínim és 1.", + "thingsboard-port-pattern": "Port no vàlid.", + "thingsboard-port-required": "Cal port.", + "tidy": "Endreçat", + "tidy-tip": "Endreçat JSON", + "title-connectors-json": "Configuració conector {{typeName}}", + "tls-path-ca-certificate": "Ruta al certificat CA al gateway", + "tls-path-client-certificate": "Ruta al certificat client al gateway", + "tls-path-private-key": "Ruta a la clau privada al gateway", + "toggle-fullscreen": "Pantalla completa fullscreen", + "transformer-json-config": "Configuració JSON*", + "update-config": "Afegir/actualizar configuració JSON" + }, + "grid": { + "delete-item-title": "Vols eliminar aquest item?", + "delete-item-text": "Atenció, després de la confirmació l'item serà eliminat i la informació relacionada serà irrecuperable.", + "delete-items-title": "Vols eliminar { count, plural, 1 {1 item} other {# items} }?", + "delete-items-action-title": "Eliminar { count, plural, 1 {1 item} other {# items} }", + "delete-items-text": "Atenció, després de la confirmació els items seleccionats seran eliminats i la informació relacionada serà irrecuperable.", + "add-item-text": "Afegir nou item", + "no-items-text": "Cap item trobat", + "item-details": "Detalls del item", + "delete-item": "Esborrar Item", + "delete-items": "Esborrar Items", + "scroll-to-top": "Anar cap amunt" + }, + "help": { + "goto-help-page": "Anar a la pàgina d'ajuda", + "show-help": "Mostra ajuda" + }, + "home": { + "home": "Principal", + "profile": "Perfil", + "logout": "Sortir", + "menu": "Menu", + "avatar": "Avatar", + "open-user-menu": "Obrir menú d'usuari" + }, + "file-input": { + "browse-file": "Navegar fitxer", + "browse-files": "Navegar fitxers" + }, + "image-input": { + "drop-image-or": "Arrossegar i deixar anar imatges o", + "drop-images-or": "Arrossegar i deixar anar imatges o", + "no-images": "No hi ha imatges seleccionades", + "images": "imatges" + }, + "import": { + "no-file": "Cap arxiu seleccionat", + "drop-file": "Deixeu anar un arxiu JSON o feu clic per seleccionar un arxiu per carregar.", + "drop-json-file-or": "Sol un fitxer JSON o", + "drop-csv-file": "Deixeu anar un fitxer CSV o feu clic per seleccionar un fitxer per carregar.", + "drop-file-csv": "Suelte un arxiu CSV o haga clic per seleccionar un arxiu per carregar.", + "column-value": "Valor", + "column-title": "Títol", + "column-example": "Dades Sortird'exemple", + "column-key": "Clau d'atribut/telemetria", + "credentials": "Credencials", + "csv-delimiter": "Delimitador CSV", + "csv-first-line-header": "La primera línia conté noms de columna.", + "csv-update-data": "Actualizar atributs/telemetria", + "details": "Detalls", + "import-csv-number-columns-error": "Un arxiu ha de contener almenys dos columnes", + "import-csv-invalid-format-error": "Formato de arxiu invàlid. Línea: '{{line}}'", + "column-type": { + "name": "Nom", + "type": "Tipus", + "label": "Etiqueta", + "column-type": "Tipus de columna", + "client-attribute": "Atribut de client", + "shared-attribute": "Atribut compartit", + "server-attribute": "Atribut de servidor", + "timeseries": "Series del temps", + "entity-field": "Camp d'entitat", + "access-token": "Token d'accés", + "x509": "X.509", + "mqtt": { + "client-id": "ID de client MQTT", + "user-name": "Nom d'usuari MQTT", + "password": "Contrasenya MQTT" + }, + "lwm2m": { + "client-endpoint": "Nom del client del punt final LwM2M", + "security-config-mode": "Mode de configuració de seguretat LwM2M", + "client-identity": "Identitat del client LwM2M", + "client-key": "Clau de client LwM2M", + "client-cert": "Clau pública del client LwM2M", + "bootstrap-server-security-mode": "Mode de seguretat del servidor d'arrencada LwM2M", + "bootstrap-server-secret-key": "Clau secreta del servidor d'arrencada LwM2M", + "bootstrap-server-public-key-id": "Clau pública o identificador del servidor d'arrencada LwM2M", + "lwm2m-server-security-mode": "Mode de seguretat del servidor LwM2M", + "lwm2m-server-secret-key": "Clau secreta del servidor LwM2M", + "lwm2m-server-public-key-id": "ID o clau pública del servidor LwM2M" + }, + "isgateway": "Es Gateway", + "activity-time-from-gateway-device": "Data d'activitat des del dispositiu gateway", + "description": "Descripció", + "edge-license-key": "Clau de llicència", + "cloud-endpoint": "Punt final del núvol", + "routing-key": "Clau Edge", + "secret": "Edge secret" + }, + "stepper-text": { + "select-file": "Selecciona un arxiu", + "configuration": "Importar configuració", + "column-type": "Seleccionar tipus de columnes", + "creat-entities": "Creant noves entitats", + "done": "Fet" + }, + "message": { + "create-entities": "Es van crear{{count}} noves entitats correctament.", + "update-entities": "{{count}} entitats s'han actualitzat correctament.", + "error-entities": "S'ha produit un error en crear {{count}} entitats." + } + }, + "integration": { + "integration": "Integració", + "integrations": "Integracions", + "select-integration": "Seleccionar integració", + "no-integrations-matching": "Integracions que coincideixin amb '{{entity}}' no han estat trobades.", + "integration-required": "Cal integració", + "delete": "Eliminar integració", + "management": "Gestió d'integracions", + "add-integration-text": "Afegir nova integració", + "no-integrations-text": "Integracions no trobades", + "selected-integrations": "{ count, plural, 1 {1 integration} other {# integrations} } seleccionades", + "delete-integration-title": "Està segur de que desitja eliminar la integració '{{integrationName}}'?", + "delete-integration-text": "Aneu amb compte, després de la confirmació, la integració i totes les dades relacionades seran irrecuperables.", + "delete-integrations-title": "Està segur de que desitja eliminar { count, plural, 1 {1 integration} other {# integrations} }?", + "delete-integrations-action-title": "Eliminar { count, plural, 1 {1 integration} other {# integrations} }", + "delete-integrations-text": "Aneu amb compte, després de la confirmació s'eliminaran totes les integracions seleccionades i totes les dades relacionades seran irrecuperables.", + "events": "Esdeveniments", + "enabled": "Habilitat", + "allow-create-devices-or-assets": "Permet crear dispositius o actius", + "add": "Afegir Integració", + "search": "Cerca integracions", + "integration-details": "Detalls d'Integració", + "details": "Detalls", + "copyId": "Copiar ID d'integració", + "idCopiedMessage": "ID d'integració ha estat copiada al porta-retalls", + "debug-mode": "Manera de depuració", + "enable-security": "Habilitar seguretat", + "enable-security-new": "Habiliteu la seguretat per les actualitzacions automàtiques de tokens", + "headers-filter": "Filtre d'encapçalats", + "header": "Encapçalats", + "no-headers-filter": "Cap filtre d'encapçalats", + "downlink-url": "URL d'enllaç descendent", + "downlink-url-required": "Cal URL d'enllaç descendent", + "create-loriot-output": "Creeu la sortida de l'aplicació Loriot", + "send-downlink": "Envia l'enllaç descendent", + "server": "Servidor", + "server-required": "Cal servidor", + "domain": "Domini", + "app-id": "ID de l'aplicació", + "app-id-required": "Cal ID de l'aplicació", + "app-token": "Token d'accés a l'aplicació", + "app-token-required": "Cal token d'accés a l'aplicació", + "email": "Correu electrònic", + "email-required": "Cal correu electrònic", + "application-uri": "Aplicació URI", + "as-id": "ID AS", + "as-id-required": "Cal ID AS.", + "as-key": "Clau AS", + "as-key-required": "Cal clau AS.", + "client-id-new": "Identificació del client", + "client-id-new-required": "Cal ID de client (inici de sessió)", + "client-secret": "Contrasenya del client", + "client-secret-required": "Cal el secret del client (contrasenya).", + "max-time-diff-in-seconds": "Màxima diferencia del temps (segons)", + "max-time-diff-in-seconds-required": "Diferencia màxima del temps és requerida.", + "created-time": "Temps creat", + "name": "Nom", + "name-required": "Cal el nom.", + "name-max-length": "El nom ha de ser inferior a 256", + "description": "Descripció", + "base-url": "URL base", + "base-url-required": "Cal URL base", + "security-key": "Clau de seguretat", + "http-endpoint": "URL del punt final HTTP", + "replace-no-content-to-ok": "Substitueix l'estat de resposta de 'Sense contingut' per 'D'acord'", + "copy-http-endpoint-url": "Copiar URL del punt final HTTP", + "http-endpoint-url-copied-message": "L'URL del punt final HTTP ha estat copiada al porta-retalls", + "host": "Host", + "host-required": "Cal Host.", + "host-type": "Tipus de Host", + "host-type-required": "Cal tipus de Host.", + "api-version": "Utilitzeu l'API v3", + "custom-host": "Host personalitzat", + "custom-host-required": "Cal Host personalitzat.", + "port": "Port", + "port-required": "Cal Port.", + "port-range": "El port ha d'estar en un rang entre 1 i 65535.", + "connect-timeout": "Temps d'espera de connexió (seg)", + "connect-timeout-required": "Cal temps d'espera de connexió.", + "connect-timeout-range": "Temps d'espera de connexió ha d'estar en un rang entre 1 i 200.", + "client-id": "ID del client", + "client-id-required": "Cal Client ID.", + "client-id-range": "L'identificador de client ha de tenir entre 1 i 23 caràcters. [MQTT-3.1.3-5]", + "client-id-pattern": "L'identificador de client ha de constar de números, lletres majúscules i minúscules. [MQTT-3.1.3-5]", + "client-id-hint": "Pista: opcional. Deixeu en blanc l'identificador de client generat automàticament. Aneu amb compte quan especifiqueu l'identificador de client. La majoria dels corredors de MQTT no permetran connexions múltiples amb el mateix identificador de client. Per connectar-se a aquests corredors, el vostre identificador de client mqtt ha de ser únic.", + "max-bytes-in-message": "Màxim bytes al missatge", + "max-bytes-in-message-range": "El màxim de bytes del missatge hauria d'estar en un interval d'1 a 256000000.", + "device-id": "ID del dispositiu", + "device-id-required": "Cal ID del dispositiu.", + "device-id-range": "L'identificador del dispositiu ha d'estar en un interval d'entre 1 i 65535 caràcters.", + "device-id-pattern": "L'identificador del dispositiu ha de constar de números, lletres majúscules i minúscules. [MQTT-3.1.3-5]", + "group-id": "ID del grup", + "group-id-required": "Cal ID del grup.", + "topics": "Temes", + "topics-required": "Cal temes.", + "routing-keys": "Claus d'encaminament", + "routing-keys-required": "Cal claus d'encaminament.", + "queues": "Cues", + "queues-required": "Cal el nom de la cua.", + "durable": "Durable", + "exclusive": "Exclusiu", + "autoDelete": "Supressió automàtica", + "exchange-name": "Nom d'intercanvi", + "exchange-name-required": "Cal nom d'intercanvi.", + "downlink-topic": "Tema d'enllaç descendent", + "connection-timeout": "Temps d'espera de connexió, ms", + "connection-timeout-min": "Valor de temps d'espera de connexió no vàlid.", + "handshake-timeout": "Temps d'espera de l'encaixada, ms", + "handshake-timeout-min": "El valor del temps d'espera de la connexió de mans no és vàlid.", + "virtual-host": "Amfitrió virtual", + "rabbit-mq-poll-interval": "Interval d'enquesta, ms", + "rabbit-mq-poll-interval-min": "El valor de l'interval d'enquesta no és vàlid.", + "application-server-url": "URL del servidor d'aplicacions", + "application-server-url-required": "Cal URL del servidor d'aplicacions.", + "application-server-api-token": "Token de l'API del servidor d'aplicacions", + "application-server-api-token-required": "Cal Token de l'API del servidor d'aplicacions.", + "bootstrap-servers": "Servidors d'arrencada", + "bootstrap-servers-required": "Cal servidors d'arrencada.", + "poll-interval": "Interval d'enquesta", + "poll-interval-required": "Cal interval d'enquesta.", + "auto-create-topics": "Creació automàtica de temes", + "clean-session": "Netejar sessió", + "enable-ssl": "Habilitar SSL", + "credentials": "Credencials", + "credentials-type": "Tipus de credencials", + "credentials-type-required": "Cal el tipus de credencials.", + "username": "Nom d'usuari", + "username-required": "Cal el nom d'usuari.", + "password": "Contrasenya", + "password-required": "Cal contrasenya és requerid.", + "azure-ca-cert": "Fitxer de certificat CA", + "ca-cert": "Arxiu de certificat CA *", + "private-key": "Arxiu de clau privat *", + "private-key-password": "Clau de contrasenya privada", + "cert": "Arxiu de certificat *", + "no-file": "Cap arxiu seleccionat.", + "drop-file": "Colocar un arxiu o fer clic per seleccionar un arxiu per carregar .", + "check-connection": "Comproveu la connexió", + "check-success": "Connexió establerta.", + "topic-filters": "Filtres de tema", + "remove-topic-filter": "Eliminar filtres de tema", + "add-topic-filter": "Afegir filtres de tema", + "add-topic-filter-prompt": "Per favor afegir filtres de tema", + "topic": "Tema", + "mqtt-qos": "QoS", + "mqtt-qos-at-most-once": "Com a màxim una vegada", + "mqtt-qos-at-least-once": "Almenys una vegada", + "mqtt-qos-exactly-once": "Exactamente una vegada", + "downlink-topic-pattern": "Patró de tema del enllaç descendent", + "downlink-topic-pattern-required": "Cal patró de tema del enllaç descendent.", + "aws-access-key-id": "Id. de la clau d'accés AWS", + "aws-secret-access-key": "Clau d'accés secreta d'AWS", + "aws-region": "Regió", + "aws-iot-endpoint": "Extrem AWS IoT", + "aws-iot-endpoint-required": "Cal extrem AWS IoT.", + "aws-iot-credentials": "Credencials AWS IoT", + "aws-sqs-polling-period-in-seconds": "Període de votació en segons", + "aws-sqs-queue-url": "URL de la cua SQS", + "aws-sqs-queue-url-required": "Cal URL de la cua SQS", + "aws-sqs-access-key-id-required": "Cal Id. de la clau d'accés", + "aws-sqs-secret-access-key-required": "Cal clau d'accés secreta", + "application-credentials": "Credencials d'aplicació", + "api-key": "Clau API", + "api-key-required": "Cal clau API.", + "api-key-format": "Format de clau d'API no vàlid.", + "auth-token": "Token d'Autenticació", + "auth-token-required": "Cal Token d'Autenticació.", + "region": "Regió", + "region-required": "Cal regió.", + "application-id": "ID d'aplicació", + "application-id-required": "Cal ID de Aplicació.", + "access-key": "Clau de Accés", + "access-key-required": "Cal clau de Accés.", + "connection-parameters": "Paràmetres de connexió", + "service-bus-namespace-name": "Espai de Noms del Bus de Serveis", + "service-bus-namespace-name-required": "Cal nom de l'espai de Noms del Bus de Serveis.", + "connection-string": "Cadena de connexió", + "consumer-group": "Grup de consumidors", + "connection-string-required": "Cal cadena de connexió!", + "event-hub-name": "Nom del Concentrador de Esdeveniments", + "event-hub-name-required": "Cal nom del Concentrador de Esdeveniments.", + "event-iot-hub-name-required": "El nom del concentrador Iot és necessari per a l'enllaç descendent", + "sas-key-name": "Nom de clau SAS", + "sas-key-name-required": "Cal nom de Clau SAS.", + "sas-key": "Clau SAS", + "sas-key-required": "Cal clau SAS.", + "iot-hub-name": "Nom del Concentrador IoT (Cal per enllaç descendent)", + "hostname": "Hostname", + "hostname-required": "Cal hostname", + "integration-clazz": "Classe d'integració", + "integration-clazz-required": "Classe d'integració", + "integration-configuration": "Configuració JSON d'integració", + "metadata": "Metadades", + "type": "Tipus", + "type-required": "Cal el tipus.", + "uplink-converter": "Convertidor de dades del enllaç ascendent", + "uplink-converter-required": "Cal convertidor de dades del enllaç ascendent.", + "downlink-converter": "Convertidor de dades del enllaç descendent", + "type-http": "HTTP", + "type-ocean-connect": "OceanConnect", + "type-sigfox": "SigFox", + "type-thingpark": "ThingPark", + "type-loriot": "Loriot", + "type-thingpark-enterprise": "ThingParkEnterprise", + "type-tmobile-iot-cdp": "iotcreators.com (T-Mobile – IoT CDP)", + "type-mqtt": "MQTT", + "type-aws-iot": "AWS IoT", + "type-aws-sqs": "AWS SQS", + "type-aws-kinesis": "AWS Kinesis", + "type-ibm-watson-iot": "IBM Watson IoT", + "type-ttn": "The Things Stack Community", + "type-tti": "The Things Stack Industries", + "type-chirpstack": "ChirpStack", + "type-azure-event-hub": "Azure Event Hub", + "type-azure-iot-hub": "Azure IoT Hub", + "type-opc-ua": "OPC-UA", + "type-custom": "Custom", + "type-coap": "CoAP", + "type-udp": "UDP", + "type-tcp": "TCP", + "type-kafka": "Kafka", + "type-rabbitmq": "RabbitMQ", + "type-pubsub": "Pub/Sub", + "opc-ua-application-name": "Nom d'aplicació", + "opc-ua-application-uri": "Aplicació URI", + "opc-ua-scan-period-in-seconds": "Període d'exploració en segons", + "opc-ua-scan-period-in-seconds-required": "Cal període d'exploració", + "opc-ua-timeout": "Temps d'espera en mil·lisegons", + "opc-ua-timeout-required": "Cal temps d'espera", + "opc-ua-security": "Seguretat", + "opc-ua-security-required": "Cal seguretat", + "opc-ua-identity": "Identitat", + "opc-ua-identity-required": "Cal identitat", + "opc-ua-keystore": "Repositori", + "add-opc-ua-keystore-prompt": "Per favor afegir arxiu del repositori", + "opc-ua-keystore-required": "Cal repositori", + "opc-ua-type": "Tipus", + "opc-ua-keystore-type": "Tipus", + "opc-ua-keystore-type-required": "Cal tipus", + "opc-ua-keystore-location": "Ubicació *", + "opc-ua-keystore-password": "Contrasenya", + "opc-ua-keystore-password-required": "Cal contrasenya", + "opc-ua-keystore-alias": "Àlies", + "opc-ua-keystore-alias-required": "Cal Àlies", + "opc-ua-keystore-key-password": "Clau de contrasenya", + "opc-ua-keystore-key-password-required": "Cal clau de contrasenya", + "opc-ua-mapping": "Mapeig", + "add-opc-ua-mapping-prompt": "Per favor afegir mapatge", + "opc-ua-mapping-type": "Tipus de mapatge", + "opc-ua-mapping-type-required": "Cal Tipus de mapatge", + "opc-ua-device-node-pattern": "Patró de Node del Dispositiu", + "opc-ua-device-node-pattern-required": "Cal Patró de Node del Dispositiu", + "opc-ua-namespace": "Namespace", + "opc-ua-add-map": "Afegir element de mapatge", + "kinesis-stream-name": "Nom del flux", + "kinesis-stream-name-required": "Cal nom del flux", + "kinesis-region": "Regió", + "kinesis-region-required": "Cal Regió", + "kinesis-access-key-id": "Id. de la clau d'accés", + "kinesis-access-key-id-required": "Cal Id. de la clau d'accés", + "kinesis-secret-access-key": "Clau d'accés secreta", + "kinesis-secret-access-key-required": "Cal clau d'accés secreta", + "kinesis-use-consumers-with-enhanced-fan-out": "Utilitzeu consumidors amb Fan-Out millorat", + "kinesis-use-credentials-from-instance-metadata": "Utilitzeu les credencials de l'Amazon EC2 Instance Metadata Service", + "kinesis-application-name": "Nom de l'aplicació (by default equals Stream name)", + "kinesis-initial-position-in-stream": "Posició inicial al corrent", + "kinesis-initial-position-in-stream-required": "Cal posició inicial al corrent", + "kinesis-max-records": "Màxim registres", + "kinesis-max-records-required": "Cal màxim registres", + "kinesis-max-records-length-range": "La longitud màxima dels registres hauria d'estar entre 1 i 10.000", + "kinesis-request-timeout": "Sol·licita el temps d'espera en segons", + "kinesis-request-timeout-required": "Cal el temps d'espera de la sol·licitud", + "other-properties": "Altres propietats", + "subscription-tags": "Etiquetes de subscripció", + "remove-subscription-tag": "Eliminar etiqueta de subscripció", + "add-subscription-tag": "Afegir etiqueta de suscripció", + "add-subscription-tag-prompt": "Per favor afegir etiqueta de suscripció", + "key": "Clau", + "path": "Trajecte", + "required": "Requerit", + "integration-key": "Clau d'integració", + "copy-integration-key": "Copiar clau d'integració", + "integration-key-copied-message": "La clau d'integració s'ha copiat al porta-retalls", + "integration-secret": "Secret d'integració", + "copy-integration-secret": "Copiar Secret d'integració", + "integration-secret-copied-message": "El secret d'integració s'ha copiat al porta-retalls", + "execute-remotely": "Executar a distància", + "handler-configuration": "Configuració del gestor", + "handler-configuration-type": "Tipus de comerciant", + "so-broadcast": "Habilita la difusió: la integració acceptarà paquets d'adreces de difusió", + "so-keepalive-option": "Habiliteu l'enviament de missatges de manteniment en els sòcols orientats a la connexió", + "so-reuse-addr": "Enllaçar el procés a un port", + "tcp-no-delay": "Força un sòcol a enviar les dades sense emmagatzemar la memòria intermèdia (desactiva l'algoritme de memòria intermèdia de Nagle)", + "fail-fast": "L'excepció llançada tan bon punt el descodificador noti que la longitud del fotograma superarà la mida màxima", + "strip-delimiter": "Delimitador de franges", + "length-field-offset": "Desplaçament de camp de longitud", + "length-field-offset-required": "Cal desplaçament de camp de longitud.", + "length-field-offset-range": "El desplaçament del camp de longitud ha d'estar en un interval de 0 a 8.", + "length-field-length": "Longitud Longitud del camp", + "length-field-length-required": "La longitud del camp és necessària.", + "length-field-length-range": "La longitud del camp ha d'estar en un interval de 0 a 8.", + "length-adjustment": "Ajust de longitud (el valor de compensació que cal afegir al valor del camp de longitud)", + "length-adjustment-required": "Cal un ajust de longitud.", + "length-adjustment-range": "L'ajust de longitud ha d'estar en un rang de 0 a 8.", + "byte-order": "Ordre de bytes del camp de longitud", + "initial-bytes-to-strip": "Nombre de primers bytes per eliminar del marc descodificat", + "initial-bytes-to-strip-required": "Cal el nombre de primers bytes per treure de la trama descodificada.", + "initial-bytes-to-strip-range": "El nombre de primers bytes per eliminar del marc descodificat hauria d'estar en un interval de 0 a 8.", + "so-backlog-option": "Nombre màxim de connexions pendents a la presa", + "so-backlog-option-required": "Cal el nombre màxim de connexions pendents al sòcol.", + "so-backlog-option-range": "El nombre màxim de connexions pendents a l'endoll hauria d'estar entre 1 i 65535.", + "so-rcv-buf": "Mida de la memòria intermèdia per al sòcol d'entrada (en KB)", + "so-rcv-buf-required": "La mida de la memòria intermèdia per al sòcol d'entrada (en KB) és necessària.", + "so-rcv-buf-range": "La mida de la memòria intermèdia per al sòcol d'entrada (en KB) hauria d'estar en un interval d'1 a 65535.", + "so-snd-buf": "Mida de la memòria intermèdia per al sòcol de sortida (en KB)", + "so-snd-buf-required": "Cal la mida de la memòria intermèdia per al sòcol de sortida (en KB).", + "so-snd-buf-range": "La mida de la memòria intermèdia per al sòcol de sortida (en KB) hauria d'estar en un interval d'1 a 65535.", + "charset-name": "Nom del conjunt de caràcters", + "charset-name-required": "Cal nom del conjunt de caràcters.", + "message-separator": "Separador de missatges", + "message-separator-required": "Cal separador de missatges.", + "character-sequence": "Seqüència de caràcters", + "character-sequence-required": "Cal seqüència de caràcters.", + "max-frame-length": "Longitud màxima del fotograma (en bytes)", + "max-frame-length-required": "Cal longitud màxima del fotograma (en bytes).", + "max-frame-length-range": "La longitud màxima del fotograma (en bytes) hauria d'estar en un interval d'1 a 65535.", + "handler-type": "Tipus de comerciant", + "message-size": "Mida del missatge", + "message-size-required": "La mida del missatge és necessària.", + "type-apache-pulsar": "Apache Pulsar", + "service-url": "URL del servei", + "service-url-required": "Cal URL del servei.", + "subscription-name": "Nom de la subscripció", + "subscription-name-required": "Cal nom de la subscripció.", + "max-num-messages": "Nombre màxim de missatges", + "max-num-messages-required": "Cal nombre màxim de missatges.", + "max-num-bytes": "Nombre màxim de bytes", + "max-num-bytes-required": "Cal nombre màxim de bytes.", + "timeout-in-ms": "Temps d'espera en mil·lisegons", + "timeout-in-ms-required": "Temps d'espera en mil·lisegons.", + "user-id": "ID d'usuari", + "user-id-required": "Cal ID d'usuari.", + "token": "Token", + "token-required": "Cal Token.", + "project-id": "ID del projecte", + "project-id-required": "Cal ID del projecte.", + "subscription-id": "Identificador de subscripció", + "subscription-id-required": "Cal identificador de subscripció.", + "service-account-key": "Fitxer de claus del compte de servei", + "service-account-key-required": "Cal fitxer de claus del compte de servei.", + "tcp": { + "system-line-separator": "Separador de línies del sistema", + "nul-delimiter": "Delimitador nul", + "byte-order-little-endian": "Petit Endian", + "byte-order-big-endian": "Big Endian" + }, + "cache-size": "Mida de la memòria cau", + "cache-time-to-live": "Temps de la memòria cau per viure en minuts", + "min-cache-size": "La mida de la memòria cau no pot ser inferior a 0", + "min-cache-time-to-live": "El temps de vida de la memòria cau no pot ser inferior a 0", + "max-cache-time-to-live": "El temps de la memòria cau en directe no és vàlid, seleccioneu entre 0 i 525600", + "coap-security-mode": "Mode de seguretat", + "coap-security-mode-required": "Cal el mode de seguretat CoAP", + "coap-security-mode-no-secure": "NO SEGUR", + "coap-security-mode-dtls": "DTLS", + "coap-security-mode-mixed": "MIXTES", + "coap-endpoint": "URL del punt final CoAP", + "coap-endpoint-url-copied-message": "L'URL del punt final CoAP s'ha copiat al porta-retalls", + "copy-coap-endpoint-url": "Copia l'URL del punt final CoAP", + "copy-coap-dtls-endpoint-url": "Copia l'URL del punt final de CoAP DTLS", + "coap-dtls-base-url": "URL base DTLS", + "coap-dtls-base-url-required": "Cal URL base DTLS", + "coap-dtls-endpoint": "URL del punt final de CoAP DTLS", + "coap-dtls-endpoint-url-copied-message": "L'URL del punt final de CoAP DTLS s'ha copiat al porta-retalls", + "type-ffb": "FFB" + }, + "item": { + "selected": "Seleccionat" + }, + "js-func": { + "no-return-error": "La funció ha de retornar un valor!", + "return-type-mismatch": "La funció ha de retornar un valor de tipus: '{{type}}'!", + "tidy": "Endreçat", + "mini": "Mini" + }, + "key-val": { + "key": "Clau", + "value": "Valor", + "remove-entry": "Esborrar entrada", + "add-entry": "Afegir entrada", + "no-data": "Sense dades" + }, + "layout": { + "layout": "Disseny", + "manage": "Administrar Dissenys", + "settings": "Configuració de disseny", + "color": "Color", + "main": "Principal", + "right": "Dret", + "select": "Seleccionar Disseny de destí" + }, + "legend": { + "direction": "Direcció de la llegenda", + "position": "Posició de la llegenda", + "sort-legend": "Ordenar claus en llegenda", + "show-max": "Mostrar valor màxim", + "show-min": "Mostrar valor mínim", + "show-avg": "Mostrar valor mitjà", + "show-total": "Mostrar valor total", + "settings": "Configuració de la llegenda", + "min": "mínim", + "max": "màxim", + "avg": "mitjana", + "total": "total", + "comparison-time-ago": { + "previousInterval": "(interval anterior)", + "customInterval": "(interval personalitzat)", + "days": "(Fa un dia)", + "weeks": "(Fa una setmana)", + "months": "(Fa un mes)", + "years": "(Fa un any)" + } + }, + "login": { + "login": "Entrar", + "request-password-reset": "Sol·licitar restablir contrasenya", + "reset-password": "Restablir contrasenya", + "create-password": "Crear contrasenya", + "passwords-mismatch-error": "¡Les contraseñas introduïdes han de ser iguals!", + "password-again": "Repetiu la contrasenya de nou", + "sign-in": "Per favor, inicieu sessió", + "username": "Nom d'usuari (correu electrònic)", + "remember-me": "Recordar-me", + "forgot-password": "Ha oblidat la contrasenya?", + "password-reset": "Restablir contrasenya", + "expired-password-reset-message": "Les teves credencials han expirat! Si us plau, crea una nova contrasenya.", + "new-password": "Nova contrasenya", + "new-password-again": "Repetiu la nova contrasenya", + "password-link-sent-message": "S'ha enviat el enllaç de restabliment de contrasenya amb èxit!", + "email": "Correu electrònic", + "no-account": "No té un compte?", + "create-account": "Crear un compte", + "login-with": "Iniciar sessió amb {{name}}", + "or": "o", + "error": "Error d'inici de sessió", + "verify-your-identity": "Verificar identitat", + "select-way-to-verify": "Seleccioneu el mode de verificació", + "resend-code": "Reenviar codi", + "resend-code-wait": "Reenviar codi a { time, plural, 1 {1 segundo} other {# segundos} }", + "try-another-way": "Altres modes de verificació", + "totp-auth-description": "Si us plau, introduïu el codi de seguretat de la vostra aplicació d'autenticació.", + "totp-auth-placeholder": "Codi", + "sms-auth-description": "S'ha enviat un codi de verificació al vostre número: {{contact}} proporcionat.", + "sms-auth-placeholder": "Codi SMS", + "email-auth-description": "S'ha enviat un codi de verificació del correu electrònic {{contact}} proporcionat.", + "email-auth-placeholder": "Codi correu electrònic", + "backup-code-auth-description": "Por favor, introduce el código de backup.", + "backup-code-auth-placeholder": "Codi de Backup" + }, + "signup": { + "firstname": "Nom", + "lastname": "Cognom", + "email": "Correu electrònic", + "signup": "Registra't", + "create-password": "Crea una contrassenya", + "repeat-password": "Repetiu la vostra contrasenya", + "have-account": "Ja tens un compte?", + "signin": "Inicia sessió", + "no-captcha-message": "Heu de confirmar que no sou un robot", + "password-length-message": "La vostra contrasenya ha de tenir almenys 6 caràcters", + "email-verification": "Correu electrònic verificació", + "email-verification-message": "S'ha enviat un correu electrònic amb les dades de verificació a l'adreça electrònica especificada.
                Seguiu les instruccions proporcionades al correu electrònic per completar el procediment d'inscripció.
                Nota: si fa temps que no consulteu el correu electrònic, si us plau comproveu la vostra carpeta 'spam' o proveu de tornar a enviar el correu electrònic fent clic al botó 'Torna a enviar'.", + "account-activation-title": "Activació del compte", + "account-activated": "El compte s'ha activat correctament!", + "account-activated-text": "Enhorabona!
                El vostre compte s'ha activat.", + "resend": "Reenviar", + "inactive-user-exists-title": "L'usuari inactiu ja existeix", + "inactive-user-exists-text": "Ja hi ha un usuari registrat amb una adreça electrònica no verificada.
                Feu clic al botó 'Torna a enviar' si voleu tornar a enviar el correu electrònic de verificació..", + "activating-account": "S'està activant el compte...", + "activating-account-text": "El teu compte s'està activant. Si us plau, espereu...", + "accept-privacy-policy": "Acceptar la política de privadesa", + "accept": "Acceptar", + "privacy-policy": "Política de privacitat", + "accept-privacy-policy-message": "Heu d'acceptar la nostra Política de privadesa", + "recaptcha-title": "reCAPTCHA", + "terms-of-use": "Condicions d'ús", + "accept-terms-of-use-message": "Heu d'acceptar les nostres Condicions d'ús" + }, + "ota-update": { + "add": "Afegeix el paquet", + "assign-firmware": "Firmware assignat", + "assign-firmware-required": "Cal firmware assignat", + "assign-software": "Programari assignat", + "assign-software-required": "Cal programari assignat", + "auto-generate-checksum": "Suma de comprovació generada automàticament", + "cant-applied-group-all": "No es pot sol·licitar per al grup Tots", + "checksum": "Suma de control", + "checksum-hint": "Si la suma de comprovació està buida, es generarà automàticament", + "checksum-algorithm": "Algorisme de suma de control", + "checksum-copied-message": "La suma de comprovació del paquet s'ha copiat al porta-retalls", + "change-firmware": "El canvi del firmware pot provocar l'actualització { count, plural, 1 {1 device} other {# devices} }.", + "change-software": "El canvi del programari pot provocar l'actualització { count, plural, 1 {1 device} other {# devices} }.", + "chose-compatible-device-profile": "El paquet penjat només estarà disponible per als dispositius amb el perfil escollit.", + "chose-firmware-distributed-device": "Trieu el firmware que es distribuirà als dispositius", + "chose-software-distributed-device": "Trieu el programari que es distribuirà als dispositius", + "content-type": "Tipus de contingut", + "copy-checksum": "Copia la suma de verificació", + "copy-direct-url": "Copia l'URL directe", + "copyId": "Copia l'identificador del paquet", + "copied": "Copiat!", + "delete": "Esborra el paquet", + "delete-ota-update-text": "Aneu amb compte, després de la confirmació, l'actualització OTA serà irrecuperable.", + "delete-ota-update-title": "Confirmes que vols suprimir l'actualització de l'OTA '{{title}}'?", + "delete-ota-updates-text": "Aneu amb compte, després de la confirmació, totes les actualitzacions OTA seleccionades s'eliminaran.", + "delete-ota-updates-title": "Esteu segur que voleu suprimir { count, plural, 1 {1 OTA update} other {# OTA updates} }?", + "description": "Descripció", + "direct-url": "URL directe", + "direct-url-copied-message": "L'URL directe del paquet s'ha copiat al porta-retalls", + "direct-url-required": "Cal un URL directe", + "download": "Descarrega el paquet", + "drop-file": "Deixeu anar un fitxer de paquet o feu clic per seleccionar un fitxer per carregar.", + "file-name": "Nom de l'arxiu", + "file-size": "Mida de l'arxiu", + "file-size-bytes": "Mida del fitxer en bytes", + "idCopiedMessage": "L'identificador del paquet s'ha copiat al porta-retalls", + "no-firmware-matching": "No coincideix cap paquet d'actualització de firmware OTA compatible '{{entity}}' els van trobar.", + "no-firmware-text": "No s'ha subministrat cap paquet d'actualització de firmware OTA compatible.", + "no-packages-text": "No s'han trobat paquets", + "no-software-matching": "No coincideix cap paquet d'actualització de programari OTA compatible '{{entity}}' els van trobar.", + "no-software-text": "No s'ha subministrat cap paquet d'actualització de programari OTA compatible.", + "ota-update": "Actualització OTA", + "ota-update-details": "Detalls actualització OTA", + "ota-updates": "Actualitzacions de l'OTA", + "package-type": "Tipus de paquet", + "packages-repository": "Repositori de paquets", + "search": "Cerca paquets", + "selected-package": "{ count, plural, 1 {1 package} other {# packages} } seleccionat", + "title": "Títol", + "title-required": "Cal títol.", + "title-max-length": "El títol ha de ser inferior a 256", + "types": { + "firmware": "Firmware", + "software": "Programari" + }, + "upload-binary-file": "Carregueu un fitxer binari", + "use-external-url": "Utilitzeu un URL extern", + "version": "Versió", + "version-required": "Cal versió.", + "version-tag": "Etiqueta de versió", + "version-tag-hint": "L'etiqueta personalitzada hauria de coincidir amb la versió del paquet informada pel vostre dispositiu.", + "version-max-length": "La versió hauria de ser inferior a 256", + "warning-after-save-no-edit": "Un cop carregat el paquet, no podreu modificar el títol, la versió, el perfil del dispositiu i el tipus de paquet." + }, + "position": { + "top": "Superior", + "bottom": "Inferior", + "left": "Esquerra", + "right": "Dreta" + }, + "profile": { + "profile": "Perfil", + "last-login-time": "Darrer accés", + "change-password": "Canviar contrasenya", + "current-password": "Contrasenya actual", + "copy-jwt-token": "Copia el testimoni JWT", + "jwt-token": "Tóken JWT", + "valid-till": "Vàlid per a {{expirationData}}", + "tokenCopiedSuccessMessage": "El testimoni JWT s'ha copiat al porta-retalls", + "tokenCopiedWarnMessage": "El testimoni JWT ha caducat! Si us plau, actualitzeu la pàgina." + }, + "security": { + "security": "Seguretat", + "2fa": { + "2fa": "Autenticació de doble factor (2FA)", + "2fa-description": "L'autenticació de dos factors protegeix el vostre compte de l'accés no autoritzat. Tot el que heu de fer és introduir un codi de seguretat en iniciar sessió.", + "authenticate-with": "Pots autenticarte amb:", + "disable-2fa-provider-text": "Desactivar {{name}} farà el teu compte menys segur", + "disable-2fa-provider-title": "Estàs segur de desactivar {{name}}?", + "get-new-code": "Obtenir un nou codi", + "main-2fa-method": "Usar com a mètode d'autenticació de doble factor principal", + "dialog": { + "activation-step-description-email": "La propera vegada que facis log-in, caldrà introduir el codi de seguretat que s'enviarà a la teva adreça de correu electrònic.", + "activation-step-description-sms": "La propera vegada que feu log-in, caldrà introduir el codi de seguretat que s'enviarà al vostre número de telèfon.", + "activation-step-description-totp": "La propera vegada que facis log-in, caldrà introduir el codi de seguretat de doble factor.", + "activation-step-label": "Activació", + "backup-code-description": "Imprimeix els codis i guarda'ls en un lloc segur, es necessitaran per fer login al teu compte. Pots utilitzar cada codi de còpia de seguretat només una vegada.", + "backup-code-warn": "Quan surtis d'aquesta pàgina, aquests codis no es tornaran a mostrar. Desa'ls en un lloc segur usant les opcions a continuació.", + "download-txt": "Descarregar (txt)", + "email-step-description": "Introdueix el teu correu electrònic per utilitzar com a autenticador.", + "email-step-label": "Correu electrònic", + "enable-email-title": "Activar autenticador per correu electrònic", + "enable-sms-title": "Activar autenticador per SMS", + "enable-totp-title": "Activar autenticador per app", + "enter-verification-code": "Entra el codi de 6-dígits", + "get-backup-code-title": "Obtenir còpia de seguretat", + "next": "Següent", + "scan-qr-code": "Escaneja el codi QR amb la teva aplicació d'autenticació", + "send-code": "Enviar codi", + "sms-step-description": "Introdueix el teu número de telèfon per utilitzar com a autenticador.", + "sms-step-label": "Número de telèfon", + "success": "Èxit!", + "totp-step-description-install": "Podeu instal·lar l'aplicació Google Authenticator, Authy, o Duo.", + "totp-step-description-open": "Obre l'aplicació d'autenticació al telèfon mòbil.", + "totp-step-label": "Obtenir app", + "verification-code": "Codi de 6-dígits", + "verification-code-invalid": "Formát de codi invàlid", + "verification-code-incorrect": "El codi de verificació és incorrecte", + "verification-code-many-request": "Massa sol·licituds, revisa el codi de verificació", + "verification-step-description": "Introdueix el codi de 6-dígits que acabem d'enviar a {{address}}", + "verification-step-label": "Verificació" + }, + "provider": { + "email": "Correu electrònic", + "email-description": "Usar un codi de seguretat enviat al teu email per autenticar-te.", + "email-hint": "Els codis d'autenticació s'han enviat per correu electrònic a {{ info }}", + "sms": "SMS", + "sms-description": "Usar el teu telèfon per autenticar-te. Enviarem un codi de seguretat via SMS.", + "sms-hint": "Els codis d'autenticació s'han enviat per missatge de text SMS a {{ info }}", + "totp": "App d'autenticació", + "totp-description": "Utilitzeu aplicacions com Google Authenticator, Authy o Duo al vostre telèfon per autenticar-vos. Es generarà un codi de seguretat per iniciar sessió.", + "totp-hint": "L'aplicació d'autenticació s'ha configurat al vostre compte", + "backup_code": "Codi Backup", + "backup-code-description": "Aquests codis de seguretat imprimibles són d'un sol ús, et permeten identificar quan no tinguis el telèfon a mà, útil quan es viatja.", + "backup-code-hint": "{{ info }} codis d'un sol ús actius en aquest moment" + } + }, + "password-requirement": { + "at-least": "Al menys:", + "character": "{ count, plural, 1 {1 caracter} other {# caracteres} }", + "digit": "{ count, plural, 1 {1 dígito} other {# dígitos} }", + "incorrect-password-try-again": "Contrasenya incorrecta. Prova una altra vegada", + "lowercase-letter": "{ count, plural, 1 {1 minúscula} other {# mínusculas} }", + "new-passwords-not-match": "Les contrasenyes no coincideixen", + "password-should-not-contain-spaces": "La contrasenya no pot contenir espais", + "password-not-meet-requirements": "La contrasenya no reuneix els requisits necessaris", + "password-requirements": "Requisits de contrasenya", + "password-should-difference": "La nova contrasenya ha de ser diferent de l'actual", + "special-character": "{ count, plural, 1 {1 caracter especial} other {# caracteres especiales} }", + "uppercase-letter": "{ count, plural, 1 {1 mayúscula} other {# mayúsculas} }" + } + }, + "relation": { + "relations": "Relacions", + "direction": "Direcció", + "search-direction": { + "FROM": "Des de", + "TO": "Cap a" + }, + "direction-type": { + "FROM": "des de", + "TO": "cap a" + }, + "from-relations": "Relacions salientes (outbound)", + "to-relations": "Relacions entrantes (inbound)", + "selected-relations": "{ count, plural, 1 {1 relación} other {# relaciones} } seleccionades", + "type": "Tipus", + "to-entity-type": "Cap a tipus d'entitat", + "to-entity-name": "Cap a nom d'entitat", + "from-entity-type": "Des de tipus d'entitat", + "from-entity-name": "Des de nom d'entitat", + "to-entity": "Cap a entitat", + "from-entity": "Des d'entitat", + "delete": "Esborrar relació", + "relation-type": "Tipus de relació", + "relation-type-required": "Cal tipus de relació.", + "relation-type-max-length": "El tipus de relació ha de ser inferior a 256", + "any-relation-type": "Qualsevol tipus", + "add": "Afegir relació", + "edit": "Editar relació", + "view": "Veure relació", + "delete-to-relation-title": "Vols eliminar la relació amb l'entitat '{{entityName}}'?", + "delete-to-relation-text": "Atenció, després de la confirmació l'entitat '{{entityName}}' no estarà relacionada amb l'entitat actual.", + "delete-to-relations-title": "Vols eliminar { count, plural, 1 {1 relación} other {# relaciones} }?", + "delete-to-relations-text": "Atenció, després de la confirmació totes les relacions seleccionades s'eliminaran i les seves entitats corresponents no estaran relacionades amb l'entitat actual.", + "delete-from-relation-title": "Vols eliminar la relació amb l'entitat '{{entityName}}'?", + "delete-from-relation-text": "Atenció, després de la confirmació l'entitat actual no estarà relacionada amb l'entitat '{{entityName}}'.", + "delete-from-relations-title": "Vols eliminar { count, plural, 1 {1 relación} other {# relaciones} }?", + "delete-from-relations-text": "Atenció, després de la confirmació totes les relacions seleccionades s'eliminaran i les seves entitats corresponents no estaran relacionades amb les entitats corresponents.", + "remove-relation-filter": "Treure filtre de relació", + "add-relation-filter": "Afegir filtre de relació", + "any-relation": "Qualsevol relació", + "relation-filters": "Filtre de relació", + "additional-info": "Informació adicional (JSON)", + "invalid-additional-info": "Error al analitzar el fitxer JSON de informació adicional.", + "no-relations-text": "No s'han trobar relacions" + }, + "resource": { + "add": "Afegeix un recurs", + "copyId": "Copia l'identificador del recurs", + "delete": "Suprimeix el recurs", + "delete-resource-text": "Aneu amb compte, després de la confirmació el recurs es tornarà irrecuperable.", + "delete-resource-title": "Esteu segur que voleu suprimir el recurs '{{resourceTitle}}'?", + "delete-resources-action-title": "Suprimeix { count, plural, 1 {1 resource} other {# resources} }", + "delete-resources-text": "Tingueu en compte que els recursos seleccionats, encara que s'utilitzin en perfils de dispositius, se suprimiran.", + "delete-resources-title": "Esteu segur que voleu suprimir { count, plural, 1 {1 resource} other {# resources} }?", + "download": "Descarrega el recurs", + "drop-file": "Deixeu anar un fitxer de recursos o feu clic per seleccionar un fitxer per carregar.", + "drop-resource-file-or": "Arrossegar i deixar anar un fitxer o", + "empty": "El recurs està buit", + "file-name": "Nom de l'arxiu", + "idCopiedMessage": "L'identificador del recurs s'ha copiat al porta-retalls", + "no-resource-matching": "No hi ha coincidència de recursos '{{widgetsBundle}}' els van trobar.", + "no-resource-text": "No s'han trobat recursos", + "open-widgets-bundle": "Obre el paquet de ginys", + "resource": "Recurs", + "resource-library-details": "Detalls de recurs", + "resource-type": "Tipus de recurs", + "resources-library": "Biblioteca de recursos", + "search": "Cerca recursos", + "selected-resources": "{ count, plural, 1 {1 resource} other {# resources} } seleccionat", + "system": "Sistema", + "title": "Títol", + "title-required": "Cal títol.", + "title-max-length": "El títol ha de ser inferior a 256" + }, + "rulechain": { + "rulechain": "Cadena de Regla", + "rulechains": "Cadenes de Regles", + "root": "Arrel", + "delete": "Esborrar cadena de regles", + "name": "Nom", + "name-required": "Cal el nom.", + "name-max-length": "El nom ha de ser inferior a 256", + "description": "Descripció", + "add": "Afegir Cadena", + "set-root": "Fer la cadena de regles arrel", + "set-root-rulechain-title": "Voleu fer la cadena de regles '{{ruleChainName}}' de tipus arrel?", + "set-root-rulechain-text": "Després de la confirmació, la cadena de regles es tornarà arrel i manejarà tots els missatges de transport entrants.", + "delete-rulechain-title": "Vols eliminar la cadena de regles '{{ruleChainName}}'?", + "delete-rulechain-text": "Atenció, després de la confirmació la cadena de regles i totes les dades seran irrecuperables.", + "delete-rulechains-title": "Està segur que vols eliminar { count, plural, 1 {1 cadena de regles} other {# cadenes de regles} }?", + "delete-rulechains-action-title": "Eliminar { count, plural, 1 {1 cadena de regles} other {# cadenes de regles} }", + "delete-rulechains-text": "Atenció, després de la confirmació totes les cadena de regles seleccionades i totes les dades seran irrecuperables.", + "add-rulechain-text": "Afegir nova cadena de regles", + "no-rulechains-text": "Cadenes de regles no trobades", + "rulechain-details": "Detalls de la cadena de regles", + "details": "Detalls", + "events": "Esdeveniments", + "system": "Sistema", + "import": "Importar cadena de regles", + "export": "Exportar cadena de regles", + "export-failed-error": "No es pot exportar la cadena de regles: {{error}}", + "create-new-rulechain": "Crear nova cadena de regles", + "rulechain-file": "Fitxer de cadena de regles", + "invalid-rulechain-file-error": "No es pot importar la cadena de regles: Estructura de dades de la cadena de regles invàlida.", + "copyId": "Copiar ID de la cadena de regles", + "idCopiedMessage": "ID de la cadena de regles ha estat copiada al porta-retalls", + "select-rulechain": "Seleccionar cadena de regles", + "no-rulechains-matching": "No s'han trobar cadenes de regles que coincideixin amb '{{entity}}' .", + "rulechain-required": "Cal cadena de regles", + "management": "Gestió de regles", + "debug-mode": "Manera de Depuració", + "search": "Buscar cadenes de regles", + "selected-rulechains": "{ count, plural, 1 {1 cadena de regles} other {# cadenes de regles} } seleccionades", + "open-rulechain": "Obrir cadena de regles", + "edge-template-root": "Arrel de plantilla", + "assign-to-edge": "Assignar a Edge", + "edge-rulechain": "Cadena de regla de vora", + "unassign-rulechain-from-edge-text": "Després de la confirmació, la cadena de regles quedará sense assignar i la vora no podrà accedir a ella", + "unassign-rulechains-from-edge-title": "Esteu segur que voleu anul·lar l'assignació { count, plural, 1 {1 rulechain} other {# rulechains} }?", + "unassign-rulechains-from-edge-text": "Després de la confirmació, totes les cadenes de regles seleccionades quedaran sense assignar i la vora no podrà accedir a ellas", + "assign-rulechain-to-edge-title": "Assignar cadena (s) de regles a vora", + "assign-rulechain-to-edge-text": "Selecciona les cadenes de regles per assignar a la vora", + "set-edge-template-root-rulechain": "Fer arrel de plantilla de vora de cadena de regles", + "set-edge-tscheduler-eventemplate-root-rulechain": "Feu una cadena de regles com a arrel de plantilla de vora", + "set-edge-template-root-rulechain-title": "Està segur de que desitja que la cadena de regles '{{ruleChainName}}' sigui l'arrel de la plantilla de vora?", + "set-edge-template-root-rulechain-text": "Després de la confirmació, la cadena de regles es convertirá en l'arrel de la plantilla de vora i serà la cadena de regles arrel per les vores recent creats.", + "invalid-rulechain-type-error": "No es pot importar la cadena de regles: Tipus de cadena de regles no vàlid. El tipus esperat és {{expectedRuleChainType}}", + "set-auto-assign-to-edge": "Assignar cadena de regles a les vores en la creació", + "set-auto-assign-to-edge-title": "Està segur de que desitja assignar automàticament la cadena de regles de vora '{{ruleChainName}}' a les vores en la creació?", + "set-auto-assign-to-edge-text": "Després de la confirmació, la cadena de regles de vora s'assignarà automàticament a les vores en la creació.", + "unset-auto-assign-to-edge": "Desmarcar assignar cadena de regles a les vores en la creació", + "unset-auto-assign-to-edge-title": "Està segur de que desitja anul·lar l'assignació de la cadena de regles de vora '{{ruleChainName}}' a les vores en la creació?", + "unset-auto-assign-to-edge-text": "Després de la confirmació, la cadena de regles de vora ja no s'assignarà automàticament a les vores en la creació.", + "unassign-rulechain-title": "Està segur de que desitja desassignar la cadena de regles '{{ruleChainName}}'?", + "unassign-rulechains": "Anul·lar assignació de cadenes de regles", + "assign-rulechains": "Assignar cadenes de regles", + "delete-rulechains": "Eliminar cadenes de regles", + "unassign-rulechain": "Anul·lar assignació de cadena de regles", + "unassign-rulechains-from-edge-action-title": "Anul·lar assignació {count, plural, 1 {1 cadena de regles} other {# cadenes de regles} } des vores", + "assign-new-rulechain": "Assignar nova cadena de regles" + }, + "rulenode": { + "details": "Detalls", + "events": "Esdeveniments", + "search": "Buscar nodes", + "open-node-library": "Obrir llibreria de nodes", + "add": "Afegir node de regles", + "name": "Nom", + "name-required": "Cal el nom.", + "name-max-length": "El nom ha de ser inferior a 256", + "type": "Tipus", + "description": "Descripció", + "delete": "Eliminar node de regles", + "select-all-objects": "Seleccionar tots els nodes i conexions", + "deselect-all-objects": "Desfer selecció de tots els nodes i conexions", + "delete-selected-objects": "Eliminar nodes i conexions seleccionats", + "delete-selected": "Eliminar seleccionat", + "create-nested-rulechain": "Crea una cadena de regles imbricades", + "select-all": "Seleccionar tots", + "copy-selected": "Copiar seleccionat", + "deselect-all": "Desfer selecció de tots", + "rulenode-details": "Detalls del node de regles", + "debug-mode": "Manera Depuració", + "configuration": "Configuració", + "link": "Enllaç", + "link-details": "Detalls del enllaç del node de regles", + "add-link": "Afegir enllaç", + "link-label": "Etiqueta del enllaç", + "link-label-required": "Cal l'etiqueta del enllaç.", + "custom-link-label": "Etiqueta del enllaç personalitzada", + "custom-link-label-required": "Cal l'etiqueta del enllaç personalitzat.", + "link-labels": "Etiquetes del enllaç", + "link-labels-required": "Cal les etiquetes del enllaç.", + "no-link-labels-found": "Etiquetes d'enllaços no trobades", + "no-link-label-matching": "'{{label}}' no trobada.", + "create-new-link-label": "Crear una nova!", + "type-filter": "Filtre", + "type-filter-details": "Filtrar missatges entrantes amb les condicions configurades", + "type-enrichment": "Enriquiment", + "type-enrichment-details": "Afegir informació adicional en missatges de metadades", + "type-transformation": "Transformació", + "type-transformation-details": "Canviar carga útil del Missatge i Metadades", + "type-action": "Acció", + "type-action-details": "Ejecutar acció especial", + "type-analytics": "Analítica", + "type-analytics-details": "Ejecutar análisis de dades transmesos o persistents", + "type-external": "Extern", + "type-external-details": "Interactuar amb sistemas externs", + "type-rule-chain": "Cadena de regles", + "type-rule-chain-details": "Reenviar els missatges entrantes a la cadena de regles especificada", + "type-flow": "Flow", + "type-flow-details": "Organitza el flow de missatges", + "type-input": "Entrada", + "type-input-details": "Entrada lògica de la Cadena de Regles, reenviar els missatges entrants al següent node de regla relacionat.", + "type-unknown": "Desconegut", + "type-unknown-details": "Regla de node no resolta", + "directive-is-not-loaded": "La directiva de configuració definida '{{directiveName}}' no está disponible.", + "ui-resources-load-error": "Error al carregar els recursos de configuració UI.", + "invalid-target-rulechain": "No es pot resoldre la cadena de regles objetiu!", + "test-script-function": "Prova Script de funció", + "message": "Missatge", + "message-type": "Tipus de missatge", + "select-message-type": "Seleccionar tipus de missatge", + "message-type-required": "Cal e tipus de missatge", + "metadata": "Metadades", + "metadata-required": "Les entrades de metadades no poden estar buides.", + "output": "Sortida", + "test": "Test", + "help": "Ajuda", + "reset-debug-mode": "Restablir el mode de depuració a tots els nodes" + }, + "role": { + "role": "Rol", + "roles": "Rols", + "management": "Gestió de rols", + "view-roles": "Veure rols", + "no-roles-matching": "No coincideixen els rols '{{entity}}' els van trobar.", + "role-list": "Llista de rols", + "add": "Afegeix un rol", + "view": "Mostra el rol", + "search": "Cerca rols", + "selected-roles": "{ count, plural, 1 {1 role} other {# roles} } seleccionat", + "no-roles-text": "No s'han trobat rols", + "role-details": "Detalls del rol", + "add-role-text": "Afegeix un paper nou", + "delete": "Suprimeix la funció", + "delete-roles": "Suprimir rols", + "delete-role-title": "Esteu segur que voleu suprimir la funció '{{roleName}}'?", + "delete-role-text": "Aneu amb compte, després de la confirmació, la funció i totes les dades relacionades es tornaran irrecuperables.", + "delete-roles-title": "Esteu segur que voleu suprimir { count, plural, 1 {1 role} other {# roles} }?", + "delete-roles-action-title": "Suprimeix { count, plural, 1 {1 role} other {# roles} }", + "delete-roles-text": "Aneu amb compte, després de la confirmació, tots els rols seleccionats s'eliminaran i totes les dades relacionades seran irrecuperables.", + "role-type": "Tipus de rol", + "role-type-required": "Cal tipus de rol.", + "select-role-type": "Seleccioneu el tipus de rol", + "enter-role-type": "Introduïu el tipus de rol", + "any-role": "Qualsevol paper", + "no-role-types-matching": "No coincideix cap tipus de rol '{{entitySubtype}}' els van trobar.", + "role-type-list-empty": "No s'ha seleccionat cap tipus de rol.", + "role-types": "Tipus de rols", + "created-time": "Temps creat", + "name": "Nom", + "name-required": "Cal el nom.", + "name-max-length": "El nom ha de ser inferior a 256", + "description": "Descripció", + "events": "Esdeveniments", + "details": "Detalls", + "copyId": "Copia l'identificador del rol", + "idCopiedMessage": "L'identificador de funció s'ha copiat al porta-retalls", + "permissions": "Permisos", + "role-required": " Cal rol", + "roles-required": "Cal rols", + "display-type": { + "GENERIC": "Genèric", + "GROUP": "Grup" + } + }, + "group-permission": { + "user-group-roles": "Rols del grup d'usuaris", + "entity-group-permissions": "Permisos del grup d'entitats", + "role-type": "Tipus de rol", + "role-name": "Nom del rol", + "group-type": "Tipus de grup", + "group-name": "Nom del grup", + "group-owner": "Propietari del grup", + "user-group-name": "Nom del grup d'usuaris", + "user-group-owner": "Propietari del grup d'usuaris", + "edit": "Edita els permisos", + "delete": "Suprimeix els permisos", + "selected-group-permissions": "{ count, plural, 1 {1 group permission} other {# group permissions} } seleccionat", + "delete-group-permission-title": "Esteu segur que voleu suprimir el permís del grup '{{roleName}}'?", + "delete-group-permission-text": "Aneu amb compte, després de la confirmació, el permís del grup i totes les dades relacionades es tornaran irrecuperables.", + "delete-group-permission": "Suprimeix el permís de grup", + "delete-group-permissions-title": "Esteu segur que voleu suprimir { count, plural, 1 {1 group permission} other {# group permission} }?", + "delete-group-permissions-text": "Aneu amb compte, després de la confirmació s'eliminaran tots els permisos de grup seleccionats i els usuaris corresponents perdran l'accés als recursos especificats.", + "delete-group-permissions": "Suprimeix els permisos del grup", + "add-group-permission": "Afegeix permís de grup", + "edit-group-permission": "Edita el permís del grup", + "entity-group": "Grup d'entitats", + "user-group": "Grup d'usuaris", + "no-owners-matching": "No hi ha cap propietari que coincideixi '{{owner}}' els van trobar.", + "target-owner-required": "El propietari del grup d'entitats és obligatori.", + "target-user-group-owner-required": "El propietari del grup d'usuaris és obligatori.", + "no-group-permissions-text": "No s'han trobat permisos de grup" + }, + "permission": { + "permissions-required": "S'ha d'especificar almenys una entrada de permís.", + "remove-permission": "Elimina l'entrada de permís", + "add-permission": "Afegeix una entrada de permís", + "other": "Altres", + "resource": { + "resource": "Recurs", + "select-resource": "Seleccioneu el recurs", + "resource-required": "Cal un recurs", + "no-resources-matching": "No hi ha recursos coincidents '{{resource}}' els van trobar.", + "display-type": { + "ALL": "Tots", + "PROFILE": "Perfil", + "ADMIN_SETTINGS": "Configuració d'administrador", + "ALARM": "Alarma", + "DEVICE": "Dispositiu", + "DEVICE_PROFILE": "Perfil del dispositiu", + "ASSET": "Actiu", + "CUSTOMER": "Client", + "DASHBOARD": "Panell", + "ENTITY_VIEW": "Vista d'entitat", + "EDGE": "Edge", + "TENANT": "Llogater", + "TENANT_PROFILE": "Perfil del llogater", + "RULE_CHAIN": "Cadena de regles", + "USER": "Usuari", + "WIDGETS_BUNDLE": "Paquet de widgets", + "WIDGET_TYPE": "Tipus de widget", + "CONVERTER": "Convertidor", + "INTEGRATION": "Integració", + "SCHEDULER_EVENT": "Agenda d'esdeveniments", + "BLOB_ENTITY": "Entitat Blob", + "CUSTOMER_GROUP": "Grup de clients", + "DEVICE_GROUP": "Grup de dispositius", + "ASSET_GROUP": "Grup d'actius", + "USER_GROUP": "Grup d'usuaris", + "ENTITY_VIEW_GROUP": "Grup de visualització d'entitats", + "DASHBOARD_GROUP": "Grup de Dashboard", + "ROLE": "Rol", + "GROUP_PERMISSION": "Permís de grup", + "WHITE_LABELING": "Etiquetat blanc", + "AUDIT_LOG": "Registre d'auditoria", + "API_USAGE_STATE": "Estat d'ús de l'API", + "TB_RESOURCE": "Recurs", + "EDGE_GROUP": "Grup Edge", + "OTA_PACKAGE": "Paquet Ota" + } + }, + "operation": { + "operation": "Operació", + "operations": "Operacions", + "operations-required": "S'ha d'especificar almenys una operació.", + "enter-operation": "Entra en funcionament", + "no-operations-matching": "No coincideix cap operació '{{operation}}' els van trobar.", + "display-type": { + "ALL": "Tots", + "CREATE": "Crear", + "READ": "Llegeix", + "WRITE": "Escriu", + "DELETE": "Suprimeix", + "ASSIGN_TO_CUSTOMER": "Assign to Customer", + "UNASSIGN_FROM_CUSTOMER": "Cancel·la l'assignació del client", + "RPC_CALL": "Trucada RPC", + "READ_CREDENTIALS": "Llegeix credencials", + "WRITE_CREDENTIALS": "Escriu credencials", + "READ_ATTRIBUTES": "Llegeix els atributs", + "WRITE_ATTRIBUTES": "Escriu atributs", + "READ_TELEMETRY": "Llegeix Telemetria", + "WRITE_TELEMETRY": "Escriu telemetria", + "CLAIM_DEVICES": "Dispositius de reclamació", + "IMPERSONATE": "Suplantar la identitat", + "CHANGE_OWNER": "Canvia de propietari", + "ADD_TO_GROUP": "Afegeix al grup", + "REMOVE_FROM_GROUP": "Elimina del grup", + "SHARE_GROUP": "Comparteix el grup", + "ASSIGN_TO_TENANT": "Assigna a l'arrendatari" + } + } + }, + "scheduler": { + "scheduler": "Planificador", + "scheduler-event": "Planificador d'esdeveniment", + "select-scheduler-event": "Seleccionar planificador d'esdeveniment", + "no-scheduler-events-matching": "Planificadors d'esdeveniments que coincideixin amb '{{entity}}' no van ser trobats.", + "scheduler-event-required": "Cal planificador d'esdeveniment", + "management": "Gestió del pla", + "scheduler-events": "Planificador d'esdeveniment", + "add-scheduler-event": "Afegir planificador d'esdeveniment", + "search-scheduler-events": "Buscar planificador d'esdeveniment", + "created-time": "Temps de creació", + "name": "Nom", + "name-required": "Cal nom", + "name-max-length": "El nom ha de ser inferior a 256.", + "type": "Tipus", + "created_customer": "Creat pel client", + "edit-scheduler-event": "Editar planificador d'esdeveniment", + "view-scheduler-event": "Mostra l'esdeveniment del programador", + "delete-scheduler-event": "Eliminar planificador d'esdeveniment", + "no-scheduler-events": "Planificador d'esdeveniment no trobat", + "selected-scheduler-events": "{ count, plural, 1 {1 planificador de esdeveniments} other {# planificadors de esdeveniments} } seleccionat", + "delete-scheduler-event-title": "Està segur que desitja eliminar el planificador d'esdeveniments '{{schedulerEventName}}'?", + "delete-scheduler-event-text": "Anar amb compte, després de la confirmació, el planificador d'esdeveniments i totes les dades relacionades és tornaran irrecuperables.", + "delete-scheduler-events-title": "Està segur que desitja eliminar { count, plural, 1 {1 planificador de esdeveniments} other {# planificadors de esdeveniments} }?", + "delete-scheduler-events-text": "Anar amb compte, després de la confirmació s'eliminaran totes els planificadors d'esdeveniments seleccionats i totes les dades relacionades és tornaran irrecuperables.", + "create": "Crear planificador d'esdeveniment", + "edit": "Editar planificador d'esdeveniment", + "view": "Mostra l'esdeveniment del programador", + "configuration": "Configuració", + "schedule": "Planificar", + "start-time": "Hora d'inici", + "repeat": "Repetició", + "repeats": "Repeticions", + "daily": "Diarament", + "weekly": "Setmanalment", + "monthly": "Mensualment", + "yearly": "Anual", + "timer": "Basat en temporitzador", + "repeats-required": "Repeticions són requerides.", + "repeat-on": "Repetir en", + "repeat-every": "Repeteix cada", + "ends-on": "Acabar en", + "sunday-label": "Dg", + "monday-label": "Dl", + "tuesday-label": "Dm", + "wednesday-label": "Dc", + "thursday-label": "Dj", + "friday-label": "Dv", + "saturday-label": "Ds", + "repeat-on-sunday": "Repetir el Diumenge", + "repeat-on-monday": "Repetir el Dilluns", + "repeat-on-tuesday": "Repetir el Dimarts", + "repeat-on-wednesday": "Repetir el Dimecres", + "repeat-on-thursday": "Repetir el Dijous", + "repeat-on-friday": "Repetir el Divendres", + "repeat-on-saturday": "Repetir el Dissabte", + "event-type": "Tipus d'esdeveniments", + "select-event-type": "Seleccionar tipus d'esdeveniment", + "event-type-required": "Tipus d'esdeveniment és requerit.", + "event-type-max-length": "El tipus d'esdeveniment ha de ser inferior a 256.", + "list-mode": "Vista de la llista", + "calendar-mode": "Vista del calendari", + "calendar-view-type": "Tipus de vista del calendari", + "month": "Mes", + "week": "Setmana", + "day": "Dia", + "agenda-week": "Agenda Setmanal", + "agenda-day": "Agenda Diaria", + "list-year": "Llista Anual", + "list-month": "Llista Mensual", + "list-week": "Llista Setmanal", + "list-day": "Llista Diaria", + "today": "Avui", + "navigate-before": "Navegar Abans de", + "navigate-next": "Navegar després de", + "starting-from": "Inician des de", + "until": "fins a", + "on": "encesa", + "sunday": "Diumenge", + "monday": "Dilluns", + "tuesday": "Dimarts", + "wednesday": "Dimecres", + "thursday": "Dijous", + "friday": "Divendres", + "saturday": "Dissabte", + "originator": "Creador", + "single-entity": "Entitat única", + "group-of-entities": "Grup d'entitats", + "entities-group-owner": "Propietari del grup d'entitats", + "single-device": "Dispositiu únic", + "group-of-devices": "Grup de dispositius", + "devices-group-owner": "Propietari del grup de dispositius", + "message-body": "Cos del missatge", + "target": "Objectiu", + "rpc-method": "Mètode", + "rpc-method-required": "Cal Mètode", + "rpc-method-white-space": "No és permet l'espai en blanc.", + "rpc-params": "Paràmetres", + "select-dashboard-state": "Seleccionar estat del panell", + "hours": "Hores", + "minutes": "Minuts", + "seconds": "Segons", + "time-interval-required": "Cal un interval de temps", + "time-unit-required": "Es requereix una unitat de temps", + "every-hour": "cada { count, plural, 1 {hour} other {# hours} }", + "every-minute": "cada { count, plural, 1 {minute} other {# minutes} }", + "every-second": "cada { count, plural, 1 {second} other {# seconds} }", + "invalid-time": "Hora no vàlida", + "assigned_customer": "Client assignat" + }, + "report": { + "report-config": "Configuració de l'informe", + "email-config": "Configuració del correu electrònic", + "dashboard-state-param": "Valor del parámetro d'estat del panell", + "base-url": "URL base", + "base-url-required": "Cal URL base.", + "use-dashboard-timewindow": "Utilitzar la finestra del temps del panell", + "timewindow": "Finestra del temps", + "name-pattern": "Report de patró de nom", + "name-pattern-required": "Cal el report de patró de nom.", + "type": "Tipus d'informe", + "use-current-user-credentials": "Utilizar credencials d'usuari actuals", + "customer-user-credentials": "Credencials d'usuari client", + "customer-user-credentials-required": "Cal credencials d'usuari client", + "generate-test-report": "Generar informe de proves", + "send-email": "Enviar correu electrònic", + "from": "Des de", + "from-required": "Cal des de.", + "to": "Per", + "to-required": "Cal per.", + "cc": "Amb copia", + "bcc": "Amb copia oculta", + "subject": "Assumpte", + "subject-required": "Cal assumpte.", + "body": "Cos", + "body-required": "Cal cos." + }, + "blob-entity": { + "blob-entity": "Entitat blob", + "select-blob-entity": "Seleccionar entitat blob", + "no-blob-entities-matching": "Entitats blob que coincideixin amb '{{entity}}' no han estat trobades.", + "blob-entity-required": "Cal entitat blob.", + "files": "Fitxers", + "search": "Buscar Fitxers", + "clear-search": "Esborrar cerca", + "no-blob-entities-prompt": "Fitxers no trobats", + "report": "Report", + "created-time": "Temps de creació", + "name": "Nom", + "type": "Tipus", + "created_customer": "Creat pel client", + "selected-blob-entities": "{ count, plural, 1 {1 file} other {# files} } seleccionat", + "download-blob-entity": "Descarregar arxiu", + "delete-blob-entity": "Eliminar arxiu", + "delete-blob-entity-title": "Està segur de que desitja eliminar l'arxiu '{{blobEntityName}}'?", + "delete-blob-entity-text": "Aneu amb compte, després de la confirmació, l'arxiu de dades es tornara irrecuperable.", + "delete-blob-entities-title": "Esteu segur que voleu suprimir { count, plural, 1 {1 file} other {# files} }?", + "delete-blob-entities-text": "Aneu amb compte, després de la confirmació, tots els fitxers seleccionats s'eliminaran i totes les dades relacionades es tornaran irrecuperables.", + "assigned_customer": "Client assignat" + }, + "timezone": { + "timezone": "Zona Horària", + "select-timezone": "Seleccionar zona horària", + "no-timezones-matching": "No hi ha zones horàries que coincideixin amb '{{timezone}}'.", + "timezone-required": "Cal zona horària.", + "browser-time": "Hora del navegador" + }, + "queue": { + "queue-name": "Cua", + "no-queues-matching": "No es van trobar cues que coincideixin amb '{{queue}}'.", + "select_name": "Seleccioneu el nom de la cua", + "name": "Nom Cua", + "name_required": "Cal especificar el nom de cua", + "name-unique": "El nom de cua ja existeix!", + "queue-required": "Cal cua!", + "topic-required": "Cal topic cua!", + "poll-interval-required": "Cal interval d'obtenció!", + "poll-interval-min-value": "L'interval no ha de ser inferior a 1", + "partitions-required": "Cal particions!", + "partitions-min-value": "El valor de partició no ha de ser inferior a 1", + "pack-processing-timeout-required": "Temps d'espera del processament", + "pack-processing-timeout-min-value": "El temps d'espera de processament no pot ser inferior a 1", + "batch-size-required": "Cal grandària del lot!", + "batch-size-min-value": "El valor de mida de lot no pot ser inferior a 1", + "retries-required": "Cal Reintents!", + "retries-min-value": "El valor dels reintents no pot ser negatiu", + "failure-percentage-required": "Cal percentatge de fallades!", + "failure-percentage-min-value": "Percentatge de fallades no pot ser inferior a 0", + "failure-percentage-max-value": "Percentatge de fallades no pot ser més gran de 100", + "pause-between-retries-required": "Cal pausa entre reintents!", + "pause-between-retries-min-value": "Pausa mínima entre reintents no pot ser inferior a 1", + "max-pause-between-retries-required": "Cal pausa màxima entre reintents!", + "max-pause-between-retries-min-value": "Pausa màxima entre reintents no pot ser inferior a 1", + "submit-strategy-type-required": "Cal estratègia d'enviament!", + "processing-strategy-type-required": "Cal estratègia de processament!", + "queues": "Cues", + "selected-queues": "{ count, plural, 1 {1 cola} other {# colas} } seleccionades", + "delete-queue-title": "Estàs segur d'esborrar la cua '{{queueName}}'?", + "delete-queues-title": "Estàs segur d'esborrar { count, plural, 1 {1 cola} other {# colas} }?", + "delete-queue-text": "Atenció, després de la confirmació la cua i totes les seves dades relacionades seran irrecuperables.", + "delete-queues-text": "Atenció, després de la confirmació totes les cues s'esborraran i no seran accessibles.", + "search": "Buscar cua", + "add": "Afegir cua", + "details": "Detalls cua", + "topic": "Tema", + "submit-strategy": "Estratègia d'enviament", + "processing-strategy": "Estratègia de processament", + "poll-interval": "Interval d'obtenció", + "partitions": "Particions", + "consumer-per-partition": "Consumidors per partició", + "consumer-per-partition-hint": "Activar consumidors separats per a cada partició", + "processing-timeout": "Timeout de processament, ms", + "batch-size": "Grandària de lot", + "retries": "Reintents (0 - ilimitados)", + "failure-percentage": "Percentatge de fallades", + "pause-between-retries": "Pausa entre reintents", + "max-pause-between-retries": "Pausa màxima entre reintents", + "delete": "Borrar cua", + "copyId": "Copiar Id de la cua", + "idCopiedMessage": "La Id de la cua s'ha copiat al porta-retalls", + "description": "Descripció", + "description-hint": "Aquest text es mostrarà a la descripció de la cua, en lloc de l'estratègia seleccionada", + "alt-description": "Estratègia d'enviament: {{submitStrategy}}, Estratègia de processament: {{processingStrategy}}", + "strategies": { + "sequential-by-originator-label": "Seqüencial per iniciador", + "sequential-by-originator-hint": "El nou missatge per exemple dispositiu A no s'enviarà fins que el missatge anterior del dispositiu A sigui admès/processat", + "sequential-by-tenant-label": "Seqüencial per propietari", + "sequential-by-tenant-hint": "El missatge nou, per exemple Propietari A, no s'enviarà fins que el missatge anterior del Propietari A sigui admès/processat", + "sequential-label": "Seqüencial", + "sequential-hint": "El missatge nou no s'enviarà fins que el missatge anterior sigui admès/processat", + "burst-label": "Ràfega (Burst)", + "burst-hint": "Tots els missatges s'enviaran cap a les cadenes de regles a l'ordre que arribin", + "batch-label": "Lots (Batch)", + "batch-hint": "El nou lot, no s'enviarà fins que l'anterior lot sigui admès/processat", + "skip-all-failures-label": "Skip to fallades", + "skip-all-failures-hint": "Ignorar tots els errors", + "skip-all-failures-and-timeouts-label": "Skip to fallades i temps morts", + "skip-all-failures-and-timeouts-hint": "Ignorar tots els errors i temps morts", + "retry-all-label": "Reintentar tots", + "retry-all-hint": "Reintentar tots els missatges del lot de processament", + "retry-failed-label": "Reintentar fallits", + "retry-failed-hint": "Reintentar tots els missatges fallits del lot de processament", + "retry-timeout-label": "Timeout de reintents", + "retry-timeout-hint": "Reintentar tots els missatges que donin timeout del lot de processament", + "retry-failed-and-timeout-label": "Reintentar fallits i timeouts", + "retry-failed-and-timeout-hint": "Reintentar tots els missatges fallits i que donin timeout del lot de processament" + } + }, + "tenant": { + "tenant": "Propietari", + "tenants": "Propietaris", + "management": "Gestió de Propietaris", + "add": "Afegir propietari", + "admins": "Admins", + "manage-tenant-admins": "Gestionar administradors del propietari", + "delete": "Eliminar propietari", + "add-tenant-text": "Afegir nou propietari", + "no-tenants-text": "Cap propietari trobat", + "tenant-details": "Detalls del propietari", + "title-max-length": "El títol ha de ser inferior a 256", + "delete-tenant-title": "Vols eliminar el propietari '{{tenantTitle}}'?", + "delete-tenant-text": "Atenció, després de la confirmació el propietari serà eliminat i la informació relacionada serà irrecuperable.", + "delete-tenants-title": "Vols eliminar { count, plural, 1 {1 propietario} other {# propietarios} }?", + "delete-tenants-action-title": "Eliminar { count, plural, 1 {1 propietario} other {# propietarios} }", + "delete-tenants-text": "Atenció, després de la confirmació els propietaris seleccionats seran eliminats i la informació relacionada serà irrecuperable.", + "title": "Títol", + "title-required": "Cal títol.", + "description": "Descripció", + "details": "Detalls", + "events": "Esdeveniments", + "copyId": "Copiar ID de propietari", + "idCopiedMessage": "El ID de propietari s'ha copiat al porta-retalls", + "select-tenant": "Seleccionar propietari", + "no-tenants-matching": "No hi ha propietaris que coincideixin amb '{{entity}}' .", + "tenant-required": "Propietari", + "selected-tenants": "{ count, plural, 1 {1 propietario} other {# propietarios} } seleccionats", + "search": "Buscar propietaris", + "allow-white-labeling": "Permet l'etiqueta blanca", + "allow-customer-white-labeling": "Permet l'etiqueta blanca del client", + "isolated-tb-core": "Processant en contenidor aïllat", + "isolated-tb-rule-engine": "Processant en contenidor Motor de Regles aïllat", + "isolated-tb-core-details": "Requereix microserveis separats per propietari aïllat", + "isolated-tb-rule-engine-details": "Requereix microserveis separats per propietari aïllat" + }, + "tenant-profile": { + "tenant-profile": "Perfil de propietari", + "tenant-profiles": "Perfils de propietarios", + "add": "Afegir perfil de propietari", + "edit": "Editar perfil de propietari", + "tenant-profile-details": "Detalls perfil de propietari", + "no-tenant-profiles-text": "No s'han trobar perfils de propietari", + "name-max-length": "El nom ha de ser inferior a 256", + "search": "Buscar perfils de propietari", + "selected-tenant-profiles": "{ count, plural, 1 {1 perfil de propietario} other {# perfils de propietario} } seleccionats", + "no-tenant-profiles-matching": "No s'han trobat perfils del propietari que coincideixin amb '{{entity}}'.", + "tenant-profile-required": "Cal perfil de propietari", + "idCopiedMessage": "El ID de perfil de propietari s'ha copiat al porta-retalls", + "set-default": "Fer perfil propietari per defecte", + "delete": "Esborrar perfil", + "copyId": "Copiar ID de perfil", + "name": "Nom", + "name-required": "Cal nom.", + "data": "Dades de perfil", + "profile-configuration": "Configuració de perfil", + "description": "Descripció", + "default": "Defecte", + "delete-tenant-profile-title": "Eliminar el perfil propietari '{{tenantProfileName}}'?", + "delete-tenant-profile-text": "Atenció, després de la confirmació, el perfil de propietari serà borrat i la seva informació relacionada serà irrecuperable.", + "delete-tenant-profiles-title": "Eliminar { count, plural, 1 {1 perfil propietario} other {# perfils propietarios} }?", + "delete-tenant-profiles-text": "Atenció, després de la confirmació, els perfils seleccionats s'eliminaran i la seva informació relacionada serà irrecuperable.", + "set-default-tenant-profile-title": "Vols fer el perfil propietari '{{tenantProfileName}}' per defecte?", + "set-default-tenant-profile-text": "Després de la confirmació, el perfil propietari serà marcat per defecte i serà utilitzat pels nous perfils propietaris que no tinguin perfil específic.", + "no-tenant-profiles-found": "No s'han trobar perfils de propietari.", + "create-new-tenant-profile": "Crear un nou perfil!", + "create-tenant-profile": "Crea un perfil d'inquilí nou", + "import": "Importa el perfil d'inquilí", + "export": "Exporta el perfil d'inquilí", + "export-failed-error": "No es pot exportar el perfil d'inquilí: {{error}}", + "tenant-profile-file": "Fitxer de perfil de llogater", + "invalid-tenant-profile-file-error": "No es pot importar el perfil d'inquilí: l'estructura de dades del perfil d'inquilí no és vàlida.", + "maximum-devices": "Nº Màxim de dispositius (0 - sense límit)", + "maximum-devices-required": "Cal Nº Màxim de dispositius.", + "maximum-devices-range": "Nº Màxim de dispositius no pot ser negatiu", + "maximum-assets": "Nº Màxim d'actius (0 - sense límits)", + "maximum-assets-required": "Cal Nº Màxim d'actius.", + "maximum-assets-range": "Nº Màxim d'actius no pot ser negatiu", + "maximum-customers": "Nº Màxim de clients (0 - sense límit)", + "maximum-customers-required": "Cal Nº Màxim de clients.", + "maximum-customers-range": "Nº Màxim de clients no pot ser negatiu", + "maximum-users": "Nº Màxim d'usuaris (0 - sense límit)", + "maximum-users-required": "Cal Nº Màxim d'usuaris.", + "maximum-users-range": "Nº Màxim d'usuaris no pot ser negatiu", + "maximum-dashboards": "Nº Màxim de panells (0 - sense límit)", + "maximum-dashboards-required": "Cal Nº Màxim de panells.", + "maximum-dashboards-range": "Nº Màxim de panells no pot ser negatiu", + "maximum-rule-chains": "Nº Màxim de cadenes de regles (0 - sense límit)", + "maximum-rule-chains-required": "Cal Nº Màxim de cadenes de regles.", + "maximum-rule-chains-range": "Nº Màxim de cadenes de regles no pot ser negatiu", + "maximum-integrations": "Nombre màxim d'integracions (0 - il·limitat)", + "maximum-integrations-required": "Cal nombre màxim d'integracions.", + "maximum-integrations-range": "Nombre màxim d'integracions no pot ser negatiu", + "maximum-converters": "Nombre màxim de convertidors (0 - il·limitat)", + "maximum-converters-required": "Cal nombre màxim de convertidors.", + "maximum-converters-range": "Nombre màxim de convertidors no pot ser negatiu", + "maximum-scheduler-events": "Nombre màxim d'esdeveniments del programador (0 - il·limitat)", + "maximum-scheduler-events-required": "Cal nombre màxim d'esdeveniments del programador.", + "maximum-scheduler-events-range": "Nombre màxim d'esdeveniments del programador no pot ser negatiu", + "maximum-resources-sum-data-size": "Suma màxima de la mida dels fitxers de recursos en bytes (0 - il·limitat)", + "maximum-resources-sum-data-size-required": "Cal suma màxima de la mida dels fitxers de recursos.", + "maximum-resources-sum-data-size-range": "Suma màxima de la mida dels fitxers de recursos no pot ser negatiu", + "maximum-ota-packages-sum-data-size": "Suma màxima de la mida dels fitxers del paquet ota en bytes (0 - il·limitat)", + "maximum-ota-package-sum-data-size-required": "Cal suma màxima de la mida dels fitxers del paquet ota.", + "maximum-ota-package-sum-data-size-range": "Suma màxima de la mida dels fitxers del paquet ota no pot ser negatiu", + "transport-tenant-msg-rate-limit": "Taxa de missatges de transport per propietari.", + "transport-tenant-telemetry-msg-rate-limit": "Taxa de missatges de telemetria per propietari.", + "transport-tenant-telemetry-data-points-rate-limit": "Taxa de punts de dades per propietari.", + "transport-device-msg-rate-limit": "Taxa de missatges de dispositiu.", + "transport-device-telemetry-msg-rate-limit": "Taxa de missatges de telemetria de dispositiu.", + "transport-device-telemetry-data-points-rate-limit": "Taxa de punts de dades de telemetria de dispositiu.", + "max-transport-messages": "Nº Màxim de missatges de transport (0 - sense límit)", + "max-transport-messages-required": "Cal Nº Màxim de missatges de transport.", + "max-transport-messages-range": "Nº Màxim de missatges de transport no pot ser negatiu", + "max-transport-data-points": "Nº Màxim de punts de dades transport (0 - sense límit)", + "max-transport-data-points-required": "Cal Nº Màxim de punts de dades transport.", + "max-transport-data-points-range": "Nº Màxim de punts de dades transport no pot ser negatiu", + "max-r-e-executions": "Nº Màxim de execucions de motor de regles (0 - sense límit)", + "max-r-e-executions-required": "Cal Nº Màxim de execucions de motor de regles.", + "max-r-e-executions-range": "Nº Màxim de execucions de motor de regles no pot ser negatiu", + "max-j-s-executions": "Nº Màxim de execucions JavaScript (0 - sense límit)", + "max-j-s-executions-required": "Cal Nº Màxim de execucions JavaScript.", + "max-j-s-executions-range": "Nº Màxim de execucions JavaScript no pot ser negatiu", + "max-d-p-storage-days": "Nº Màxim de dies a gravar en punts de dades (0 - sense límit)", + "max-d-p-storage-days-required": "Cal Nº Màxim de dies.", + "max-d-p-storage-days-range": "Nº Màxim de dies no pot ser negatiu", + "default-storage-ttl-days": "Dies per defecte gravats TTL (0 - sense límit)", + "default-storage-ttl-days-required": "Cal dies per defecte TTL.", + "default-storage-ttl-days-range": "Dies per defecte TTL no pot ser negatiu", + "alarms-ttl-days": "Alarmes TTL dies (0 - il·limitat)", + "alarms-ttl-days-required": "Cal alarmes TTL dies", + "alarms-ttl-days-days-range": "Alarmes TTL dies no pot ser negatiu", + "rpc-ttl-days": "RPC TTL days (0 - il·limitat)", + "rpc-ttl-days-required": "Cal RPC TTL dies", + "rpc-ttl-days-days-range": "RPC TTL days no pot ser negatiu", + "max-rule-node-executions-per-message": "Nº Màxim de execucions (cadena de regles) per missatge (0 - sense límit)", + "max-rule-node-executions-per-message-required": "Cal Nº Màxim de execucions per missatge.", + "max-rule-node-executions-per-message-range": "Nº Màxim de execucions per missatge no pot ser negatiu", + "max-correus electrònics": "Nº Màxim de correus electrònics (0 - sense límit)", + "max-correus electrònics-required": "Cal Nº Màxim de correus electrònics.", + "max-correus electrònics-range": "Nº Màxim de correus electrònics no pot ser negatiu", + "max-sms": "Nº Màxim de missatges SMS (0 - sense límit)", + "max-sms-required": "Cal Nº Màxim de missatges SMS.", + "max-sms-range": "Nº Màxim de missatges SMS no pot ser negatiu", + "max-created-alarms": "Nombre màxim d'alarmes creades (0 - il·limitat)", + "max-created-alarms-required": "Cal nombre màxim d'alarmes creades.", + "max-created-alarms-range": "Nombre màxim d'alarmes creades no pot ser negatiu", + "no-queue": "No s'ha configurat cap cua", + "add-queue": "Afegeix cua", + "queues-with-count": "Cues ({{count}})", + "tenant-rest-limits": "Límit de tarifa per a les sol·licituds REST per a llogaters", + "customer-rest-limits": "Límit de tarifa per a sol·licituds REST per al client", + "incorrect-pattern-for-rate-limits": "El format és parells separats per comes de capacitat i punt (in seconds) amb dos punts entre, p. 100:1,2000:60", + "too-small-value-zero": "El valor ha de ser més gran que 0", + "too-small-value-one": "El valor ha de ser més gran que 1", + "cassandra-tenant-limits-configuration": "Límit de taxa de consultes de Cassandra per al llogater", + "ws-limit-max-sessions-per-tenant": "Nombre màxim de sessions de WS per inquilí", + "ws-limit-max-sessions-per-customer": "Nombre màxim de sessions de WS per client", + "ws-limit-max-sessions-per-public-user": "Nombre màxim de sessions de WS per usuari públic", + "ws-limit-queue-per-session": "Mida màxima de la cua de missatges de WS per sessió", + "ws-limit-max-subscriptions-per-tenant": "Nombre màxim de subscripcions de WS per inquilí", + "ws-limit-max-subscriptions-per-customer": "Nombre màxim de subscripcions de WS per client", + "ws-limit-max-subscriptions-per-regular-user": "Nombre màxim de subscripcions de WS per usuari habitual", + "ws-limit-max-subscriptions-per-public-user": "Nombre màxim de subscripcions de WS per usuari públic", + "ws-limit-updates-per-session": "Límit de tarifa per a les actualitzacions de WS per sessió" + }, + "timeinterval": { + "seconds-interval": "{ seconds, plural, 1 {1 segundo} other {# segons} }", + "minutes-interval": "{ minutes, plural, 1 {1 minuto} other {# minutos} }", + "hours-interval": "{ hours, plural, 1 {1 hora} other {# horas} }", + "days-interval": "{ days, plural, 1 {1 día} other {# dies} }", + "days": "Dies", + "hours": "Hores", + "minutes": "Minuts", + "seconds": "Segons", + "advanced": "Avançat", + "predefined": { + "yesterday": "Ahir", + "day-before-yesterday": "Abans d'ahir", + "this-day-last-week": "Aquest dia de la setmana passada", + "previous-week": "Setmana anterior (dg - ds)", + "previous-week-iso": "Setmana anterior (dl-dg)", + "previous-month": "Mes anterior", + "previous-year": "Any anterior", + "current-hour": "Hora actual", + "current-day": "Dia actual", + "current-day-so-far": "Dia actual fins ara", + "current-week": "Setmana actual (dg - ds)", + "current-week-iso": "Setmana actual (dl-dg)", + "current-week-so-far": "La setmana actual fins ara (dg - ds)", + "current-week-iso-so-far": "La setmana actual fins al moment (dl - dg)", + "current-month": "Aquest mes", + "current-month-so-far": "Mes actual fins ara", + "current-year": "Aquest any", + "current-year-so-far": "Any en curs fins ara" + } + }, + "timeunit": { + "milliseconds": "Mil·lisegons", + "seconds": "Segons", + "minutes": "Minuts", + "hours": "Hores", + "days": "Dies" + }, + "timewindow": { + "days": "{ days, plural, 1 { día } other {# dies } }", + "hours": "{ hours, plural, 0 { horas } 1 {1 hora } other {# horas } }", + "minutes": "{ minutes, plural, 0 { minutos } 1 {1 minuto } other {# minutos } }", + "seconds": "{ seconds, plural, 0 { segons } 1 {1 segundo } other {# segons } }", + "realtime": "Temps-real", + "history": "Històric", + "last-prefix": "últim(s)", + "period": "Des de {{ startTime }} fins a {{ endTime }}", + "edit": "Editar finestra del temps", + "date-range": "Rang de dates", + "last": "Últims(s)", + "time-period": "Període del temps", + "hide": "Amagar", + "interval": "Interval" + }, + "user": { + "user": "Usuari", + "users": "Usuaris", + "management": "Gestió d'usuaris", + "customer-users": "Usuaris del Client", + "tenant-admins": "Admins propietaris", + "sys-admin": "Administrador del Sistema", + "tenant-admin": "Administrador del Propietari", + "customer": "Client", + "anonymous": "Anònim", + "add": "Afegir usuari", + "delete": "Eliminar usuari", + "add-user-text": "Afegir nou usuari", + "no-users-text": "Ningun usuari trobat", + "user-details": "Detalls de l'usuari", + "delete-users": "Eliminar usuaris", + "delete-user-title": "Eliminar l'usuari '{{userEmail}}'?", + "delete-user-text": "Atenció, després de la confirmació l'usuari seleccionar serà eliminat i la informació relacionada serà irrecuperable.", + "delete-users-title": "Eliminar { count, plural, 1 {1 usuario} other {# usuaris} }?", + "delete-users-action-title": "Esborrar { count, plural, 1 {1 usuario} other {# usuaris} }", + "delete-users-text": "Atenció, després de la confirmació dels usuaris seleccionats seran eliminats i la informació relacionada serà irrecuperable.", + "activation-email-sent-message": "Correu d'activació enviat amb èxit!", + "resend-activation": "Reenviar activació", + "email": "Correu", + "email-required": "Cal correu.", + "invalid-email-format": "Format del correu no vàlid.", + "first-name": "Nom", + "last-name": "Cognom", + "description": "Descripció", + "default-dashboard": "Panell per defecte", + "always-fullscreen": "Sempre en pantalla completa", + "select-user": "Seleccionar usuari", + "no-users-matching": "No s'han trobat usuaris coincidint amb '{{entity}}' .", + "user-required": "Cal usuari", + "activation-method": "Mètode d'activació", + "display-activation-link": "Mostrar l'enllaç d'activació", + "send-activation-mail": "Enviar correu d'activació", + "activation-link": "Enllaç d'activació d'usuari", + "activation-link-text": "Per activar l'usuari, utilitza el següent enllaç: Activar Usuari :", + "copy-activation-link": "Copiar enllaç d'activació", + "activation-link-copied-message": "L'enllaç d'activació s'ha copiat al porta-retalls", + "selected-users": "{ count, plural, 1 {1 usuario} other {# usuaris} } seleccionats", + "search": "Buscar usuaris", + "details": "Detalls", + "login-as-tenant-admin": "Iniciar sessió com a Administrador Propietari", + "login-as-customer-user": "Iniciar sessió com a Usuari Client", + "select-group-to-add": "Seleccioneu el grup objectiu per afegir usuaris seleccionats", + "select-group-to-move": "Seleccioneu el grup objectiu per moure els usuaris seleccionats", + "remove-users-from-group": "Esteu segur que voleu eliminar-lo { count, plural, 1 {1 user} other {# users} } del grup '{{entityGroup}}'?", + "group": "Grup d'usuaris", + "list-of-groups": "{ count, plural, 1 {One user group} other {List of # user groups} }", + "group-name-starts-with": "Grups d'usuaris els noms dels quals comencen per '{{prefix}}'", + "disable-account": "Deshabilitar compte d'usuari", + "enable-account": "Habilitar compte d'usuari", + "enable-account-message": "El compte d'usuari s'ha habilitat correctament!", + "disable-account-message": "El compte d'usuari s'ha deshabilitat correctament!!", + "copyId": "Copia l'ID de l'usuari", + "idCopiedMessage": "L'identificador d'usuari s'ha copiat al porta-retalls" + }, + "value": { + "type": "Tipus de valor", + "string": "Cadena de text", + "string-value": "Valor de cadena de text", + "string-value-required": "Cal valor de cadena de text", + "integer": "Número enter", + "integer-value": "Valor del número enter", + "integer-value-required": "Cal valor enter", + "invalid-integer-value": "Valor de enter invàlid", + "double": "Número decimal", + "double-value": "Valor número decimal", + "double-value-required": "Cal valor número decimal", + "boolean": "Booleà", + "boolean-value": "Valor booleà", + "false": "Fals", + "true": "Verdader", + "long": "Número llarg", + "json": "JSON", + "json-value": "Valor JSON", + "json-value-invalid": "El valor JSON té un format invàlid", + "json-value-required": "Cal valor JSON" + }, + "version-control": { + "version-control": "Control de versió", + "management": "Administrador de versions", + "branch": "Branca", + "default": "Per defecte", + "select-branch": "Seleccionar branca", + "branch-required": "Cal branca", + "create-entity-version": "Versió creació important", + "version-name": "Nom de versió", + "version-name-required": "Cal nom de versió", + "author": "Autor", + "export-relations": "Exportar relacions", + "export-attributes": "Exportar atributs", + "export-credentials": "Exportar credencials", + "entity-versions": "Versions d'entitat", + "versions": "Versions", + "created-time": "Hora de creació", + "version-id": "ID de versió", + "no-entity-versions-text": "No s'han trobat versions d'entitat", + "no-versions-text": "No s'han trobat versions", + "copy-full-version-id": "Copiar el ID de versió", + "create-version": "Crear versió", + "nothing-to-commit": "No hi ha canvis a publicar", + "restore-version": "Restaurar versió", + "restore-entity-from-version": "Restaurar entitat des de versió '{{versionName}}'", + "load-relations": "Cargar relacions", + "load-attributes": "Cargar atributs", + "load-credentials": "Cargar credencials", + "diff-entity-with-version": "Diff de la versió de entitat '{{versionName}}'", + "previous-difference": "Anterior diferència", + "next-difference": "Següent diferència", + "current": "Actual", + "differences": "{ count, plural, 1 {1 diferencia} other {# diferencias} }", + "create-entities-version": "Crear versió d'entitats", + "default-sync-strategy": "Estratègia de sincronització per defecte", + "sync-strategy-merge": "Fusionar (Merge)", + "sync-strategy-overwrite": "Sobreescriure", + "entities-to-export": "Entitats a exportar", + "entities-to-restore": "Entitats a restaurar", + "sync-strategy": "Estratègia de sincronizació", + "all-entities": "Totes les entitats", + "no-entities-to-export-prompt": "Si us plau, especifica les entitats a exportar", + "no-entities-to-restore-prompt": "Si us plau, especifica les entitats a restaurar", + "add-entity-type": "Afegir tipus d'entitat", + "remove-all": "Borrar tot", + "version-create-result": "{ added, plural, 0 {Ninguna entidad} 1 {1 entidad} other {# entidades} } afegides.
                { modified, plural, 0 {Ninguna entidad} 1 {1 entidad} other {# entidades} } modificades.
                { removed, plural, 0 {Ninguna entidad} 1 {1 entidad} other {# entidades} } esborrades.", + "remove-other-entities": "Esborrar altres entitats", + "find-existing-entity-by-name": "Buscar entitat existent per nom", + "restore-entities-from-version": "Restaurar entitats des de la versió '{{versionName}}'", + "no-entities-restored": "No es van restaurar entitats", + "created": "{{created}} creades", + "updated": "{{updated}} actualitzades", + "deleted": "{{deleted}} esborrades", + "remove-other-entities-confirm-text": "Atenció! Aquesta acció esborrarà permanentment todas les entitats actuals
                no presents a la versió a restaurar.

                Escriu eliminar altres entitats per confirmar.", + "auto-commit-to-branch": "autopublicar a la branca {{ branch }}", + "default-create-entity-version-name": "{{entityName}} actualizació", + "sync-strategy-merge-hint": "Crea o actualitza les entitats seleccionades al repositori. Les altres entitats no seran modificades.", + "sync-strategy-overwrite-hint": "Crea o actualitza les entitats seleccionades al repositori. Les altres entitats seran esborrades.", + "device-credentials-conflict": "Fallada en carregar el dispositiu amb ID extern {{entityId}}
                pel fet que les mateixes credencials estan ja presents a la base de dades per a un altre dispositiu.
                Si us plau, considereu desactivar l'ajustament carregar credencials al formulari de restauració.", + "missing-referenced-entity": "Fallada en carregar {{sourceEntityTypeName}} amb ID extern {{sourceEntityId}}
                perquè fa referència al tipus d'entitat {{targetEntityTypeName}} amb el ID {{targetEntityId}}." + }, + "widget": { + "widget-library": "Bibloteca de Widgets", + "widget-bundle": "Paquets de Widgets", + "all-bundles": "Tots els paquets", + "select-widgets-bundle": "Seleccionar paquet de widgets", + "management": "Gestió de Widgets", + "editor": "Editor de widgets", + "widget-type-not-found": "Problema al carregar la configuració del widget.
                Probablement associat\n El tipus de widget ha estat eliminat.", + "widget-type-load-error": "El widget no ha estat carregat degut a aquests errors:", + "remove": "Eliminar widget", + "edit": "Editar widget", + "remove-widget-title": "Eliminar el widget '{{widgetTitle}}'?", + "remove-widget-text": "Atenció, després de la confirmació el widget serà eliminat i tota la informació relacionada serà irrecuperable.", + "timeseries": "Sèries del temps", + "search-data": "Buscar dades", + "no-data-found": "No s'han trobat dades", + "latest": "Últims valors", + "rpc": "Widget de control", + "alarm": "Widget d'alarma", + "static": "Widget estàtic", + "select-widget-type": "Seleccionar tipus de widget", + "missing-widget-title-error": "El titul del widget ha de ser especificat!", + "widget-saved": "Widget guardat", + "unable-to-save-widget-error": "Impossible guardar widget! Té errors!", + "save": "Guardar widget", + "saveAs": "Guardar widget com", + "save-widget-type-as": "Guardar tipus de widget com", + "save-widget-type-as-text": "Per favor, introduïu un nou títol i/o seleccioneu un paquet de destinació.", + "toggle-fullscreen": "Canviar a pantalla completa", + "run": "Ejecutar widget", + "title": "Títol", + "title-required": "Cal títol.", + "type": "Tipus", + "resources": "Recursos", + "resource-url": "URL JavaScript/CSS", + "resource-is-module": "És mòdul", + "remove-resource": "Eliminar recurs", + "add-resource": "Afegir recurs", + "html": "HTML", + "tidy": "Endreçat", + "css": "CSS", + "settings-schema": "Esquema de configuració", + "datakey-settings-schema": "Esquema de configuració de clau de dades", + "latest-datakey-settings-schema": "Esquema de darrers valors", + "widget-settings": "Configuració Widget", + "description": "Descripció", + "image-preview": "Vista prèvia de la imatge", + "settings-form-selector": "Selector de formulari de configuració", + "data-key-settings-form-selector": "Selector de formulari de configuració claus de dades", + "latest-data-key-settings-form-selector": "Selector formulari dels darrers valors", + "javascript": "Javascript", + "js": "JS", + "remove-widget-type-title": "Eliminar el tipus del widget '{{widgetName}}'?", + "remove-widget-type-text": "Atenció, després de la confirmació el tipus serà eliminat i la informació relacionada serà irrecuperable.", + "remove-widget-type": "Eliminar tipus de widget.", + "add-widget-type": "Afegir nou tipus de widget", + "widget-type-load-failed-error": "Error al carregar el tipus de widget!", + "widget-template-load-failed-error": "Error al carregar la plantilla del widget!", + "add": "Afegir Widget", + "undo": "Desfer canvis", + "export": "Exportar widget", + "export-data": "Exportar dades del widget", + "export-to-csv": "Exportar dades a CSV...", + "export-to-excel": "Exportar dades a XLS...", + "export-to-excel-xlsx": "Exportar dades a XLSX...", + "no-data": "No hi ha dades per mostrar en widget", + "data-overflow": "El widget mostra {{count}} de {{total}} entitats", + "alarm-data-overflow": "El widget mostra alarmes per {{allowedEntities}} entitats (màxim permès) de {{totalEntities}} entitats", + "search": "Cerca widget", + "filter": "Tipus de filtre Widget", + "loading-widgets": "S'estan carregant els widgets..." + }, + "widget-action": { + "header-button": "Botó d'encapçalats widget", + "open-dashboard-state": "Navegar a un nou estat de panell", + "update-dashboard-state": "Actualizar l'estat del panell actual", + "open-dashboard": "Navegar a un altre panell", + "custom": "Acció personalitzada", + "custom-pretty": "Acció personalitzada (amb plantilla HTML)", + "mobile-action": "Acció mòbil", + "target-dashboard-state": "Estat de panell de destí", + "target-dashboard-state-required": "Cal estat de panell de destí", + "set-entity-from-widget": "Establir entitat des de widget", + "target-dashboard": "Panell de destí", + "open-right-layout": "Obrir disseny de panell (Dret)(vista móvil)", + "state-display-type": "Opció de visualització de l'estat del tauler", + "open-normal": "Normal", + "open-in-separate-dialog": "Obrir en un diàleg separat", + "open-in-popover": "Obre en popover", + "dialog-title": "Títol del diàleg", + "dialog-hide-dashboard-toolbar": "Ocultar barra d'eines en el diàleg", + "dialog-width": "Ample de diàleg en percentatge relatiu a l'ample del viewport", + "dialog-height": "Alt de diàleg en percentatge relatiu a l'alt del viewport", + "dialog-size-range-error": "La grandària del diàleg ha de ser entre un rang de 1 a 100", + "popover-preferred-placement": "Col·locació popover preferida", + "popover-placement-top": "Superior", + "popover-placement-topLeft": "Superior esquerra", + "popover-placement-topRight": "Superior dret", + "popover-placement-right": "Dret", + "popover-placement-rightTop": "Dret superior", + "popover-placement-rightBottom": "Dret inferior", + "popover-placement-bottom": "Bottom", + "popover-placement-bottomLeft": "Bottom esquerra", + "popover-placement-bottomRight": "Bottom dret", + "popover-placement-left": "Esquerra", + "popover-placement-leftTop": "Esquerra superior", + "popover-placement-leftBottom": "Esquerra inferior", + "popover-hide-on-click-outside": "Amaga la finestra emergent al clic exterior", + "popover-hide-dashboard-toolbar": "Amaga la barra d'eines del tauler a la finestra emergent", + "popover-width": "Amplada emergent en unitats del navegador (p. ex. 100 píxels, 25 vw)", + "popover-height": "Alçada emergent en unitats del navegador (p. ex. 100px, 25vh)", + "popover-style": "Estil popover", + "open-new-browser-tab": "Obrir en una nova pestanya", + "mobile": { + "action-type": "Tipus d'acció mòbil", + "action-type-required": "Cal el tipus d'acció mòbil", + "take-picture-from-gallery": "Fer una foto de la galeria", + "take-photo": "Fer foto", + "map-direction": "Obriu les indicacions del mapa", + "map-location": "Obre la ubicació del mapa", + "scan-qr-code": "Escaneja el codi QR", + "make-phone-call": "Fes una trucada telefònica", + "get-location": "Obteniu la ubicació del telèfon", + "take-screenshot": "Fes una captura de pantalla" + } + }, + "widgets-bundle": { + "current": "Paquet actual", + "widgets-bundles": "Paquet de Widgets", + "add": "Afegir paquet de widgets", + "delete": "Eliminar paquet de widgets", + "title": "Títol", + "title-required": "Cal títol.", + "title-max-length": "El títol ha de ser inferior a 256", + "description": "Descripció", + "image-preview": "Vista prèvia de la imatge", + "add-widgets-bundle-text": "Afegir nou paquet de widgets", + "no-widgets-bundles-text": "Cap paquet de widgets trobat", + "empty": "Paquet de widgets buit.", + "details": "Detalls", + "widgets-bundle-details": "Detalls del paquet de Widgets", + "delete-widgets-bundle-title": "Eliminar el paquet de widgets '{{widgetsBundleTitle}}'?", + "delete-widgets-bundle-text": "Atenció, després de la confirmació tots els paquets seleccionats seran eliminats i la seva informació relacionada serà irrecuperable.", + "delete-widgets-bundles-title": "Eliminar { count, plural, 1 {1 paquet de widgets} other {# paquetes de widgets} }?", + "delete-widgets-bundles-action-title": "Eliminar { count, plural, 1 {1 paquet de widgets} other {# paquetes de widgets} }", + "delete-widgets-bundles-text": "Atenció, després de la confirmació tots els paquets seleccionats seran eliminats i la informació relacionada serà irrecuperable.", + "no-widgets-bundles-matching": "Cap paquet '{{widgetsBundle}}' trobat.", + "widgets-bundle-required": "Cal paquet de widget.", + "system": "Widget de Sistema", + "import": "Importar paquet de widgets", + "export": "Exportar paquet de widgets", + "export-failed-error": "Impossible exportar paquet de widgets: {{error}}", + "create-new-widgets-bundle": "Crear nou paquet de widgets", + "widgets-bundle-file": "Arxiu de paquet de widgets", + "invalid-widgets-bundle-file-error": "Impossible importar paquet de widgets: Estructura de dades invàlida.", + "search": "Buscar paquet de widgets", + "selected-widgets-bundles": "{ count, plural, 1 {1 paquet de widgets} other {# paquetes de widgets} } seleccionats", + "open-widgets-bundle": "Obrir paquet de widgets", + "loading-widgets-bundles": "S'estan carregant paquets de widgets..." + }, + "widget-config": { + "data": "Dades", + "settings": "Configuració", + "advanced": "Avançat", + "title": "Títol", + "title-tooltip": "Tooltip Títol", + "general-settings": "Configuració generals", + "display-title": "Mostrar títol", + "drop-shadow": "Ombra", + "enable-fullscreen": "Habilitar pantalla completa", + "enable-data-export": "Habilitar exportació de dades", + "background-color": "Color de fons", + "text-color": "Color del text", + "padding": "Farciment", + "margin": "Marge", + "widget-style": "Estil de widget", + "widget-css": "Widget CSS", + "title-style": "Estil de títol", + "mobile-mode-settings": "Configuració móvil.", + "order": "Orde", + "height": "Alçada", + "mobile-hide": "Amaga el widget en mode mòbil", + "units": "Caràcter especial a mostrar al següent valor", + "decimals": "Números de dígits després de la coma", + "timewindow": "Finestra del temps", + "use-dashboard-timewindow": "Utilitzar finestra del temps del panell", + "display-timewindow": "Mostrar finestra del temps", + "legend": "Llegenda", + "display-legend": "Mostrar llegenda", + "datasources": "Set de dades", + "maximum-datasources": "Un màxim de { count, plural, 1 {1 set de dades és permès.} other {# set de dades són permitidos} }", + "datasource-type": "Tipus", + "datasource-parameters": "Paràmetres", + "remove-datasource": "Eliminar set de dades", + "add-datasource": "Afegir set de dades", + "target-device": "Dispositiu destí", + "alarm-source": "Origen de alarmes", + "actions": "Accions", + "action": "Acció", + "add-action": "Afegir acció", + "search-actions": "Buscar accions", + "no-actions-text": "No s'han trobar accions", + "action-source": "Origen de acció", + "action-source-required": "Cal origen de acció.", + "action-name": "Nom", + "action-name-required": "Cal nom de acció.", + "action-name-not-unique": "Existe una acció amb el mateix nom.
                El nom d'acció ha de ser únic dins de la mateixa font d'acció (origen).", + "action-icon": "Icona", + "show-hide-action-using-function": "Mostra/amaga l'acció mitjançant la funció", + "action-type": "Tipus", + "action-type-required": "Cal tipus d'acció.", + "edit-action": "Editar acció", + "delete-action": "Esborrar acció", + "delete-action-title": "Esborrar acció de widget", + "delete-action-text": "Eliminar l'acció de widget amb el nom '{{actionName}}'?", + "title-icon": "Icona del títol", + "display-icon": "Mostrar icona del títol", + "icon-color": "Color de l'icona", + "icon-size": "Mida de l'icona", + "advanced-settings": "Configuració avançada", + "data-settings": "Configuració de dades", + "no-data-display-message": "\"No hi ha dades per mostrar\" missatge alternatiu", + "data-page-size": "Nombre màxim d'entitats per font de dades", + "settings-component-not-found": "Component de configuració no trobat per al selector '{{selector}}'" + }, + "widget-type": { + "import": "Importar tipus de widget", + "export": "Exportar tipus de widget", + "export-failed-error": "Impossible exportar tipus de widget: {{error}}", + "create-new-widget-type": "Crear nou tipus de widget", + "widget-type-file": "Arxiu de tipus de widget", + "invalid-widget-type-file-error": "No es pot importar tipus de widget: Estructura de dades del tipus de widget és invàlida." + }, + "solution-template": { + "solution-template": "Plantilla de solució", + "solution-templates": "Plantilles de solució", + "management": "Gestionar plantilles de solució", + "details": "Detalls", + "install": "Instal·lar", + "level": "Nivell", + "install-title": "La plantilla de solució s'ha instal·lat correctament", + "install-failed-title": "La instal·lació de la plantilla de solució ha fallat", + "instructions": "Instruccions", + "goto-main-dashboard": "Vés al tauler principal", + "delete": "Suprimeix", + "delete-solution-title": "Esteu segur que voleu suprimir la solució '{{solutionTitle}}'?", + "delete-solution-text": "Aneu amb compte, després de la confirmació, la solució i totes les dades relacionades es tornaran irrecuperables.", + "installing": "Instal·lació de la plantilla de solució..." + }, + "markdown": { + "edit": "Edita", + "preview": "Vista prèvia", + "copy-code": "Feu clic per copiar", + "copied": "Copiat!" + }, + "white-labeling": { + "white-labeling": "Etiquetatge Blanc", + "login-white-labeling": "Iniciar sessió de Etiquetado Blanco", + "preview": "Avanç", + "app-title": "Títol de l'aplicació", + "favicon": "Icona del lloc WEB", + "favicon-description": "Imatge *.ico, *.gif o *.png amb grandària màxima {{kbSize}} KBytes.", + "favicon-size-error": "Imatge del lloc web és molt gran. Mesura màxima permesa de la imatge del lloc web {{kbSize}} KBytes.", + "favicon-type-error": "Formato de arxiu de imagen de lloc WEB invàlid. Solamente es aceptan imágenes ICO, GIF o PNG.", + "drop-favicon-image": "Colocar la imagen del icona de un lloc WEB o haga clic per seleccionar un arxiu per carregar .", + "no-favicon-image": "Icona no seleccionat", + "logo": "Logo", + "logo-description": "Alguna imagen amb grandària màxima {{kbSize}} KBytes.", + "logo-size-error": "L'imatge del logo és molt gran. Mida màxima permesa de la imatge del logo {{kbSize}} KBytes.", + "logo-type-error": "Format darxiu del logotip invàlid. Només és accepten imatges.", + "drop-logo-image": "Colocar una imagen del logotipo o fes clic per seleccionar un arxiu per carregar .", + "no-logo-image": "Logo no seleccionat", + "logo-height": "Alçada del logo, píxel", + "primary-palette": "Paleta primària", + "accent-palette": "Paleta d'accents", + "customize-palette": "Personalitzar", + "advanced-css": "CSS avançat", + "edit-palette": "Editar paleta", + "save-palette": "Guardar paleta", + "primary-background": "Color de fons primari", + "secondary-background": "Color de fons secundari", + "hue1": "HUE 1", + "hue2": "HUE 2", + "hue3": "HUE 3", + "page-background-color": "Color de fons de la pàgina", + "dark-foreground": "Primer pla fosc", + "domain-name": "Nom del domini", + "base-url": "Base URL", + "base-url-required": "Cal la base URL.", + "prohibit-different-url": "Prohibir l'ús del nom d'amfitrió de les capçaleres de la sol·licitud del client", + "prohibit-different-url-hint": "Aquesta configuració s'hauria d'habilitar per als entorns de producció. Pot causar problemes de seguretat quan està desactivat", + "help-link-base-url": "URL base dels enllaços d'ajuda", + "ui-help-base-url": "URL de la base d'ajuda de la IU", + "enable-help-links": "Activa els enllaços d'ajuda", + "error-verification-url": "Un nom de domini no ha de contenir els símbols '/' i ':'. Exemple: thingsboard.io", + "show-platform-name-version": "Mostra el nom i la versió de la plataforma", + "platform-name": "Nom de la plataforma", + "platform-version": "Versió de plataforma", + "version-mask": "{{name}} v.{{version}}", + "position": { + "label": "Nom de la plataforma i posició de la versió", + "under-logo": "Sota el logotip", + "bottom": "Part inferior del formulari d'inici de sessió" + } + }, + "widgets": { + "chart": { + "common-settings": "Configuració comuna", + "enable-stacking-mode": "Activar mode d'apilament (stacking)", + "line-shadow-size": "Grandària d'ombra (línia)", + "display-smooth-lines": "Mostra línies suaus (curvas)", + "default-bar-width": "Ample de barra per defecte per a dades no agregades (millisegundos)", + "bar-alignment": "Alineació de barres", + "bar-alignment-left": "Esquerra", + "bar-alignment-right": "Dreta", + "bar-alignment-center": "Centre", + "default-font-size": "Grandaria de la font per defecte", + "default-font-color": "Color de la font per defecte", + "thresholds-line-width": "Ample de línia per defecte per a tots els llindars", + "tooltip-settings": "Ajustaments de suggeriments (tooltip)", + "show-tooltip": "Mostra suggeriments", + "hover-individual-points": "Passa el cursor sobre punts individuals", + "show-cumulative-values": "Mostrar valors acumulats en mode apilat", + "hide-zero-false-values": "Amagar valors zero/false en suggeriments", + "tooltip-value-format-function": "Funció de formateig de valors en suggeriments (tooltip)", + "grid-settings": "Configuració de la graella", + "show-vertical-lines": "Mostrar línies verticales", + "show-horizontal-lines": "Mostrar línies horizontales", + "grid-outline-border-width": "Ample de quadrícula/contorn a px", + "primary-color": "Color primari", + "background-color": "Color de fons", + "ticks-color": "Color de ticks", + "xaxis-settings": "Configuració eix X", + "axis-title": "Títol d'eix", + "xaxis-tick-labels-settings": "Configuració d'etiquetes en ticks eix X", + "show-tick-labels": "Mostrar etiquetes en ticks", + "yaxis-settings": "Configuració eix Y", + "min-scale-value": "Valor mínim en l'escala", + "max-scale-value": "Valor máxim en l'escala", + "yaxis-tick-labels-settings": "Configuració 'etiquetes en ticks eix Y", + "tick-step-size": "Grandaria de pas entre ticks", + "number-of-decimals": "Número de decimals", + "ticks-formatter-function": "Funció de formateig de ticks", + "comparison-settings": "Configuració de comparació", + "enable-comparison": "Activar comparació", + "time-for-comparison": "Període de comparació", + "time-for-comparison-previous-interval": "Interval anterior (per defecte)", + "time-for-comparison-days": "Fa un dia", + "time-for-comparison-weeks": "Fa una setmana", + "time-for-comparison-months": "Fa un mes", + "time-for-comparison-years": "Fa un any", + "time-for-comparison-custom-interval": "Interval personalitzat", + "custom-interval-value": "Valor d'interval personalitzat (ms)", + "comparison-x-axis-settings": "Configuració comparació de l'eix X", + "axis-position": "Posició eix", + "axis-position-top": "Superior (por defecto)", + "axis-position-bottom": "Inferior", + "custom-legend-settings": "Configuració llegenda", + "enable-custom-legend": "Activar llegenda personalitzada (permet poder utilitzar valors d'atributs/series temporals a les etiquetes)", + "key-name": "Nom clau", + "key-name-required": "Cal nom clau", + "key-type": "Tipus clau", + "key-type-attribute": "Atribut", + "key-type-timeseries": "Sèries temporals", + "label-keys-list": "Llista de claus per utilitzar a etiquetes", + "no-label-keys": "No hi ha claus configurades", + "add-label-key": "Afegir nova clau", + "line-width": "Ample de línia", + "color": "Color", + "data-is-hidden-by-default": "Ocultar dades per defecte", + "disable-data-hiding": "Desactivar ocultació de dades", + "remove-from-legend": "Treure clau de la llegenda", + "exclude-from-stacking": "Excloure del mode apilat(disponible en el mode \"Apilat\")", + "line-settings": "Configuració de línia", + "show-line": "Mostrar línia", + "fill-line": "Omplir línia", + "points-settings": "Configuració de punts", + "show-points": "Mostrar punts", + "points-line-width": "Ample de línia en punts", + "points-radius": "Radi dels punts", + "point-shape": "Forma del punt", + "point-shape-circle": "Cercle", + "point-shape-cross": "Creu", + "point-shape-diamond": "Diamant", + "point-shape-square": "Quadrat", + "point-shape-triangle": "Triangle", + "point-shape-custom": "Funció personalitzada", + "point-shape-draw-function": "Funció de forma del punt", + "show-separate-axis": "Mostrar eix separat", + "axis-position-left": "Esquerra", + "axis-position-right": "Dreta", + "thresholds": "Llindars", + "no-thresholds": "No hi ha llindars configurats", + "add-threshold": "Afegir nou llindar", + "show-values-for-comparison": "Mostrar valors històrics per la seva comparació", + "comparison-values-label": "Etiqueta de valors històrics", + "threshold-settings": "Configuració de llindars", + "use-as-threshold": "Usar valor de clau com a llindar", + "threshold-line-width": "Ample de línia (per a llindar)", + "threshold-color": "Color llindar", + "common-pie-settings": "Configuració comunes diagrama de sectors", + "radius": "Radi", + "inner-radius": "Radi interior", + "tilt": "Inclinació", + "stroke-settings": "Configuració de traç", + "width-pixels": "Ample (pixels)", + "show-labels": "Mostrar etiquetes", + "animation-settings": "Configuració de animació", + "animated-pie": "Activar animació (experimental)", + "border-settings": "Configuració de vores", + "border-width": "Ancho de la vora", + "border-color": "Color de la vora", + "legend-settings": "Configuració de llegenda", + "display-legend": "Mostrar llegenda", + "labels-font-color": "Color de text en llegenda" + }, + "dashboard-state": { + "dashboard-state-settings": "Configuració estat del panell", + "dashboard-state": "Id del estat del panell", + "autofill-state-layout": "Autoremplenar alçada per defecte (obtinguda de l'estat)", + "default-margin": "Marge entre widgets per defecte", + "default-background-color": "Color de fons per defecte", + "sync-parent-state-params": "Sincronitzar paràmetres d'estat amb el panell pare" + }, + "date-range-navigator": { + "date-range-picker-settings": "Ajustaments del selector de dates", + "hide-date-range-picker": "Esmagar el selector de dades", + "picker-one-panel": "Selector de dates per a un panell", + "picker-auto-confirm": "Auto-confirmar al selector de dates", + "picker-show-template": "Mostra plantilla de selector de dates", + "first-day-of-week": "Primer dia de la setmana", + "interval-settings": "Paràmetres d'interval", + "hide-interval": "Ocultar interval", + "initial-interval": "Interval inicial", + "interval-hour": "Hora", + "interval-day": "Dia", + "interval-week": "Setmana", + "interval-two-weeks": "2 setmanes", + "interval-month": "Mes", + "interval-three-months": "3 mesos", + "interval-six-months": "6 mesos", + "step-settings": "Paràmetres de pas", + "hide-step-size": "Amagar mida de pas", + "initial-step-size": "Mida de pas inicial", + "hide-labels": "Ocultar etiquetes", + "use-session-storage": "Utilitzar emmagatzematge de sessió", + "localizationMap": { + "Sun": "Dg.", + "Mon": "Dl.", + "Tue": "Dm.", + "Wed": "Dc.", + "Thu": "Dj.", + "Fri": "Dv.", + "Sat": "Ds.", + "Jan": "Gen.", + "Feb": "Feb.", + "Mar": "Mar.", + "Apr": "Abr.", + "May": "Mai.", + "Jun": "Jun.", + "Jul": "Jul.", + "Aug": "Ago.", + "Sep": "Sept.", + "Oct": "Oct.", + "Nov": "Nov.", + "Dec": "Des.", + "January": "Gener", + "February": "Febrer", + "March": "Març", + "April": "Abril", + "June": "Juny", + "July": "Juliol", + "August": "Agost", + "September": "Setembre", + "October": "Octubre", + "November": "Novembre", + "December": "Desembre", + "Custom Date Range": "Interval de dates personalitzat", + "Date Range Template": "Plantilla de rang de dates", + "Today": "Avui", + "Yesterday": "Ahir", + "This Week": "Aquesta setmana", + "Last Week": "La setmana passada", + "This Month": "Aquest mes", + "Last Month": "El mes passat", + "Year": "Any", + "This Year": "Aquest any", + "Last Year": "Últim", + "Date picker": "Selector de data", + "Hour": "Hora", + "Day": "Dia", + "Week": "Setmana", + "2 weeks": "2 Setmanes", + "Month": "Mes", + "3 months": "3 Mesos", + "6 months": "6 Mesos", + "Custom interval": "Interval personalitzat", + "Interval": "Interval", + "Step size": "Nombre de passos", + "Ok": "Ok" + } + }, + "entities-hierarchy": { + "hierarchy-data-settings": "Configuració de dades de jerarquia", + "relations-query-function": "Funció d'obtenció de relacions", + "has-children-function": "El node té una funció filla", + "node-state-settings": "Paràmetres estat node", + "node-opened-function": "Funció per defecte en obrir node", + "node-disabled-function": "Funció amb node desactivat", + "display-settings": "Mostrar ajustaments", + "node-icon-function": "Funció d'icona node", + "node-text-function": "Funció de text node", + "sort-settings": "Paràmetres d'ordenació", + "nodes-sort-function": "Funció d'ordenació" + }, + "edge": { + "display-default-title": "Mostrar títol per defecte" + }, + "gateway": { + "general-settings": "Ajustaments generals", + "widget-title": "Títol del widget", + "default-archive-file-name": "Nom del fitxer per defecte", + "device-type-for-new-gateway": "Tipus de dispositiu per a nou gateway", + "messages-settings": "Paràmetres de missatges", + "save-config-success-message": "Missatge d'èxit enregistrant la configuració", + "device-name-exists-message": "Missatge de text quan el nom del dispositiu ja existeixi", + "gateway-title": "Formulari Gateway", + "read-only": "Només lectura", + "events-title": "Títol del formulari d'esdeveniments", + "events-filter": "Filtre d'esdeveniments", + "event-key-contains": "La clau d'esdeveniment conté..." + }, + "gauge": { + "default-color": "Color per defecte", + "radial-gauge-settings": "Paràmetres d'indicador radial", + "ticks-settings": "Ajustaments de ticks", + "min-value": "Valor mínim", + "max-value": "Valor màxim", + "start-ticks-angle": "Angle d'inici ticks", + "ticks-angle": "Angle Ticks", + "major-ticks-count": "Nombre de ticks principals", + "major-ticks-color": "Color Núm. de ticks principals", + "minor-ticks-count": "Nombre de ticks secundaris", + "minor-ticks-color": "Color Núm. de ticks secundaris", + "tick-numbers-font": "Font del tick", + "unit-title-settings": "Paràmetres d'unitat (títol)", + "show-unit-title": "Mostra unitats (títol)", + "unit-title": "Títol d'unitat", + "title-font": "Font de text del títol", + "units-settings": "Ajustaments d'unitats", + "units-font": "Font de text de les unitats", + "value-box-settings": "Paràmetres de valor", + "show-value-box": "Mostrar valor", + "value-int": "Nombre de dígits per a la part sencera del valor", + "value-font": "Font de text del valor", + "value-box-rect-stroke-color": "Color del traç del rectangle (valor)", + "value-box-rect-stroke-color-end": "Color del traç del rectangle (valor) - gradient final", + "value-box-background-color": "Color de fons del valor", + "value-box-shadow-color": "Color d'ombra del valor", + "plate-settings": "Ajustaments de la placa", + "show-plate-border": "Mostra vora de la placa", + "plate-color": "Color de la placa", + "needle-settings": "Paràmetres de l'agulla", + "needle-circle-size": "Mida del cercle de l'agulla", + "needle-color": "Color de l'agulla", + "needle-color-end": "Color de l'agulla - gradient final", + "needle-color-shadow-up": "Color d'ombreig de la meitat superior de l'agulla", + "needle-color-shadow-down": "Color d'ombra de l'agulla", + "highlights-settings": "Ajustaments de ressalt", + "highlights-width": "Amplada del ressalt", + "highlights": "Resalts", + "highlight-from": "De", + "highlight-to": "A", + "highlight-color": "Color", + "no-highlights": "No hi ha ressalts configurats", + "add-highlight": "Afegir ressalt", + "animation-settings": "Paràmetres d'animació", + "enable-animation": "Activar animació", + "animation-duration": "Duració d'animació", + "animation-rule": "Regla d'animació", + "animation-linear": "Lineal", + "animation-quad": "Quad", + "animation-quint": "Quint", + "animation-cycle": "Cicle (cycle)", + "animation-bounce": "Rebot (bounce)", + "animation-elastic": "Elastic", + "animation-dequad": "Dequad", + "animation-dequint": "Dequint", + "animation-decycle": "Decycle", + "animation-debounce": "Debounce", + "animation-delastic": "Delastic", + "linear-gauge-settings": "Paràmetres d'indicador lineal", + "bar-stroke-width": "Amplada del traç", + "bar-stroke-color": "Color del traç", + "bar-background-color": "Color de fons de l'indicador", + "bar-background-color-end": "Color de fons de l'indicador - gradient final", + "progress-bar-color": "Color de la barra de progrés", + "progress-bar-color-end": "Color de la barra de progrés - gradient final", + "major-ticks-names": "Nom de ticks principals", + "show-stroke-ticks": "Mostrar traç de ticks", + "major-ticks-font": "Font de ticks principals", + "border-color": "Color de la vora", + "border-width": "Amplada de la vora", + "needle-circle-color": "Color del cercle de l'agulla", + "animation-target": "Destinació de l'animació", + "animation-target-needle": "Agulla", + "animation-target-plate": "Placa", + "common-settings": "Ajustaments comuns de l'indicador", + "gauge-type": "Tipus d'indicador", + "gauge-type-arc": "Arc", + "gauge-type-donut": "Donut", + "gauge-type-horizontal-bar": "Barra horitzontal", + "gauge-type-vertical-bar": "Barra vertical", + "donut-start-angle": "Angle d'inici", + "bar-settings": "Paràmetres de barra indicadora", + "relative-bar-width": "Amplada relativa", + "neon-glow-brightness": "Efecte brillantor de neó, (0-100), 0 - desactiva efecte", + "stripes-thickness": "Grossor de les ratlles, 0 - sense ratlles", + "rounded-line-cap": "Mostra tapa de línia arrodonida", + "bar-color-settings": "Paràmetres de color de barra", + "use-precise-level-color-values": "Usar nivells precisos de color", + "bar-colors": "Colors de la barra, del més baix al més alt", + "color": "Color", + "no-bar-colors": "No hi ha colors configurats", + "add-bar-color": "Afegir color", + "from": "De", + "to": "A", + "fixed-level-colors": "Colors de barra usant valors límit", + "gauge-title-settings": "Paràmetres de títol de l'indicador", + "show-gauge-title": "Mostra títol d'indicador", + "gauge-title": "Títol d'indicador", + "gauge-title-font": "Font del títol de l'indicador", + "unit-title-and-timestamp-settings": "Paràmetres del títol d'unitats i timestamp", + "show-timestamp": "Mostrar valor timestamp", + "timestamp-format": "Format timestamp", + "label-font": "Font de l'etiqueta que es mostra sota el valor", + "value-settings": "Paràmetres del valor", + "show-value": "Mostra text del valor", + "min-max-settings": "Etiqueta mínim/màxim", + "show-min-max": "Mostra valors mínims i màxims", + "min-max-font": "Font dels valors mínim i màxim", + "show-ticks": "Mostrar ticks", + "tick-width": "Amplada de tick", + "tick-color": "Color de tick", + "tick-values": "Valors de tick", + "no-tick-values": "No hi ha valors configurats", + "add-tick-value": "Afegir valor de tick" + }, + "gpi": { + "pin": "Pin", + "label": "Etiqueta", + "row": "Fila", + "column": "Columna", + "color": "Color", + "panell-settings": "Paràmetres de panell", + "background-color": "Color de fons", + "gpio-switches": "switches GPIO", + "no-gpio-switches": "No hi ha switches GPIO configurats", + "add-gpio-switch": "Afegeix switch GPIO", + "gpio-status-request": "Sol·licitud estat GPIO", + "method-name": "Nom del mètode RPC", + "method-body": "Cos del mètode RPC", + "gpio-status-change-request": "Sol·licitud de canvi d'estat GPIO", + "parse-gpio-status-function": "Funció de parsejat d'estat GPIO", + "gpio-leds": "Leds GPIO", + "no-gpio-leds": "No hi ha Leds GPIO configurats", + "add-gpio-led": "Afegir led GPIO" + }, + "html-card": { + "html": "HTML", + "css": "CSS" + }, + "input-widgets": { + "attribute-not-allowed": "El paràmetre d'atribut no es pot utilitzar aquest widget", + "blocked-location": "La funció de geolocalització està bloquejada al vostre navegador", + "claim-device": "Reclamar dispositiu", + "claim-failed": "Error al reclamar dispositiu!", + "claim-not-found": "Dispositiu no trobat!", + "claim-successful": "Dispositiu reclamat correctament!", + "date": "Data", + "device-name": "Nom del dispositiu", + "device-name-required": "Cal nom de dispositiu", + "discard-changes": "Descartar els canvis", + "entity-attribute-required": "Cal atribut d'entitat", + "entity-coordinate-required": "Cal tots dos camps (latitud i longitud)", + "entity-timeseries-required": "Cal la sèrie del temps de l'entitat", + "get-location": "Obtenir localització actual", + "invalid-date": "Data invàlida", + "latitude": "Latitud", + "longitude": "Longitud", + "min-value-error": "El valor mínim és {{value}}", + "max-value-error": "El valor màxim és {{value}}", + "not-allowed-entity": "La entitat seleccionada no pot tenir atributs compartits", + "no-attribute-selected": "No s'ha seleccionat cap atribut", + "no-datakey-selected": "No s'ha seleccionar cap clau de dades", + "no-coordinate-specified": "No s'ha especificat la clau per latitud/longitud", + "no-entity-selected": "Ninguna entitat seleccionada", + "no-image": "Sense imatge", + "no-support-geolocation": "El teu navegador no suporta geolocalització", + "no-support-web-camera": "No hi ha càmera web compatible", + "enable-https-use-widget": "Per favor, activa HTTPS per poder utilitzar aquest widget", + "no-found-your-camera": "No és possible trobar la càmera", + "no-permission-camera": "Permís denegat per l'usuari / Aquesta pàgina no té permisos per utilitzar la càmera", + "no-timeseries-selected": "No hi ha series del temps seleccionades", + "secret-key": "Clau", + "secret-key-required": "Cal clau", + "switch-attribute-value": "Canviar el valor de l'atribut d'entitat", + "switch-camera": "Canviar de càmera", + "switch-timeseries-value": "Canviar el valor de la sèrie del temps de l'entitat", + "take-photo": "Prendre foto", + "time": "Temps", + "timeseries-not-allowed": "El paràmetre Sèries temporals no es pot utilitzar en aquest widget", + "update-failed": "Actualizació fallida", + "update-successful": "Actualizació exitosa", + "update-attribute": "Actualizar atribut", + "update-timeseries": "Actualizar series del temps", + "general-settings": "Ajustaments Generals", + "widget-title": "Títol del giny", + "claim-button-label": "Etiqueta del botó de reclamar", + "show-secret-key-field": "Mostrar el camp 'Clave Secreta'", + "labels-settings": "Paràmetres d'etiquetes", + "show-labels": "Mostrar etiquetes", + "device-name-label": "Etiqueta per al camp d'entrada 'Nom de dispositiu'", + "secret-key-label": "Etiqueta per al camp d'entrés 'Clave Secreta'", + "messages-settings": "Paràmetres de missatges", + "claim-device-success-message": "Missatge a mostrar quan el dispositiu s'hagi reclamat ok", + "claim-device-not-found-message": "Missatge a mostrar quan el dispositiu no estigui", + "claim-device-failed-message": "Missatge a mostrar quan es produeixi un error reclamant el dispositiu", + "claim-device-name-required-message": "Missatge d'error a mostrar amb 'Nom de dispositiu requerit'", + "claim-device-secret-key-required-message": "Missatge d'error a mostrar amb 'Clave Secreta requerida'", + "show-label": "Mostrar etiqueta", + "label": "Etiqueta", + "required": "Requerit", + "required-error-message": "Error a mostrar amb camp 'Requerit'", + "show-result-message": "Missatge per mostrar resultat", + "integer-field-settings": "Paràmetres de camps sencers", + "min-value": "Valor Mínim", + "max-value": "Valor Màxim", + "double-field-settings": "Paràmetres de camps tipus double", + "text-field-settings": "Paràmetres de camp tipus text", + "min-length": "Longitud mínima", + "max-length": "Longitud màxima", + "checkbox-settings": "Paràmetres de camp tipus checkbox", + "true-label": "Etiqueta checked", + "false-label": "Etiqueta unchecked", + "image-input-settings": "Paràmetres de camp tipus d'imatge", + "display-preview": "Mostrar previsualització", + "display-clear-button": "Mostrar botó d'esborrar", + "display-apply-button": "Mostrar botó d'aplicar", + "display-discard-button": "Mostrar botó de descartar", + "datetime-field-settings": "Paràmetres de camp tipus Data/Hora", + "display-time-input": "Mostra l'entrada d'hora", + "latitude-key-name": "Nom clau latitud", + "longitude-key-name": "Nom clau longitud", + "show-get-location-button": "Mostrar botó 'Obtenir localització actual'", + "use-high-accuracy": "Usar alta precisió", + "location-fields-settings": "Paràmetres de camps de localització", + "latitude-label": "Etiqueta per a latitud", + "longitude-label": "Etiqueta per a longitud", + "input-fields-alignment": "Alineat de camps d'entrada", + "input-fields-alignment-column": "Columna (per defecte)", + "input-fields-alignment-row": "Fila", + "latitude-field-required": "Camp latitud requerit", + "longitude-field-required": "Camp longitud requerit", + "attribute-settings": "Paràmetres d'atributs", + "widget-mode": "Mode del giny", + "widget-mode-update-attribute": "Actualitzar atribut", + "widget-mode-update-timeseries": "Actualitzar timeseries", + "attribute-scope": "Abast d'atributs", + "attribute-scope-server": "Atributs de servidor", + "attribute-scope-shared": "Atributs compartits", + "value-required": "Valor requerit", + "image-settings": "Paràmetres d'imatge", + "image-format": "Format d'imatge", + "image-format-jpeg": "JPEG", + "image-format-png": "PNG", + "image-format-webp": "WEBP", + "image-quality": "Qualitat d'imatge que utilitza compressió amb pèrdua com jpeg i webp", + "max-image-width": "Màxim ample d'imatge", + "max-image-height": "Màxim alt d'imatge", + "action-buttons": "Botons d'acció", + "show-action-buttons": "Mostra botons d'acció", + "update-all-values": "Actualitzar tots els valors, no només els modificats", + "save-button-label": "Etiqueta de botó 'GRAVAR'", + "reset-button-label": "Etiqueta de botó 'DESFER'", + "group-settings": "Paràmetres de grup", + "show-group-title": "Mostrar títol per al grup de camps relacionats amb diferents entitats", + "group-title": "Títol del grup", + "fields-alignment": "Alineat de camps", + "fields-alignment-row": "Fila (per defecte)", + "fields-alignment-column": "Columna", + "fields-in-row": "Nombre de camps a la fila", + "option-value": "Valor (escriu 'null' per crear una opció buida)", + "option-label": "Etiqueta", + "hide-input-field": "Amagar camp d'entrada", + "datakey-type": "Tipus de clau de dades", + "datakey-type-server": "Atribut de servidor (per defecte)", + "datakey-type-shared": "Atribut compartit", + "datakey-type-timeseries": "Timeseries", + "datakey-value-type": "Tipus de valor de la clau de dades", + "datakey-value-type-string": "String", + "datakey-value-type-double": "Double", + "datakey-value-type-integer": "Enter", + "datakey-value-type-boolean-checkbox": "Boolean (Checkbox)", + "datakey-value-type-boolean-switch": "Boolean (Switch)", + "datakey-value-type-date-time": "Data & Hora", + "datakey-value-type-date": "Data", + "datakey-value-type-time": "Hora", + "datakey-value-type-select": "Selecció", + "value-is-required": "Valor requerit", + "ability-to-edit-attribute": "Possibilitat d'editar atribut", + "ability-to-edit-attribute-editable": "Editable (per defecte)", + "ability-to-edit-attribute-disabled": "Desactivat", + "ability-to-edit-attribute-readonly": "Només lectura", + "disable-on-datakey-name": "Desactivar quan una altra clau de dades sigui false (especifiqueu el nom de clau de dades)", + "slide-toggle-settings": "Configuració del botó lliscant", + "slide-toggle-label-position": "Posició d'etiqueta", + "slide-toggle-label-position-after": "Després", + "slide-toggle-label-position-before": "Abans", + "select-options": "Seleccionar opcions", + "no-select-options": "No hi ha opcions configurades", + "add-select-option": "Afegir opció", + "numeric-field-settings": "Paràmetres de camps numèrics", + "step-interval": "Passos entre valors (interval)", + "error-messages": "Missatges d'erro", + "min-value-error-message": "Missatge d'error de 'Valor Mínim'", + "max-value-error-message": "Missatge d'error de 'Valor Màxim'", + "invalid-date-error-message": "Missatge d'error de 'Data Invàlida'", + "icon-settings": "Configuració d'icones", + "use-custom-icon": "Usar icona personalitzada", + "input-cell-icon": "Icona a mostrar abans del camp d'entrada", + "value-conversion-settings": "Paràmetres de conversió de valor", + "get-value-settings": "Paràmetres d'obtenció de valors", + "use-get-value-function": "Fer servir getValue", + "get-value-function": "Funció getValue", + "set-value-settings": "Paràmetres d'establiment de valors", + "use-set-value-function": "Usar funció setValue", + "set-value-function": "Funció setValue" + }, + "qr-code": { + "use-qr-code-text-function": "Utilitzar funció de text QR", + "qr-code-text-pattern": "Patró del codi QR (per ex. '${entityName} | ${keyName} - text addicional.')", + "qr-code-text-pattern-required": "Es requereix patró del codi QR.", + "qr-code-text-function": "Funció del codi QR" + }, + "label-widget": { + "label-pattern": "Patró", + "label-pattern-hint": "Ajuda: ex. 'Text ${keyName} unitats.' o ${#<key index>} ; unitats'", + "label-pattern-required": "Es requereix patró", + "label-position": "Posició (Percentatge relatiu al fons)", + "x-pos": "X", + "i-pos": "Y", + "background-color": "Color de fons", + "font-settings": "Paràmetres de font", + "background-image": "Imatge de fons", + "labels": "Etiquetes", + "no-labels": "No hi ha etiquetes configurades", + "add-label": "Afegir etiqueta" + }, + "navigation": { + "title": "Títol", + "navigation-path": "Ruta de navegació", + "filter-type": "Tipus de filtre", + "filter-type-all": "Tots els objectes", + "filter-type-include": "Incloure objectes", + "filter-type-exclude": "Excloure objectes", + "items": "Objectes", + "enter-urls-to-filter": "Especificar URLs a filtrar..." + }, + "persistent-table": { + "rpc-id": "RPC ID", + "message-type": "Tipus de missatge", + "method": "Mètode", + "params": "Paràmetres", + "created-time": "Temps creat", + "expiration-time": "Temps de caducitat", + "retries": "Reintents", + "status": "Estat", + "filter": "Filtre", + "refresh": "Actualització", + "add": "Cal afegeix RPC ", + "details": "Detalls", + "delete": "Suprimeix", + "delete-request-title": "Cal suprimir RPC persistent", + "delete-request-text": "Esteu segur que voleu suprimir la sol·licitud?", + "details-title": "Detalls RPC ID: ", + "additional-info": "Informació addicional", + "response": "Resposta", + "any-status": "Qualsevol estat", + "rpc-status-list": "Llista d'estats RPC", + "no-request-prompt": "No hi ha cap sol·licitud per mostrar", + "send-request": "Enviar sol·licitud", + "add-title": "Crea una sol·licitud RPC persistent", + "method-error": "Cal mètode.", + "timeout-error": "El valor mínim de temps d'espera és 5000 (5 segons).", + "white-space-error": "No es permet l'espai en blanc.", + "rpc-status": { + "QUEUED": "A LA CUA", + "SENT": "ENVIAT", + "DELIVERED": "LLIURAT", + "SUCCESSFUL": "ÈXIT", + "TIMEOUT": "TEMPS D'EXPLORACIÓ", + "EXPIRED": "CADUCAT", + "FAILED": "FALLADA" + }, + "rpc-search-status-all": "TOTS", + "message-types": { + "false": "Bidireccional", + "true": "Una direcció" + }, + "general-settings": "Ajustaments Generals", + "enable-filter": "Activar filtre", + "enable-sticky-header": "Mostrar encapçalat mentre es fa scroll", + "enable-sticky-action": "Mostra columna d'accions mentre es fa scroll", + "display-request-details": "Mostrar detalls de petició", + "allow-send-request": "Permetre enviar petició RPC", + "allow-delete-request": "Permetre esborrar petició", + "columns-settings": "Paràmetres de columnes", + "display-columns": "Columnes per mostrar", + "column": "Columna", + "no-columns-found": "No s'han trobat columnes", + "no-columns-matching": "'{{column}}' no trobada." + }, + "rpc": { + "value-settings": "Paràmetres de valor", + "initial-value": "Valor inicial", + "retrieve-value-settings": "Obtenir ajustaments valors on/off", + "retrieve-value-method": "Obtenir valor usant mètode", + "retrieve-value-method-none": "No obtenir", + "retrieve-value-method-rpc": "Mètode per a anomenada RPC d'obtenció de valor", + "retrieve-value-method-attribute": "Subscriure per atribut", + "retrieve-value-method-timeseries": "Subscriure per timeseries", + "attribute-value-key": "Clau d'atribut", + "timeseries-value-key": "Clau de timeseries", + "get-value-method": "Mètode per obtenir valor via RPC", + "parse-value-function": "Funció de parseig de valor", + "update-value-settings": "Paràmetres d'actualització de valor", + "set-value-method": "Mètode per establir valor via RPC", + "convert-value-function": "Funció de conversió", + "rpc-settings": "Paràmetres RPC", + "request-timeout": "Timeout de petició RPC (ms)", + "persistent-rpc-settings": "Paràmetres de RPC persistent", + "request-persistent": "Petició RPC persistent", + "persistent-polling-interval": "Interval de sondeig (ms) per obtenir resposta de l'ordre RPC persistent", + "common-settings": "Ajustaments comuns", + "switch-title": "Títol del switch", + "show-on-off-labels": "Mostrar etiquetes on/off", + "slide-toggle-label": "Etiqueta d'interruptor lliscant", + "label-position": "Posició d'etiqueta", + "label-position-before": "Abans", + "label-position-after": "Després", + "slider-color": "Color del botó lliscant", + "slider-color-primary": "Primari", + "slider-color-accent": "Accent", + "slider-color-warn": "Avís", + "button-style": "Estil de botó", + "button-raised": "Botó aixecat", + "button-primary": "Color primari", + "button-background-color": "Color de fons", + "button-text-color": "Color de text", + "widget-title": "Títol del giny", + "button-label": "Etiqueta del botó", + "device-attribute-scope": "Abast d'atributs del dispositiu", + "server-attribute": "Atributs de servidor", + "shared-attribute": "Atributs compartits", + "device-attribute-parameters": "Paràmetres d'atributs del dispositiu", + "is-one-way-command": "És una ordre d'una via (one way)", + "rpc-method": "Mètode RPC", + "rpc-method-params": "Paràmetres mètode RPC", + "show-rpc-error": "Mostra errors d'execució de RPC", + "led-title": "Títol LED", + "led-color": "Color LED", + "check-status-settings": "Paràmetres de comprovació d'estat", + "perform-rpc-status-check": "Realitzar comprovació del dispositiu RPC", + "retrieve-led-status-value-method": "Obtenir estat del led usant mètode", + "led-status-value-attribute": "Atribut del dispositiu que conté el valor de l'estat del led", + "led-status-value-timeseries": "Timeseries del dispositiu que conté el valor de l'estat del led", + "check-status-method": "Mètode RPC per revisar l'estat del dispositiu", + "parse-led-status-value-function": "Funció de parseig per a l'estat del led", + "knob-title": "Títol de comandament", + "min-value": "Valor mínim", + "max-value": "Valor màxim" + }, + "maps": { + "select-entity": "Seleccioneu l'entitat", + "select-entity-hint": "Consell: després de la selecció, feu clic al mapa per establir la posició", + "tooltips": { + "placeMarker": "Feu clic per col·locar-lo '{{entityName}}' entitat", + "firstVertex": "Polígon per '{{entityName}}': feu clic per col·locar el primer punt", + "firstVertex-cut": "Click to place first point", + "continueLine": "Polígon per '{{entityName}}': feu clic per continuar dibuixant", + "continueLine-cut": "Feu clic per continuar dibuixant", + "finishLine": "Feu clic a qualsevol marcador existent per acabar", + "finishPoly": "Polígon per '{{entityName}}': feu clic al primer marcador per acabar i desar", + "finishPoly-cut": "Feu clic al primer marcador per acabar i desar", + "finishRect": "Polígon per '{{entityName}}': feu clic per acabar i desar", + "startCircle": "Cercle per '{{entityName}}': feu clic per col·locar el centre del cercle", + "finishCircle": "Cercle per '{{entityName}}': feu clic per acabar el cercle", + "placeCircleMarker": "Feu clic per col·locar el marcador de cercle" + }, + "actions": { + "finish": "Acabar", + "cancel": "Cancel·lar", + "removeLastVertex": "Elimina l'últim punt" + }, + "buttonTitles": { + "drawMarkerButton": "Entitat de lloc", + "drawPolyButton": "Crea un polígon", + "drawLineButton": "Crear polilínia", + "drawCircleButton": "Crea un cercle", + "drawRectButton": "Crea un rectangle", + "editButton": "Mode d'edició", + "dragButton": "Mode arrossegar i deixar anar", + "cutButton": "Retalla l'àrea del polígon", + "deleteButton": "Eliminar", + "drawCircleMarkerButton": "Crea un marcador de cercle", + "rotateButton": "Gira el polígon" + }, + "map-provider-settings": "Paràmetres proveïdor de mapes", + "map-provider": "Proveïdor de mapes", + "map-provider-google": "Google maps", + "map-provider-openstreet": "OpenStreet maps", + "map-provider-here": "HERE maps", + "map-provider-image": "Mapa d'imatge", + "map-provider-tencent": "Tencent maps", + "openstreet-provider": "Proveïdor OpenStreet map", + "openstreet-provider-mapnik": "OpenStreetMap.Mapnik (Default)", + "openstreet-provider-hot": "OpenStreetMap.HOT", + "openstreet-provider-esri-street": "Esri.WorldStreetMap", + "openstreet-provider-esri-topo": "Esri.WorldTopoMap", + "openstreet-provider-cartodb-positron": "CartoDB.Positron", + "openstreet-provider-cartodb-dark-matter": "CartoDB.DarkMatter", + "use-custom-provider": "Usar proveïdor personalitzat", + "custom-provider-tile-url": "URL de proveïdor personalitzat", + "google-maps-api-key": "API Key Google Maps", + "default-map-type": "Tipus de mapa per defecte", + "google-map-type-roadmap": "Carretera", + "google-map-type-satelite": "Satelite", + "google-map-type-hybrid": "Híbrid", + "google-map-type-terrain": "Terreny", + "map-layer": "Capa de mapa", + "here-map-normal-day": "HERE.normalDay (Defecte)", + "here-map-normal-night": "HERE.normalNight", + "here-map-hybrid-day": "HERE.hybridDay", + "here-map-terrain-day": "HERE.terrainDay", + "credentials": "Credencials", + "here-app-id": "HERE app id", + "here-app-code": "HERE app code", + "tencent-maps-api-key": "API Key Tencent Maps", + "tencent-map-type-roadmap": "Carretera", + "tencent-map-type-satelite": "Satelite", + "tencent-map-type-hybrid": "Híbrid", + "image-map-background": "Fons de mapa d'imatge", + "image-map-background-from-entity-attribute": "Obtenir imatge de fons des d'un atribut de l'entitat", + "image-url-source-entity-alias": "Àlies importants per a URL d'imatge", + "image-url-source-entity-attribute": "Atribut d'entitat per a URL d'imatge", + "common-map-settings": "Paràmetres comuns de mapes", + "x-pos-key-name": "Clau per a posició X", + "y-pos-key-name": "Clau per a posició I", + "latitude-key-name": "Clau per a latitud", + "longitude-key-name": "Clau per a longitud", + "default-map-zoom-level": "Nivell de zoom per defecte (0 - 20)", + "default-map-center-position": "Posició central del mapa per defecte (0,0)", + "disable-scroll-zooming": "Desactivar zoom a scroll", + "disable-zoom-control-buttons": "Desactivar botons de zoom", + "fit-map-bounds": "Ajustar els límits del mapa per cobrir tots els marcadors", + "use-default-map-center-position": "Usar posició central per defecte", + "entities-limit": "Límit d'entitats a carregar", + "markers-settings": "Paràmetres dels marcadors", + "marker-offset-x": "Offset X relatiu a la posició multiplicada per l'amplada del marcador", + "marker-offset-y": "Offset I relatiu a la posició multiplicat per l'alt del marcador", + "position-function": "Funció de conversió de posició, ha de retornar coordenades x,i com a valor double de 0 a 1", + "draggable-marker": "Marcador arrossegable", + "label": "Etiqueta", + "show-label": "Mostrar etiqueta", + "use-label-function": "Usar funció d'etiqueta", + "label-pattern": "Etiqueta (Exemples de patró: '${entityName}', '${entityName}: (Text ${keyName} unitats.)' )", + "label-function": "Funció d'etiqueta", + "tooltip": "Suggeriments (tooltip)", + "show-tooltip": "Mostrar suggeriments", + "show-tooltip-action": "Acció per mostrar els suggeriments", + "show-tooltip-action-click": "Mostrar suggeriments tooltip a click (per defecte)", + "show-tooltip-action-hover": "Mostrar suggeriments on hover", + "auto-close-tooltips": "Auto-tancar suggeriments", + "use-tooltip-function": "Funció de suggeriments", + "tooltip-pattern": "Suggeriment (per ex. 'Text ${keyName} unitats.' o Text Link')", + "tooltip-function": "Funció de suggeriment", + "tooltip-offset-x": "Offset X relatiu a l'àncora del marcador multiplicat per l'amplada", + "tooltip-offset-y": "Offset I relatiu a l'àncora del marcador multiplicat per l'alçada", + "color": "Color", + "use-color-function": "Utilitzar funció de color", + "color-function": "Funció de color", + "marker-image": "Imatge de marcador", + "use-marker-image-function": "Usar funció d'imatge de marcador", + "custom-marker-image": "Imatge de marcador personalitzada", + "custom-marker-image-size": "Mida d'imatge personalitzada (px)", + "marker-image-function": "Funció d'imatge de marcador", + "marker-images": "Imatges de marcador", + "polygon-settings": "Paràmetres de polígon", + "show-polygon": "Mostrar polígon", + "polygon-key-name": "Clau del polígon", + "enable-polygon-edit": "Polígon editabli", + "polygon-label": "Etiqueta del polígon", + "show-polygon-label": "Mostra etiqueta del polígon", + "use-polygon-label-function": "Utilitzar funcions d'etiqueta al polígon", + "polygon-label-pattern": "Etiqueta del polígon (Exemples de patró: '${entityName}', '${entityName}: (Text ${keyName} unitats.)' )", + "polygon-label-function": "Funció d'etiqueta del polígon", + "polygon-tooltip": "Suggeriment polígon", + "show-polygon-tooltip": "Mostrar suggeriments", + "auto-close-polygon-tooltips": "Auto-tancar suggeriments polígon", + "use-polygon-tooltip-function": "Utilitzar funció de suggeriments al polígon", + "polygon-tooltip-pattern": "Suggeriments (per ex. 'Text ${keyName} unitats.' o Text Link')", + "polygon-tooltip-function": "Funció de suggeriment de polígon", + "polygon-color": "Color de polígon", + "polygon-opacity": "Opacitat de polígon", + "use-polygon-color-function": "Utilitzar funció de color al polígon", + "polygon-color-function": "Funció de color de polígon", + "polygon-stroke": "Traç de polígon", + "stroke-color": "Color de traç", + "stroke-opacity": "Opacitat de traç", + "stroke-weight": "Pes de traç", + "use-polygon-stroke-color-function": "Utilitzar funció de color de traç al polígon", + "polygon-stroke-color-function": "Funció de color de traç", + "circle-settings": "Ajustaments de cercle", + "show-circle": "Mostrar cercle", + "circle-key-name": "Clau de cercle", + "enable-circle-edit": "Activar edició de cercle", + "circle-label": "Etiqueta de cercle", + "show-circle-label": "Mostra etiqueta de cercle", + "use-circle-label-function": "Utilitzar funció per a l'etiqueta de cercle", + "circle-label-pattern": "Etiqueta de cercle (Exemples patró: '${entityName}', '${entityName}: (Text ${keyName} unitats.)' )", + "circle-label-function": "Funció d'etiqueta de cercle", + "cercle-tooltip": "Suggeriments de cercle", + "show-circle-tooltip": "Mostrar suggeriments de cercle", + "auto-close-circle-tooltips": "Auto-tancar suggeriments", + "use-circle-tooltip-function": "Utilitzar funció de suggeriments en cercle", + "circle-tooltip-pattern": "Suggeriment (per ex. 'Text ${keyName} unitats.' o Text Link')", + "circle-tooltip-function": "Funció de suggeriment cercle", + "cercle-fill-color": "Color de farciment cercle", + "circle-fill-color-opacity": "Opacitat color de farciment", + "use-circle-fill-color-function": "Fer servir funció de color de farciment", + "circle-fill-color-function": "Funció de color de farciment", + "circle-stroke": "Traç del cercle", + "use-circle-stroke-color-function": "Fer servir funció de color de traç", + "circle-stroke-color-function": "Funció de color de traç", + "markers-clustering-settings": "Paràmetres d'agrupació de marcadors", + "use-map-markers-clustering": "Usar agrupació de marcadors al mapa", + "zoom-on-cluster-click": "Zoom quan es faci clic en un grup", + "max-cluster-zoom": "Nivell màxim de zoom en què un marcador pot ser part d'un grup (0 - 18)", + "max-cluster-radius-pixels": "Ràdio màxim que un grup cobreix en píxels", + "cluster-zoom-animation": "Mostrar animació en marcadors quan es faci zoom", + "show-markers-bounds-on-cluster-mouse-over": "Mostrar els límits dels marcadors quan el ratolí passi per sobre d'un grup", + "spiderfy-max-zoom-level": "Spiderfy al màxim nivell de zoom (per veure tots els marcadors)", + "load-optimization": "Optimització de càrrega", + "cluster-chunked-loading": "Fer servir fragments per afegir marcadors perquè la pàgina no es congeli", + "cluster-markers-lazy-load": "Utilitzeu lazy load en afegir marcadors", + "editor-settings": "Paràmetres de l'editor", + "enable-snapping": "Habilitar ajustament a altres vèrtexs per dibuixar amb precisió", + "init-draggable-mode": "Inicialitzar mapa en mode arrossegament", + "hide-all-edit-buttons": "Amagar tots els botons d'edició", + "hide-draw-buttons": "Ocultar botons de dibuix", + "hide-edit-buttons": "Amagar botons d'edició", + "hide-remove-button": "Amagar botons d'esborrat", + "route-map-settings": "Paràmetres de mapa de ruta", + "trip-animation-settings": "Paràmetres d'animació de ruta", + "normalization-step": "Passos de normalització (ms)", + "tooltip-background-color": "Color de fons del suggeriment", + "tooltip-font-color": "Color de font dels suggeriments", + "tooltip-opacity": "Opacitat dels suggeriments (0-1)", + "auto-close-tooltip": "Auto-tancar suggeriments", + "rotation-angle": "Angle de rotació addicional per al marcador (graus)", + "path-settings": "Paràmetres de ruta", + "path-color": "Color de ruta", + "use-path-color-function": "Utilitzar funció de color de ruta", + "path-color-function": "Funció de color de ruta", + "path-decorator": "Decorador de ruta", + "use-path-decorator": "Utilitzar decorador de ruta", + "decorator-symbol": "Símbol del decorador", + "decorator-symbol-arrow-head": "Fletxa", + "decorator-symbol-dash": "Estrella", + "decorator-symbol-size": "Mida del decorador (px)", + "use-path-decorator-custom-color": "Usar color personalitzat al decorador", + "decorator-custom-color": "Color personalitzat del decorador", + "decorator-offset": "Offset decorador", + "end-decorator-offset": "Offset final del decorador", + "decorator-repeat": "Repetició del decorador", + "points-settings": "Ajustaments de punts", + "show-points": "Mostrar punts", + "point-color": "Color de punts", + "point-size": "Mida de punts (px)", + "use-point-color-function": "Utilitzar funció de color de punts", + "point-color-function": "Funció de color de punts", + "use-point-as-anchor": "Usar punt com àncora", + "point-as-anchor-function": "Funció de punt com a àncora", + "independent-point-tooltip": "Suggeriment independent en punt" + }, + "markdown": { + "use-markdown-text-function": "Fer servir markdown/HTML", + "markdown-text-function": "Funció de valor Markdown/HTML", + "markdown-text-pattern": "Patró de Markdown/HTML (markdown o HTML amb variables, per ex. '${entityName} o ${keyName} - text.')", + "markdown-css": "Markdown/HTML CSS" + }, + "simple-card": { + "label-position": "Posició etiqueta", + "label-position-left": "Esquerra", + "label-position-top": "Superior" + }, + "table": { + "common-table-settings": "Ajustaments comuns en taules", + "enable-search": "Activar cerca", + "enable-sticky-header": "Mostrar sempre la capçalera", + "enable-sticky-action": "Mostra sempre la columna d'accions", + "hidden-cell-button-display-mode": "Visualització de botons d'acció oculta", + "show-empty-space-hidden-action": "Mostrar espai buit en lloc de cel·la oculta", + "dont-reserve-space-hidden-action": "No reserveu espai per als botons en cel·la oculta", + "display-timestamp": "Mostra columna timestamp", + "display-milliseconds": "Mostrar mil·lisegons", + "display-pagination": "Mostrar pàgines", + "default-page-size": "Mida de pàgina per defecte", + "use-entity-label-tab-name": "Usar etiqueta important al nom de la taula", + "hide-empty-lines": "Ocultar línies buides", + "row-style": "Estil de fila", + "use-row-style-function": "Utilitzar funció d'estil de fila", + "row-style-function": "Funció d'estil de fila", + "cell-style": "Estil de cel·la", + "use-cell-style-function": "Utilitzar funció d'estil de cel·la", + "cell-style-function": "Funció d'estil de cel·la", + "cell-content": "Contingut de cel·la", + "use-cell-content-function": "Utilitzar funció de contingut de cel·la", + "cell-content-function": "Funció de contingut de cel·la", + "show-latest-data-column": "Mostra columna de darreres dades", + "latest-data-column-order": "Ordre de columna d'últimes dades", + "entities-table-title": "Títol de taula d'entitats", + "enable-select-column-display": "Activar possibilitat de seleccionar columnes a mostrar", + "display-entity-name": "Mostra columna de nom important", + "entity-name-column-title": "Títol de columna en nom d'entitat", + "display-entity-label": "Mostra columna d'etiqueta important", + "entity-label-column-title": "Títol de columna a l'etiqueta important", + "display-entity-type": "Mostra columna de tipus important", + "default-sort-order": "Ordenació per defecte", + "column-width": "Amplada de columna (px o %)", + "default-column-visibility": "Visibilitat per defecte a columna", + "column-visibility-visible": "Visible", + "column-visibility-hidden": "Oculta", + "column-selection-to-display": "Selecció de columnes a 'Columnes a Mostra'", + "column-selection-to-display-enabled": "Activada", + "column-selection-to-display-disabled": "Desactivada", + "alarms-table-title": "Títol de taula d'alarmes", + "enable-alarms-selection": "Activar selecció d'alarmes", + "enable-alarms-search": "Activar cerca d'alarmes", + "enable-alarm-filter": "Activar filtre d'alarmes", + "display-alarm-details": "Mostra detalls d'alarma", + "allow-alarms-ack": "Permetre reconeixement d'alarmes", + "allow-alarms-clear": "Permetre esborrat d'alarmes" + }, + "value-source": { + "value-source": "Origen valor", + "predefined-value": "Valor predefinit", + "entity-attribute": "Valor pres d'un atribut important", + "value": "Valor", + "source-entity-alias": "Àlies entitat d'origen", + "source-entity-attribute": "Atribut entitat d'origen" + }, + "widget-font": { + "font-family": "Família (font family)", + "size": "Mida", + "relative-font-size": "Mida font relativa (percentatge)", + "font-style": "Estil", + "font-style-normal": "Normal", + "font-style-italic": "Cursiva", + "font-style-oblique": "Subratllada", + "font-weight": "Pes", + "font-weight-normal": "Normal", + "font-weight-bold": "Negreta", + "font-weight-bolder": "Negreta+", + "font-weight-lighter": "Lighter", + "color": "Color", + "shadow-color": "Color ombra" + }, + "subscription": { + "entity-limit-text": "Tanmateix, podeu actualitzar el vostre pla de subscripció per augmentar els vostres límits.", + "upgrade-your-plan": "Actualitza el pla de subscripció", + "white-labeling-feature": "Característica d'etiqueta blanca", + "white-labeling-text-full": "Canvia la marca de la interfície web de la plataforma ThingsBoard amb el logotip i l'esquema de colors de la teva empresa o producte en 2 minuts.

                Elimineu 'Powered By' al peu de pàgina del tauler.
                No cal codificar ni reiniciar el servei. Permet als vostres clients també marcar en blanc la seva interfície.", + "enable-white-labeling": "Activa ara la funció d'etiqueta blanca actualitzant el teu pla de subscripció!", + "read-more": "Read more", + "white-labeling-video-text": "Vegeu el vídeo tutorial a continuació per veure com funciona aquesta funció!" + }, + "subscription-error": { + "title": "Incompliment de la subscripció", + "warning-title": "Avís de subscripció", + "upgrade-subscription-plan": "Si us plau, actualitzeu el vostre pla de subscripció", + "upgrade-subscription-plan-to-install-solution-template": "Per tal d'instal·lar {{solutionTemplateName}} solució, heu d'actualitzar la vostra subscripció almenys al {{planName}} pla!", + "limit-reached": { + "device-count": "Heu arribat al màxim de dispositius ({{value}}) permisos per al vostre pla de subscripció!", + "asset-count": "Heu assolit el màxim d'actius ({{value}}) permisos per al vostre pla de subscripció!" + } + } + }, + "icon": { + "icon": "Icona", + "select-icon": "Seleccionar icones", + "material-icons": "Icones material-disseny", + "show-all": "Mostrar totes les icones" + }, + "phone-input": { + "phone-input-label": "Número de telèfon", + "phone-input-required": "Cal número de telèfon", + "phone-input-validation": "El número és invàlid o erroni", + "phone-input-pattern": "Número invàlid. Ha de cumplir el format E.164, ej. {{phoneNumber}}", + "phone-input-hint": "Número en el format E.164, ej. {{phoneNumber}}" + }, + "custom": { + "widget-action": { + "action-cell-button": "Acció botó de cel·la", + "row-click": "En clic de fila", + "polygon-click": "Clic al polígon", + "marker-click": "En clic de marcador", + "circle-click": "Al cercle clic", + "tooltip-tag-action": "Acció de l'etiqueta Tooltip", + "node-selected": "Clic en el node seleccionat", + "element-click": "Clic en el element HTML", + "pie-slice-click": "Clic en la porció", + "row-double-click": "Doble clic en la fila" + } + }, + "language": { + "language": "Llenguatge" + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json index a361d1cab4..1522a03731 100644 --- a/ui-ngx/src/assets/locale/locale.constant-uk_UA.json +++ b/ui-ngx/src/assets/locale/locale.constant-uk_UA.json @@ -2479,6 +2479,14 @@ "row-double-click": "Дії при подвійному натисканні на рядок" } }, + "paginator" : { + "items-per-page": "Елементів на сторінці:", + "first-page-label": "Перша сторінка", + "last-page-label": "Остання сторінка", + "next-page-label": "Наступна сторінка", + "previous-page-label": "Попередня сторінка", + "items-per-page-separator": "з" + }, "language": { "language": "Мова" }