diff --git a/application/src/main/data/resources/js_modules/gateway-management-extension.js b/application/src/main/data/resources/js_extensions/gateway-management-extension.js similarity index 100% rename from application/src/main/data/resources/js_modules/gateway-management-extension.js rename to application/src/main/data/resources/js_extensions/gateway-management-extension.js diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 5ac230096b..d2ab4c85ac 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -41,6 +41,7 @@ import org.thingsboard.server.common.msg.CalculatedFieldStatePartitionRestoreMsg import org.thingsboard.server.common.msg.cf.CalculatedFieldPartitionChangeMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.gen.transport.TransportProtos.AttributeScopeProto; import org.thingsboard.server.gen.transport.TransportProtos.AttributeValueProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; @@ -53,6 +54,7 @@ import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesAggregationCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.alarm.AlarmCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; @@ -75,6 +77,7 @@ import java.util.stream.Collectors; import static org.thingsboard.server.common.data.DataConstants.REEVALUATION_MSG; import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; +import static org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry.getValueForTsRecord; import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType; /** @@ -132,6 +135,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } else { removeState(cfId); } + msg.getCallback().onSuccess(); } public void process(CalculatedFieldStatePartitionRestoreMsg msg) { @@ -242,7 +246,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } if (state instanceof PropagationCalculatedFieldState propagationState) { PropagationArgumentEntry entry = new PropagationArgumentEntry(); - entry.setAdded(msg.getRelatedEntityId()); + entry.setAdded(List.of(msg.getRelatedEntityId())); updatedArgs = propagationState.update(Map.of(PROPAGATION_CONFIG_ARGUMENT, entry), ctx); } if (CollectionsUtil.isEmpty(updatedArgs)) { @@ -421,19 +425,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM if (state == null) { state = createState(ctx); justRestored = true; - } else if (ctx.shouldFetchRelationQueryDynamicArgumentsFromDb(state)) { - log.debug("[{}][{}] Going to update dynamic arguments for CF.", entityId, ctx.getCfId()); - try { - Map dynamicArgsFromDb = cfService.fetchDynamicArgsFromDb(ctx, entityId); - dynamicArgsFromDb.forEach(newArgValues::putIfAbsent); - if (ctx.getCfType() == CalculatedFieldType.GEOFENCING) { - var geofencingState = (GeofencingCalculatedFieldState) state; - geofencingState.updateLastDynamicArgumentsRefreshTs(); - } - } catch (Exception e) { - throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); - } - } else if (ctx.shouldFetchEntityRelations(state)) { + } else if (ctx.shouldFetchRelatedEntities(state)) { log.debug("[{}][{}] Going to update related entities for CF.", entityId, ctx.getCfId()); try { if (state instanceof RelatedEntitiesAggregationCalculatedFieldState relatedEntitiesState) { @@ -447,6 +439,11 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM justRestored = true; } } + if (state instanceof GeofencingCalculatedFieldState geofencingCalculatedFieldState) { + Map dynamicArgsFromDb = cfService.fetchDynamicArgsFromDb(ctx, entityId); + dynamicArgsFromDb.forEach(newArgValues::putIfAbsent); + geofencingCalculatedFieldState.updateScheduledRefreshTs(); + } } catch (Exception e) { throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); } @@ -476,9 +473,9 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM state.setCtx(ctx, actorCtx); state.init(false); - if (ctx.getCfType() == CalculatedFieldType.GEOFENCING && ctx.isRelationQueryDynamicArguments()) { + if (ctx.getCfType() == CalculatedFieldType.GEOFENCING && ctx.isCfHasRelationPathQuerySource()) { GeofencingCalculatedFieldState geofencingState = (GeofencingCalculatedFieldState) state; - geofencingState.updateLastDynamicArgumentsRefreshTs(); + geofencingState.updateScheduledRefreshTs(); } Map arguments = fetchArguments(ctx); @@ -587,30 +584,61 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM if (!relatedEntityArgs.isEmpty() || !args.isEmpty()) { for (TsKvProto item : data) { ReferencedEntityKey key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_LATEST, null); - Set argNames = relatedEntityArgs.get(key); - if (argNames != null) { - argNames.forEach(argName -> { - arguments.put(argName, new SingleValueArgumentEntry(originator, item)); - }); - } - argNames = args.get(key); - if (argNames != null) { - argNames.forEach(argName -> { - arguments.put(argName, new SingleValueArgumentEntry(item)); - }); - } + + SingleValueArgumentEntry relatedArgIncoming = new SingleValueArgumentEntry(originator, item); + mapLatest(relatedArgIncoming, relatedEntityArgs.get(key), arguments); + + SingleValueArgumentEntry incoming = new SingleValueArgumentEntry(item); + mapLatest(incoming, args.get(key), arguments); + key = new ReferencedEntityKey(item.getKv().getKey(), ArgumentType.TS_ROLLING, null); - argNames = args.get(key); - if (argNames != null) { - argNames.forEach(argName -> { - arguments.put(argName, new SingleValueArgumentEntry(item)); - }); - } + mapRolling(item, args.get(key), arguments); } } return arguments; } + private void mapLatest(SingleValueArgumentEntry incoming, + Set argNames, + Map arguments) { + if (argNames != null) { + argNames.forEach(argName -> arguments.compute(argName, (name, existing) -> { + if (existing == null) { + return incoming; + } + existing.updateEntry(incoming); + return existing; + })); + } + } + + private void mapRolling(TsKvProto item, + Set argNames, + Map arguments) { + if (argNames != null) { + Double recordValue = getValueForTsRecord(ProtoUtils.fromProto(item.getKv())); + argNames.forEach(argName -> arguments.compute(argName, (name, existing) -> { + if (existing instanceof TsRollingArgumentEntry rolling) { + if (recordValue != null) { + rolling.getTsRecords().put(item.getTs(), recordValue); + } + return rolling; + } + TsRollingArgumentEntry rolling = new TsRollingArgumentEntry(); + if (recordValue != null) { + rolling.getTsRecords().put(item.getTs(), recordValue); + } + if (existing instanceof SingleValueArgumentEntry single) { + Double existingValue = getValueForTsRecord(single.getKvEntryValue()); + if (existingValue != null) { + rolling.getTsRecords().put(single.getTs(), existingValue); + } + } + return rolling; + })); + } + } + private Map mapToArguments(CalculatedFieldCtx ctx, AttributeScopeProto scope, List attrDataList) { return mapToArguments(entityId, ctx.getMainEntityArguments(), ctx.getMainEntityGeofencingArgumentNames(), Collections.emptyMap(), scope, attrDataList); } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index b2fe6d2fd9..5d3965feda 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -167,10 +167,13 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware if (ctx != null) { msg.setCtx(ctx); - log.debug("Pushing CF state restore msg to specific actor [{}]", msg.getId().entityId()); + log.debug("[{}] Pushing CF state restore msg to specific actor [{}]", tenantId, msg.getId().entityId()); getOrCreateActor(msg.getId().entityId()).tellWithHighPriority(msg); - } else { + } else if (msg.getState() != null) { + log.debug("[{}] Received CF state restore msg for non-existing CF [{}]. Removing state", tenantId, cfId); cfStateService.deleteState(msg.getId(), msg.getCallback()); + } else { + msg.getCallback().onSuccess(); } } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldStateRestoreMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldStateRestoreMsg.java index d1c2f11aeb..3969afbabf 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldStateRestoreMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldStateRestoreMsg.java @@ -19,6 +19,7 @@ import lombok.Data; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; +import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; @@ -30,6 +31,7 @@ public class CalculatedFieldStateRestoreMsg implements ToCalculatedFieldSystemMs private final CalculatedFieldEntityCtxId id; private final CalculatedFieldState state; private final TopicPartitionInfo partition; + private final TbCallback callback; private CalculatedFieldCtx ctx; @Override @@ -41,4 +43,5 @@ public class CalculatedFieldStateRestoreMsg implements ToCalculatedFieldSystemMs public TenantId getTenantId() { return id.tenantId(); } + } diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java index 11a8651026..be7826df2b 100644 --- a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java @@ -43,7 +43,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainType; -import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbActorStopReason; import org.thingsboard.server.common.msg.TbMsg; @@ -139,13 +138,22 @@ public class TenantActor extends RuleChainManagerActor { @Override protected boolean doProcess(TbActorMsg msg) { if (cantFindTenant) { - log.info("[{}] Processing missing Tenant msg: {}", tenantId, msg); - if (msg.getMsgType().equals(MsgType.QUEUE_TO_RULE_ENGINE_MSG)) { - QueueToRuleEngineMsg queueMsg = (QueueToRuleEngineMsg) msg; - queueMsg.getMsg().getCallback().onSuccess(); - } else if (msg.getMsgType().equals(MsgType.TRANSPORT_TO_DEVICE_ACTOR_MSG)) { - TransportToDeviceActorMsgWrapper transportMsg = (TransportToDeviceActorMsgWrapper) msg; - transportMsg.getCallback().onSuccess(); + log.debug("[{}] Processing message for non-existing tenant: {}", tenantId, msg); + switch (msg.getMsgType()) { + case QUEUE_TO_RULE_ENGINE_MSG -> { + ((QueueToRuleEngineMsg) msg).getMsg().getCallback().onSuccess(); + } + case TRANSPORT_TO_DEVICE_ACTOR_MSG -> { + ((TransportToDeviceActorMsgWrapper) msg).getCallback().onSuccess(); + } + case CF_STATE_RESTORE_MSG -> { + ((CalculatedFieldStateRestoreMsg) msg).getCallback().onSuccess(); + } + default -> { + if (!log.isDebugEnabled()) { + log.info("[{}] Processing message for non-existing tenant: {}", tenantId, msg); + } + } } return true; } 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 83603f7ea5..136a101e93 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -150,8 +150,6 @@ import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.domain.DomainService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; -import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.exception.EntitiesLimitExceededException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.job.JobService; import org.thingsboard.server.dao.mobile.MobileAppBundleService; @@ -175,6 +173,8 @@ import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; +import org.thingsboard.server.exception.DataValidationException; +import org.thingsboard.server.exception.EntitiesLimitExceededException; import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; diff --git a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java index 1c6b23084e..0935f47379 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java @@ -21,6 +21,7 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.http.HttpStatus; @@ -68,7 +69,7 @@ import org.thingsboard.server.service.security.permission.Operation; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; -import java.util.List; +import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -198,12 +199,12 @@ public class CalculatedFieldController extends BaseController { @RequestParam int pageSize, @Parameter(description = PAGE_NUMBER_DESCRIPTION, required = true) @RequestParam int page, - @Parameter(description = "Calculated field type filter.") - @RequestParam CalculatedFieldType type, + @Parameter(description = "Calculated field types filter.") + @RequestParam(required = false) Set types, @Parameter(description = "Entity type filter. If not specified, calculated fields for all supported entity types will be returned.") @RequestParam(required = false) EntityType entityType, @Parameter(description = "Entities filter. If not specified, calculated fields for entity type filter will be returned.") - @RequestParam(required = false) List entities, + @RequestParam(required = false) Set entities, @Parameter(description = "Name filter. To specify multiple names, duplicate 'name' parameter for each name, for example '?name=name1&name=name2") @RequestParam(required = false) String name, // for Swagger only, retrieved from MultiValueMap params (due to issues when name contains comma) @Parameter(description = CF_TEXT_SEARCH_DESCRIPTION) @@ -215,6 +216,12 @@ public class CalculatedFieldController extends BaseController { @RequestParam MultiValueMap params) throws ThingsboardException { PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); SecurityUser user = getCurrentUser(); + + if (CollectionUtils.isEmpty(types)) { + types = EnumSet.allOf(CalculatedFieldType.class); + types.remove(CalculatedFieldType.ALARM); + } + Set entityTypes; if (entityType == null) { entityTypes = CalculatedField.SUPPORTED_ENTITIES.keySet(); @@ -223,10 +230,10 @@ public class CalculatedFieldController extends BaseController { } CalculatedFieldFilter filter = CalculatedFieldFilter.builder() - .type(type) + .types(types) .entityTypes(entityTypes) .entityIds(entities) - .names(params.get("name")) + .names(Optional.ofNullable(params.get("name")).map(HashSet::new).orElse(null)) .build(); return calculatedFieldService.findCalculatedFieldsByTenantIdAndFilter(user.getTenantId(), filter, pageLink); } @@ -361,8 +368,7 @@ public class CalculatedFieldController extends BaseController { return; } case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ); - default -> - throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities."); + default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities."); } } } diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java index 1b3e494802..df9e5c6476 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -54,7 +54,7 @@ import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportReques import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportResult; import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; import org.thingsboard.server.config.annotations.ApiOperation; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.queue.util.TbCoreComponent; diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java index 3ef24e2db9..8cabf2ed7b 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java @@ -52,7 +52,6 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.msg.tools.MaxPayloadSizeExceededException; import org.thingsboard.server.common.msg.tools.TbRateLimitsException; -import org.thingsboard.server.dao.exception.EntitiesLimitExceededException; import org.thingsboard.server.service.security.exception.AuthMethodNotSupportedException; import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; import org.thingsboard.server.service.security.exception.UserPasswordExpiredException; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java index 13ceb406bb..ec645085e6 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java @@ -22,17 +22,18 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.gson.JsonElement; -import com.google.gson.JsonParser; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.Data; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.function.TriConsumer; import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.rule.engine.api.AttributesSaveRequest; import org.thingsboard.rule.engine.api.AttributesSaveRequest.Strategy; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; +import org.thingsboard.server.actors.calculatedField.MultipleTbCallback; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.data.AttributeScope; @@ -80,7 +81,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.function.Function; @@ -391,6 +391,24 @@ public abstract class AbstractCalculatedFieldProcessingService { return new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), startTs, endTs, 0, limit, Aggregation.NONE); } + protected void handlePropagationResults(PropagationCalculatedFieldResult propagationResult, TbCallback callback, + TriConsumer telemetryResultHandler) { + List propagationEntityIds = propagationResult.getEntityIds(); + if (propagationEntityIds.isEmpty()) { + callback.onSuccess(); + return; + } + if (propagationEntityIds.size() == 1) { + EntityId propagationEntityId = propagationEntityIds.get(0); + telemetryResultHandler.accept(propagationEntityId, propagationResult.getResult(), callback); + return; + } + MultipleTbCallback multipleTbCallback = new MultipleTbCallback(propagationEntityIds.size(), callback); + for (var propagationEntityId : propagationEntityIds) { + telemetryResultHandler.accept(propagationEntityId, propagationResult.getResult(), multipleTbCallback); + } + } + protected void sendMsgToRuleEngine(TenantId tenantId, EntityId entityId, TbCallback callback, TbMsg msg) { try { clusterService.pushMsgToRuleEngine(tenantId, entityId, msg, new TbQueueCallback() { @@ -413,7 +431,7 @@ public abstract class AbstractCalculatedFieldProcessingService { protected void saveTelemetryResult(TenantId tenantId, EntityId entityId, String cfName, TelemetryCalculatedFieldResult cfResult, List cfIds, TbCallback callback) { OutputType type = cfResult.getType(); - JsonElement jsonResult = JsonParser.parseString(Objects.requireNonNull(cfResult.stringValue())); + JsonElement jsonResult = cfResult.toJsonElement(); log.trace("[{}][{}] Saving CF result: {}", tenantId, entityId, jsonResult); switch (type) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java index dd0bf45eb9..81cc9c4087 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldStateService.java @@ -68,7 +68,7 @@ public abstract class AbstractCalculatedFieldStateService implements CalculatedF protected abstract void doRemove(CalculatedFieldEntityCtxId stateId, TbCallback callback); - protected void processRestoredState(CalculatedFieldStateProto stateMsg, TopicPartitionInfo partition) { + protected void processRestoredState(CalculatedFieldStateProto stateMsg, TopicPartitionInfo partition, TbCallback callback) { var id = fromProto(stateMsg.getId()); if (partition == null) { try { @@ -79,12 +79,12 @@ public abstract class AbstractCalculatedFieldStateService implements CalculatedF } } var state = fromProto(id, stateMsg); - processRestoredState(id, state, partition); + processRestoredState(id, state, partition, callback); } - protected void processRestoredState(CalculatedFieldEntityCtxId id, CalculatedFieldState state, TopicPartitionInfo partition) { + protected void processRestoredState(CalculatedFieldEntityCtxId id, CalculatedFieldState state, TopicPartitionInfo partition, TbCallback callback) { partition = partition.withTopic(DataConstants.CF_STATES_QUEUE_NAME); - actorSystemContext.tellWithHighPriority(new CalculatedFieldStateRestoreMsg(id, state, partition)); + actorSystemContext.tellWithHighPriority(new CalculatedFieldStateRestoreMsg(id, state, partition, callback)); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java index 53d64e5b27..13fad720b3 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java @@ -27,9 +27,11 @@ import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.aggregation.single.AggIntervalEntry; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry; import java.util.List; import java.util.Map; +import java.util.Optional; public interface CalculatedFieldProcessingService { @@ -37,6 +39,8 @@ public interface CalculatedFieldProcessingService { Map fetchDynamicArgsFromDb(CalculatedFieldCtx ctx, EntityId entityId); + Optional fetchPropagationArgumentFromDb(CalculatedFieldCtx ctx, EntityId entityId); + List fetchRelatedEntities(CalculatedFieldCtx ctx, EntityId entityId); Map fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map arguments); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java index c973cebc18..8b4c2a0101 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java @@ -15,11 +15,14 @@ */ package org.thingsboard.server.service.cf; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.msg.TbMsg; import java.util.List; +import java.util.Objects; public interface CalculatedFieldResult { @@ -29,4 +32,8 @@ public interface CalculatedFieldResult { boolean isEmpty(); + default JsonElement toJsonElement() { + return JsonParser.parseString(Objects.requireNonNull(stringValue())); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index 271fdb828d..1ab8f5a076 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -17,13 +17,13 @@ package org.thingsboard.server.service.cf; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.function.TriConsumer; import org.springframework.stereotype.Service; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; import org.thingsboard.server.actors.calculatedField.MultipleTbCallback; 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.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.aggregation.AggMetric; import org.thingsboard.server.common.data.cf.configuration.aggregation.RelatedEntitiesAggregationCalculatedFieldConfiguration; @@ -50,6 +50,7 @@ import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.aggregation.single.AggIntervalEntry; +import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import java.util.ArrayList; @@ -57,6 +58,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -94,11 +96,18 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF @Override public Map fetchDynamicArgsFromDb(CalculatedFieldCtx ctx, EntityId entityId) { - return switch (ctx.getCfType()) { - case GEOFENCING -> resolveArgumentFutures(fetchGeofencingCalculatedFieldArguments(ctx, entityId, true, System.currentTimeMillis())); - case PROPAGATION -> resolveArgumentFutures(Map.of(PROPAGATION_CONFIG_ARGUMENT, fetchPropagationCalculatedFieldArgument(ctx, entityId))); - default -> Collections.emptyMap(); - }; + return ctx.getCfType() == CalculatedFieldType.GEOFENCING ? + resolveArgumentFutures(fetchGeofencingCalculatedFieldArguments(ctx, entityId, true, System.currentTimeMillis())) : + Collections.emptyMap(); + } + + @Override + public Optional fetchPropagationArgumentFromDb(CalculatedFieldCtx ctx, EntityId entityId) { + if (ctx.getCfType() != CalculatedFieldType.PROPAGATION) { + return Optional.empty(); + } + return Optional.of((PropagationArgumentEntry) + resolveArgumentValue(PROPAGATION_CONFIG_ARGUMENT, fetchPropagationCalculatedFieldArgument(ctx, entityId))); } @Override @@ -169,24 +178,6 @@ public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedF sendMsgToRuleEngine(tenantId, entityId, callback, result.toTbMsg(entityId, cfName, cfIds)); } - private void handlePropagationResults(PropagationCalculatedFieldResult propagationResult, TbCallback callback, - TriConsumer telemetryResultHandler) { - List propagationEntityIds = propagationResult.getEntityIds(); - if (propagationEntityIds.isEmpty()) { - callback.onSuccess(); - return; - } - if (propagationEntityIds.size() == 1) { - EntityId propagationEntityId = propagationEntityIds.get(0); - telemetryResultHandler.accept(propagationEntityId, propagationResult.getResult(), callback); - return; - } - MultipleTbCallback multipleTbCallback = new MultipleTbCallback(propagationEntityIds.size(), callback); - for (var propagationEntityId : propagationEntityIds) { - telemetryResultHandler.accept(propagationEntityId, propagationResult.getResult(), multipleTbCallback); - } - } - @Override public void pushMsgToLinks(CalculatedFieldTelemetryMsg msg, List linkedCalculatedFields, TbCallback callback) { Map> unicasts = new HashMap<>(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index 5173c48892..09ca35cc4b 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -48,7 +48,6 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, protected Map arguments = new HashMap<>(); protected boolean sizeExceedsLimit; - protected long latestTimestamp = DEFAULT_LAST_UPDATE_TS; protected ReadinessStatus readinessStatus; @Setter @@ -63,7 +62,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, this.ctx = ctx; this.actorCtx = actorCtx; this.requiredArguments = ctx.getArgNames(); - this.readinessStatus = checkReadiness(requiredArguments, arguments); + this.readinessStatus = checkReadiness(); } @Override @@ -108,7 +107,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, if (updatedArguments == null) { return Collections.emptyMap(); } - readinessStatus = checkReadiness(requiredArguments, arguments); + readinessStatus = checkReadiness(); return updatedArguments; } @@ -121,7 +120,6 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, requiredArguments = null; arguments.clear(); sizeExceedsLimit = false; - latestTimestamp = DEFAULT_LAST_UPDATE_TS; } @Override @@ -183,13 +181,13 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState, return latestTs; } - protected ReadinessStatus checkReadiness(List requiredArguments, Map currentArguments) { - if (currentArguments == null) { + protected ReadinessStatus checkReadiness() { + if (arguments == null) { return ReadinessStatus.from(requiredArguments); } List emptyArguments = null; for (String requiredArgumentKey : requiredArguments) { - ArgumentEntry argumentEntry = currentArguments.get(requiredArgumentKey); + ArgumentEntry argumentEntry = arguments.get(requiredArgumentKey); if (argumentEntry == null || argumentEntry.isEmpty()) { if (emptyArguments == null) { emptyArguments = new ArrayList<>(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index b97be4dcd8..50ee88cae6 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -63,8 +63,7 @@ import org.thingsboard.server.dao.util.TimeUtils; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; -import org.thingsboard.server.service.cf.ctx.state.aggregation.RelatedEntitiesAggregationCalculatedFieldState; -import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.ScheduledRefreshSupported; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import java.io.Closeable; @@ -122,14 +121,14 @@ public class CalculatedFieldCtx implements Closeable { private long maxSingleValueArgumentSize; private long intermediateAggregationIntervalMillis; - private boolean relationQueryDynamicArguments; + private boolean cfHasRelationPathQuerySource; private List mainEntityGeofencingArgumentNames; private List linkedEntityAndCurrentOwnerGeofencingArgumentNames; private List relatedEntityArgumentNames; private long scheduledUpdateIntervalMillis; - private long cfCheckReevaluationInterval; - private long alarmReevaluationInterval; + private long cfCheckReevaluationIntervalMillis; + private long alarmReevaluationIntervalMillis; private Argument propagationArgument; private boolean applyExpressionForResolvedArguments; @@ -161,10 +160,11 @@ public class CalculatedFieldCtx implements Closeable { if (refId == null) { if (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION.equals(cfType)) { relatedEntityArguments.compute(refKey, (key, existingNames) -> CollectionsUtil.addToSet(existingNames, entry.getKey())); + cfHasRelationPathQuerySource = true; continue; } if (entry.getValue().hasRelationQuerySource()) { - relationQueryDynamicArguments = true; + cfHasRelationPathQuerySource = true; continue; } if (entry.getValue().hasOwnerSource()) { @@ -201,7 +201,7 @@ public class CalculatedFieldCtx implements Closeable { if (calculatedField.getConfiguration() instanceof PropagationCalculatedFieldConfiguration propagationConfig) { propagationArgument = propagationConfig.toPropagationArgument(); applyExpressionForResolvedArguments = propagationConfig.isApplyExpressionToResolvedArguments(); - relationQueryDynamicArguments = true; + cfHasRelationPathQuerySource = true; } } if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledConfig) { @@ -246,8 +246,7 @@ public class CalculatedFieldCtx implements Closeable { boolean requiresScheduledReevaluation = calculatedField.getConfiguration().requiresScheduledReevaluation(); if (calculatedField.getConfiguration() instanceof AlarmCalculatedFieldConfiguration) { if (requiresScheduledReevaluation) { - long reevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(alarmReevaluationInterval); - if (now - lastReevaluationTs >= reevaluationIntervalMillis) { + if (now - lastReevaluationTs >= alarmReevaluationIntervalMillis) { lastReevaluationTs = now; return true; } @@ -306,8 +305,8 @@ public class CalculatedFieldCtx implements Closeable { this.maxStateSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; this.maxSingleValueArgumentSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxSingleValueArgumentSizeInKBytes) * 1024; this.intermediateAggregationIntervalMillis = TimeUnit.SECONDS.toMillis(apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getIntermediateAggregationIntervalInSecForCF)); - this.cfCheckReevaluationInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getCfReevaluationCheckInterval); - this.alarmReevaluationInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getAlarmsReevaluationInterval); + this.cfCheckReevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getCfReevaluationCheckInterval)); + this.alarmReevaluationIntervalMillis = TimeUnit.SECONDS.toMillis(apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getAlarmsReevaluationInterval)); } public double evaluateSimpleExpression(Expression expression, CalculatedFieldState state) { @@ -757,38 +756,20 @@ public class CalculatedFieldCtx implements Closeable { return scheduledUpdateIntervalMillis == DISABLED_INTERVAL_VALUE; } - public boolean shouldFetchRelationQueryDynamicArgumentsFromDb(CalculatedFieldState state) { - if (!relationQueryDynamicArguments) { + public boolean shouldFetchRelatedEntities(CalculatedFieldState state) { + if (!cfHasRelationPathQuerySource) { return false; } - return switch (cfType) { - case PROPAGATION -> true; - case GEOFENCING -> { - if (isScheduledUpdateDisabled()) { - yield false; - } - var geofencingState = (GeofencingCalculatedFieldState) state; - if (geofencingState.getLastDynamicArgumentsRefreshTs() == DEFAULT_LAST_UPDATE_TS) { - yield true; - } - yield geofencingState.getLastDynamicArgumentsRefreshTs() < - System.currentTimeMillis() - scheduledUpdateIntervalMillis; - } - default -> false; - }; - } - - public boolean shouldFetchEntityRelations(CalculatedFieldState state) { - if (!(state instanceof RelatedEntitiesAggregationCalculatedFieldState relatedEntitiesAggState)) { + if (isScheduledUpdateDisabled()) { return false; } - if (isScheduledUpdateDisabled()) { + if (!(state instanceof ScheduledRefreshSupported scheduledRefreshSupported)) { return false; } - if (relatedEntitiesAggState.getLastRelatedEntitiesRefreshTs() == DEFAULT_LAST_UPDATE_TS) { + if (scheduledRefreshSupported.getLastScheduledRefreshTs() == DEFAULT_LAST_UPDATE_TS) { return true; } - return relatedEntitiesAggState.getLastRelatedEntitiesRefreshTs() < System.currentTimeMillis() - scheduledUpdateIntervalMillis; + return scheduledRefreshSupported.getLastScheduledRefreshTs() < System.currentTimeMillis() - scheduledUpdateIntervalMillis; } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java index f254631491..df0999922e 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java @@ -110,7 +110,13 @@ public interface CalculatedFieldState extends Closeable { private static final String MISSING_PROPAGATION_TARGETS_ERROR = "No entities found via 'Propagation path to related entities'. " + "Verify the configured relation type and direction."; private static final String MISSING_PROPAGATION_TARGETS_AND_ARGUMENTS_ERROR = MISSING_PROPAGATION_TARGETS_ERROR + " Missing arguments to propagate: "; - private static final ReadinessStatus READY = new ReadinessStatus(true, null); + public static final String MISSING_AGGREGATION_ENTITIES_ERROR = "No entities found via 'Aggregation path to related entities'. " + + "Verify the configured relation type and direction."; + public static final ReadinessStatus READY = new ReadinessStatus(true, null); + + public static ReadinessStatus notReady(String errorMsg) { + return new ReadinessStatus(false, errorMsg); + } public static ReadinessStatus from(List emptyOrMissingArguments) { if (CollectionsUtil.isEmpty(emptyOrMissingArguments)) { @@ -118,13 +124,12 @@ public interface CalculatedFieldState extends Closeable { } boolean propagationCtxIsEmpty = emptyOrMissingArguments.remove(PROPAGATION_CONFIG_ARGUMENT); if (!propagationCtxIsEmpty) { - return new ReadinessStatus(false, MISSING_REQUIRED_ARGUMENTS_ERROR + String.join(", ", emptyOrMissingArguments)); + return notReady(MISSING_REQUIRED_ARGUMENTS_ERROR + String.join(", ", emptyOrMissingArguments)); } if (emptyOrMissingArguments.isEmpty()) { - return new ReadinessStatus(false, MISSING_PROPAGATION_TARGETS_ERROR); + return notReady(MISSING_PROPAGATION_TARGETS_ERROR); } - return new ReadinessStatus(false, MISSING_PROPAGATION_TARGETS_AND_ARGUMENTS_ERROR + - String.join(", ", emptyOrMissingArguments)); + return notReady(MISSING_PROPAGATION_TARGETS_AND_ARGUMENTS_ERROR + String.join(", ", emptyOrMissingArguments)); } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java index ffc3c1584b..016db006dc 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/KafkaCalculatedFieldStateService.java @@ -43,6 +43,8 @@ import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory; import org.thingsboard.server.service.cf.AbstractCalculatedFieldStateService; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import static org.thingsboard.server.queue.common.AbstractTbQueueTemplate.bytesToString; @@ -61,6 +63,8 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta @Value("${queue.calculated_fields.poll_interval:25}") private long pollInterval; + @Value("${queue.calculated_fields.pack_processing_timeout:60000}") + private long packProcessingTimeout; private TbKafkaProducerTemplate> stateProducer; @@ -74,21 +78,39 @@ public class KafkaCalculatedFieldStateService extends AbstractCalculatedFieldSta .topic(partitionService.getTopic(queueKey)) .pollInterval(pollInterval) .msgPackProcessor((msgs, consumer, consumerKey, config) -> { + CountDownLatch completionLatch = new CountDownLatch(msgs.size()); for (TbProtoQueueMsg msg : msgs) { + TbCallback callback = new TbCallback() { + @Override + public void onSuccess() { + int processedMsgCount = counter.incrementAndGet(); + if (processedMsgCount % 10000 == 0) { + log.info("Processed {} CF state messages", processedMsgCount); + } + completionLatch.countDown(); + } + + @Override + public void onFailure(Throwable t) { + log.error("Failed to process CF state message: {}", msg, t); + completionLatch.countDown(); + } + }; + try { if (msg.getValue() != null) { - processRestoredState(msg.getValue(), consumerKey.partition()); + processRestoredState(msg.getValue(), consumerKey.partition(), callback); } else { - processRestoredState(getStateId(msg.getHeaders()), null, consumerKey.partition()); + processRestoredState(getStateId(msg.getHeaders()), null, consumerKey.partition(), callback); } } catch (Throwable t) { - log.error("Failed to process state message: {}", msg, t); + callback.onFailure(t); } + } - int processedMsgCount = counter.incrementAndGet(); - if (processedMsgCount % 10000 == 0) { - log.info("Processed {} calculated field state msgs", processedMsgCount); - } + boolean success = completionLatch.await(packProcessingTimeout, TimeUnit.MILLISECONDS); + if (!success) { + log.error("Timeout to process CF state messages pack of size {}", msgs.size()); } }) .consumerCreator((queueConfig, tpi) -> queueFactory.createCalculatedFieldStateConsumer()) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java index 05bfb8b717..7b9653a699 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/RocksDBCalculatedFieldStateService.java @@ -62,11 +62,22 @@ public class RocksDBCalculatedFieldStateService extends AbstractCalculatedFieldS public void restore(QueueKey queueKey, Set partitions) { if (stateService.getPartitions().isEmpty()) { cfRocksDb.forEach((key, value) -> { + CalculatedFieldStateProto stateMsg; try { - processRestoredState(CalculatedFieldStateProto.parseFrom(value), null); + stateMsg = CalculatedFieldStateProto.parseFrom(value); } catch (Exception e) { - log.error("[{}] Failed to process restored state", key, e); + log.error("Failed to parse CalculatedFieldStateProto for key {}", key, e); + return; } + processRestoredState(stateMsg, null, new TbCallback() { + @Override + public void onSuccess() {} + + @Override + public void onFailure(Throwable t) { + log.error("Failed to process CF state message: {}", stateMsg, t); + } + }); }); } super.restore(queueKey, partitions); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java index 66d2807e6c..8cdc9ddcf9 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/TsRollingArgumentEntry.java @@ -122,20 +122,11 @@ public class TsRollingArgumentEntry implements ArgumentEntry, HasLatestTs { } private void addTsRecord(Long ts, KvEntry value) { - try { - switch (value.getDataType()) { - case LONG -> value.getLongValue().ifPresent(aLong -> tsRecords.put(ts, aLong.doubleValue())); - case DOUBLE -> value.getDoubleValue().ifPresent(aDouble -> tsRecords.put(ts, aDouble)); - case BOOLEAN -> value.getBooleanValue().ifPresent(aBoolean -> tsRecords.put(ts, aBoolean ? 1.0 : 0.0)); - case STRING -> value.getStrValue().ifPresent(aString -> tsRecords.put(ts, Double.parseDouble(aString))); - case JSON -> value.getJsonValue().ifPresent(aString -> tsRecords.put(ts, Double.parseDouble(aString))); - } - } catch (Exception e) { - tsRecords.put(ts, Double.NaN); - log.debug("Invalid value '{}' for time series rolling arguments. Only numeric values are supported.", value.getValue()); - } finally { - cleanupExpiredRecords(); + Double recordValue = getValueForTsRecord(value); + if (recordValue != null) { + tsRecords.put(ts, recordValue); } + cleanupExpiredRecords(); } private void addTsRecord(Long ts, double value) { @@ -150,4 +141,19 @@ public class TsRollingArgumentEntry implements ArgumentEntry, HasLatestTs { tsRecords.entrySet().removeIf(tsRecord -> tsRecord.getKey() < System.currentTimeMillis() - timeWindow); } + public static Double getValueForTsRecord(KvEntry value) { + try { + return switch (value.getDataType()) { + case LONG -> value.getLongValue().map(Long::doubleValue).orElse(null); + case DOUBLE -> value.getDoubleValue().orElse(null); + case BOOLEAN -> value.getBooleanValue().map(b -> b ? 1.0 : 0.0).orElse(null); + case STRING -> value.getStrValue().map(Double::parseDouble).orElse(null); + case JSON -> value.getJsonValue().map(Double::parseDouble).orElse(null); + }; + } catch (Exception e) { + log.debug("Invalid value '{}' for time series rolling arguments. Only numeric values are supported.", value.getValue()); + return Double.NaN; + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java index 7036e4bd77..1edfea70a3 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/RelatedEntitiesAggregationCalculatedFieldState.java @@ -41,6 +41,7 @@ import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.aggregation.function.AggEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.ScheduledRefreshSupported; import java.util.ArrayList; import java.util.HashMap; @@ -52,16 +53,17 @@ import java.util.stream.Collectors; import static java.util.concurrent.TimeUnit.SECONDS; import static org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx.DISABLED_INTERVAL_VALUE; +import static org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState.ReadinessStatus.MISSING_AGGREGATION_ENTITIES_ERROR; @Slf4j -@Getter -public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculatedFieldState { +public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculatedFieldState implements ScheduledRefreshSupported { @Setter + @Getter private long lastArgsRefreshTs = DEFAULT_LAST_UPDATE_TS; @Setter + @Getter private long lastMetricsEvalTs = DEFAULT_LAST_UPDATE_TS; - @Setter private long lastRelatedEntitiesRefreshTs = DEFAULT_LAST_UPDATE_TS; private long deduplicationIntervalMs = DISABLED_INTERVAL_VALUE; private Map metrics; @@ -103,13 +105,24 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat @Override public void reset() { // must reset everything dependent on arguments super.reset(); + resetScheduledRefreshTs(); lastArgsRefreshTs = DEFAULT_LAST_UPDATE_TS; lastMetricsEvalTs = DEFAULT_LAST_UPDATE_TS; - lastRelatedEntitiesRefreshTs = DEFAULT_LAST_UPDATE_TS; metrics = null; } - public void updateLastRelatedEntitiesRefreshTs() { + @Override + public void resetScheduledRefreshTs() { + lastRelatedEntitiesRefreshTs = DEFAULT_LAST_UPDATE_TS; + } + + @Override + public long getLastScheduledRefreshTs() { + return lastRelatedEntitiesRefreshTs; + } + + @Override + public void updateScheduledRefreshTs() { lastRelatedEntitiesRefreshTs = System.currentTimeMillis(); } @@ -127,7 +140,7 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat public List checkRelatedEntities(List relatedEntities) { Map> entityInputs = prepareInputs(); findOutdatedEntities(entityInputs, relatedEntities).forEach(this::cleanupEntityData); - updateLastRelatedEntitiesRefreshTs(); + updateScheduledRefreshTs(); return findMissingEntities(entityInputs, relatedEntities); } @@ -165,6 +178,7 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat }); lastMetricsEvalTs = DEFAULT_LAST_UPDATE_TS; lastArgsRefreshTs = System.currentTimeMillis(); + readinessStatus = checkReadiness(); } public void scheduleReevaluation() { @@ -276,4 +290,18 @@ public class RelatedEntitiesAggregationCalculatedFieldState extends BaseCalculat record EntityArgument(EntityInfo entity, JsonNode entityArguments) {} + @Override + protected ReadinessStatus checkReadiness() { + if (arguments == null) { + return ReadinessStatus.notReady(MISSING_AGGREGATION_ENTITIES_ERROR); + } + for (String requiredArgumentKey : requiredArguments) { + ArgumentEntry argumentEntry = arguments.get(requiredArgumentKey); + if (argumentEntry == null || argumentEntry.isEmpty()) { + return ReadinessStatus.notReady(MISSING_AGGREGATION_ENTITIES_ERROR); + } + } + return ReadinessStatus.READY; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java index 721e93c6fc..0f6e01a344 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/aggregation/single/EntityAggregationCalculatedFieldState.java @@ -205,7 +205,7 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt handleExpiredInterval(intervalEntry, args, results); expiredIntervals.add(intervalEntry); } else if (now - startTs >= intervalEntry.getIntervalDuration()) { - handleActiveInterval(ctx.getCfCheckReevaluationInterval(), intervalEntry, args, results); + handleActiveInterval(ctx.getCfCheckReevaluationIntervalMillis(), intervalEntry, args, results); if (watermarkDuration == 0) { expiredIntervals.add(intervalEntry); } @@ -288,7 +288,7 @@ public class EntityAggregationCalculatedFieldState extends BaseCalculatedFieldSt } if (!metricsNode.isEmpty()) { ObjectNode resultNode = JacksonUtil.newObjectNode(); - resultNode.put("ts", interval.getEndTs() - 1); + resultNode.put("ts", interval.getStartTs()); resultNode.set("values", metricsNode); result.add(resultNode); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java index 18629bd370..4f2cbede09 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/alarm/AlarmCalculatedFieldState.java @@ -94,8 +94,6 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { private Alarm currentAlarm; private boolean initialFetchDone; - // TODO: deprecate device profile node, describe the differences and improvements - public AlarmCalculatedFieldState(EntityId entityId) { super(entityId); } @@ -358,7 +356,7 @@ public class AlarmCalculatedFieldState extends BaseCalculatedFieldState { newAlarm.setAcknowledged(false); newAlarm.setCleared(false); newAlarm.setSeverity(severity); - long startTs = latestTimestamp; + long startTs = getLatestTimestamp(); long currentTime = System.currentTimeMillis(); if (startTs <= 0L || startTs > currentTime) { startTs = currentTime; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java index ea47dafa59..e336672877 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java @@ -21,8 +21,6 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.geo.Coordinates; @@ -52,11 +50,9 @@ import static org.thingsboard.server.common.data.cf.configuration.geofencing.Ent import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.INSIDE; import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.OUTSIDE; -@Getter -@Setter @Slf4j @EqualsAndHashCode(callSuper = true) -public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { +public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState implements ScheduledRefreshSupported { private long lastDynamicArgumentsRefreshTs = DEFAULT_LAST_UPDATE_TS; @@ -147,10 +143,21 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { @Override public void reset() { super.reset(); + resetScheduledRefreshTs(); + } + + @Override + public void resetScheduledRefreshTs() { lastDynamicArgumentsRefreshTs = DEFAULT_LAST_UPDATE_TS; } - public void updateLastDynamicArgumentsRefreshTs() { + @Override + public long getLastScheduledRefreshTs() { + return lastDynamicArgumentsRefreshTs; + } + + @Override + public void updateScheduledRefreshTs() { lastDynamicArgumentsRefreshTs = System.currentTimeMillis(); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/ScheduledRefreshSupported.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/ScheduledRefreshSupported.java new file mode 100644 index 0000000000..f43959443a --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/ScheduledRefreshSupported.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2025 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.cf.ctx.state.geofencing; + +public interface ScheduledRefreshSupported { + + void resetScheduledRefreshTs(); + + long getLastScheduledRefreshTs(); + + void updateScheduledRefreshTs(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java index 0450a0599a..8536c0f65f 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationArgumentEntry.java @@ -22,6 +22,8 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; +import java.util.ArrayList; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -30,10 +32,11 @@ import java.util.Set; public class PropagationArgumentEntry implements ArgumentEntry { private Set entityIds; - private transient EntityId added; + private transient List added; private transient EntityId removed; - private boolean forceResetPrevious; + private transient boolean forceResetPrevious; + private transient boolean ignoreRemovedEntities; public PropagationArgumentEntry() { this.entityIds = new HashSet<>(); @@ -57,27 +60,44 @@ public class PropagationArgumentEntry implements ArgumentEntry { @Override public boolean updateEntry(ArgumentEntry entry) { - if (!(entry instanceof PropagationArgumentEntry propagationArgumentEntry)) { + if (!(entry instanceof PropagationArgumentEntry updated)) { throw new IllegalArgumentException("Unsupported argument entry type for propagation argument entry: " + entry.getType()); } - if (propagationArgumentEntry.getAdded() != null) { - boolean updated = entityIds.add(propagationArgumentEntry.getAdded()); - if (updated) { - added = propagationArgumentEntry.getAdded(); - } - return updated; + if (updated.getAdded() != null) { + return checkAdded(updated.getAdded()); + } + if (updated.getRemoved() != null) { + return entityIds.remove(updated.getRemoved()); } - if (propagationArgumentEntry.getRemoved() != null) { - return entityIds.remove(propagationArgumentEntry.getRemoved()); + if (updated.isIgnoreRemovedEntities()) { + Set updatedIds = updated.getEntityIds(); + if (updatedIds.isEmpty()) { + entityIds.clear(); + return false; + } + entityIds.retainAll(updatedIds); + return checkAdded(updatedIds); } - if (propagationArgumentEntry.isEmpty()) { + if (updated.isEmpty()) { entityIds.clear(); return true; } - entityIds = propagationArgumentEntry.getEntityIds(); + entityIds = updated.getEntityIds(); return true; } + private boolean checkAdded(Collection updatedIds) { + for (EntityId id : updatedIds) { + if (entityIds.add(id)) { + if (added == null) { + added = new ArrayList<>(); + } + added.add(id); + } + } + return added != null && !added.isEmpty(); + } + @Override public boolean isEmpty() { return entityIds.isEmpty(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java index 5a7753c86a..c4533aef37 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/propagation/PropagationCalculatedFieldState.java @@ -25,6 +25,8 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.util.CollectionsUtil; +import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.PropagationCalculatedFieldResult; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; @@ -41,6 +43,8 @@ import static org.thingsboard.server.common.data.cf.configuration.PropagationCal public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState { + private CalculatedFieldProcessingService cfProcessingService; + public PropagationCalculatedFieldState(EntityId entityId) { super(entityId); } @@ -49,14 +53,30 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState public void setCtx(CalculatedFieldCtx ctx, TbActorRef actorCtx) { this.ctx = ctx; this.actorCtx = actorCtx; + this.cfProcessingService = ctx.getCfProcessingService(); this.requiredArguments = new ArrayList<>(ctx.getArgNames()); requiredArguments.add(PROPAGATION_CONFIG_ARGUMENT); - this.readinessStatus = checkReadiness(requiredArguments, arguments); + this.readinessStatus = checkReadiness(); if (ctx.isApplyExpressionForResolvedArguments()) { this.tbelExpression = ctx.getTbelExpressions().get(ctx.getExpression()); } } + @Override + public void init(boolean restored) { + super.init(restored); + if (restored) { + cfProcessingService.fetchPropagationArgumentFromDb(ctx, entityId).ifPresent(fromDb -> { + fromDb.setIgnoreRemovedEntities(true); + var updatedArgs = update(Map.of(PROPAGATION_CONFIG_ARGUMENT, fromDb), ctx); + if (updatedArgs.isEmpty()) { + return; + } + ctx.scheduleReevaluation(0L, actorCtx); + }); + } + } + @Override public CalculatedFieldType getType() { return CalculatedFieldType.PROPAGATION; @@ -68,9 +88,10 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState if (!(argumentEntry instanceof PropagationArgumentEntry propagationArgumentEntry)) { return Futures.immediateFuture(PropagationCalculatedFieldResult.builder().build()); } + boolean newEntityAdded = propagationArgumentEntry.getAdded() != null; List entityIds; - if (propagationArgumentEntry.getAdded() != null) { - entityIds = List.of(propagationArgumentEntry.getAdded()); + if (newEntityAdded) { + entityIds = propagationArgumentEntry.getAdded(); propagationArgumentEntry.setAdded(null); } else { if (propagationArgumentEntry.getEntityIds().isEmpty()) { @@ -86,13 +107,43 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState .build(), MoreExecutors.directExecutor()); } + if (newEntityAdded || CollectionsUtil.isEmpty(updatedArgs)) { + updatedArgs = arguments; + } return Futures.immediateFuture(PropagationCalculatedFieldResult.builder() .entityIds(entityIds) - .result(toTelemetryResult(ctx)) + .result(toTelemetryResult(ctx, updatedArgs)) .build()); } - private TelemetryCalculatedFieldResult toTelemetryResult(CalculatedFieldCtx ctx) { + @Override + protected ReadinessStatus checkReadiness() { + if (ctx.isApplyExpressionForResolvedArguments() || arguments == null) { + return super.checkReadiness(); + } + boolean propagationNotEmpty = false; + boolean hasOtherNonEmpty = false; + List emptyArguments = null; + for (String requiredArgumentKey : requiredArguments) { + ArgumentEntry argumentEntry = arguments.get(requiredArgumentKey); + if (argumentEntry == null || argumentEntry.isEmpty()) { + if (emptyArguments == null) { + emptyArguments = new ArrayList<>(); + } + emptyArguments.add(requiredArgumentKey); + } else if (PROPAGATION_CONFIG_ARGUMENT.equals(requiredArgumentKey)) { + propagationNotEmpty = true; + } else { + hasOtherNonEmpty = true; + } + } + if (propagationNotEmpty && hasOtherNonEmpty) { + return ReadinessStatus.READY; + } + return ReadinessStatus.from(emptyArguments); + } + + private TelemetryCalculatedFieldResult toTelemetryResult(CalculatedFieldCtx ctx, Map updatedArgs) { Output output = ctx.getOutput(); TelemetryCalculatedFieldResult.TelemetryCalculatedFieldResultBuilder telemetryCfBuilder = TelemetryCalculatedFieldResult.builder() @@ -100,12 +151,14 @@ public class PropagationCalculatedFieldState extends ScriptCalculatedFieldState .type(output.getType()) .scope(output.getScope()); ObjectNode valuesNode = JacksonUtil.newObjectNode(); - arguments.forEach((outputKey, argumentEntry) -> { + updatedArgs.forEach((outputKey, argumentEntry) -> { if (argumentEntry instanceof PropagationArgumentEntry) { return; } if (argumentEntry instanceof SingleValueArgumentEntry singleArgumentEntry) { - JacksonUtil.addKvEntry(valuesNode, singleArgumentEntry.getKvEntryValue(), outputKey); + if (!singleArgumentEntry.isEmpty()) { + JacksonUtil.addKvEntry(valuesNode, singleArgumentEntry.getKvEntryValue(), outputKey); + } return; } throw new IllegalArgumentException("Unsupported argument type: " + argumentEntry.getType() + " detected for argument: " + outputKey + ". " + 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 782104cc9b..c8925f9ee9 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 @@ -258,8 +258,7 @@ public class DeviceBulkImportService extends AbstractBulkImportService { Lwm2mDeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration(); transportConfiguration.setBootstrap(Collections.emptyList()); - transportConfiguration.setClientLwM2mSettings(new OtherConfiguration(false,1, 1, 1, PowerMode.DRX, null, null, null, null, null, V1_0.toString())); - transportConfiguration.setObserveAttr(new TelemetryMappingConfiguration(Collections.emptyMap(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptyMap(), false, SINGLE)); + transportConfiguration.setClientLwM2mSettings(new OtherConfiguration()); DeviceProfileData deviceProfileData = new DeviceProfileData(); DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); 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 ec4a43310b..d523a9176c 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 @@ -206,7 +206,6 @@ public class EdgeContextComponent { @Autowired private Optional statsCounterService; - // processors @Autowired private AlarmProcessor alarmProcessor; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java index 505b03a1cd..6b50b8ae15 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtils.java @@ -21,6 +21,7 @@ import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; import com.google.gson.reflect.TypeToken; import lombok.extern.slf4j.Slf4j; @@ -52,6 +53,7 @@ import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.domain.DomainInfo; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.AssetId; @@ -76,6 +78,7 @@ import org.thingsboard.server.common.data.id.TenantId; 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.kv.AttributeKvEntry; import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.template.NotificationTemplate; @@ -88,6 +91,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; +import org.thingsboard.server.common.transport.util.JsonUtils; import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmCommentUpdateMsg; import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg; @@ -127,11 +131,16 @@ import org.thingsboard.server.gen.edge.v1.WidgetTypeUpdateMsg; import org.thingsboard.server.gen.edge.v1.WidgetsBundleUpdateMsg; import org.thingsboard.server.gen.transport.TransportProtos; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; @Slf4j public class EdgeMsgConstructorUtils { @@ -670,4 +679,128 @@ public class EdgeMsgConstructorUtils { .setIdLSB(aiModelId.getId().getLeastSignificantBits()).build(); } + public static List mergeAndFilterDownlinkDuplicates(List edgeEvents) { + try { + edgeEvents = removeDownlinkDuplicates(edgeEvents); + + List attrUpdateMsgs = new ArrayList<>(); + for (EdgeEvent edgeEvent : edgeEvents) { + if (EdgeEventActionType.ATTRIBUTES_UPDATED.equals(edgeEvent.getAction())) { + attrUpdateMsgs.add(new AttrUpdateMsg(edgeEvent.getEntityId(), edgeEvent.getBody())); + } + } + Map> latestTsByEntityAndKey = computeLatestTsByEntityAndKey(attrUpdateMsgs); + + List result = new ArrayList<>(); + for (EdgeEvent edgeEvent : edgeEvents) { + if (!EdgeEventActionType.ATTRIBUTES_UPDATED.equals(edgeEvent.getAction())) { + result.add(edgeEvent); + continue; + } + + Map latestByKey = latestTsByEntityAndKey.get(edgeEvent.getEntityId()); + JsonNode filteredBody = filterAttributesBody(edgeEvent.getBody(), latestByKey); + if (filteredBody == null) { + continue; + } + + result.add(createFilteredEdgeEvent(edgeEvent, filteredBody)); + } + + result.sort(Comparator.comparingLong(EdgeEvent::getSeqId)); + return result; + } catch (Exception e) { + log.warn("Can't merge downlink duplicates, edgeEvents [{}]", edgeEvents, e); + return edgeEvents; + } + } + + private static AttrsTs extractAttributes(JsonNode body) { + if (body == null) { + return new AttrsTs(0L, List.of()); + } + String bodyStr = JacksonUtil.toString(body); + var jsonObject = JsonParser.parseString(bodyStr).getAsJsonObject(); + long ts = jsonObject.get("ts").getAsLong(); + var kv = jsonObject.getAsJsonObject("kv"); + List attrs = JsonConverter.convertToAttributes( + JsonUtils.getJsonObject( + JsonConverter.convertToAttributesProto(kv).getKvList() + ), ts); + return new AttrsTs(ts, attrs); + } + + private static JsonNode filterAttributesBody(JsonNode body, Map latestByKey) { + if (body == null || latestByKey == null || latestByKey.isEmpty()) { + return null; + } + String bodyStr = JacksonUtil.toString(body); + JsonObject jsonObject = JsonParser.parseString(bodyStr).getAsJsonObject(); + long ts = jsonObject.get("ts").getAsLong(); + JsonObject kv = jsonObject.getAsJsonObject("kv"); + for (Iterator> it = kv.entrySet().iterator(); it.hasNext(); ) { + Map.Entry e = it.next(); + Long latestTs = latestByKey.get(e.getKey()); + if (latestTs == null || !latestTs.equals(ts)) { + it.remove(); + } + } + if (kv.isEmpty()) { + return null; + } + return JacksonUtil.toJsonNode(jsonObject.toString()); + } + + private static Map> computeLatestTsByEntityAndKey(List attrUpdateMsgs) { + Map> latestTsByEntityAndKey = new HashMap<>(); + for (AttrUpdateMsg attrUpdateMsg : attrUpdateMsgs) { + UUID entityId = attrUpdateMsg.entityId(); + AttrsTs attrsTs = extractAttributes(attrUpdateMsg.body()); + Map map = latestTsByEntityAndKey.computeIfAbsent(entityId, id -> new HashMap<>()); + long ts = attrsTs.ts(); + for (AttributeKvEntry attr : attrsTs.attrs()) { + map.merge(attr.getKey(), ts, Math::max); + } + } + return latestTsByEntityAndKey; + } + + private static EdgeEvent createFilteredEdgeEvent(EdgeEvent edgeEvent, JsonNode filteredBody) { + EdgeEvent filtered = new EdgeEvent(); + filtered.setSeqId(edgeEvent.getSeqId()); + filtered.setTenantId(edgeEvent.getTenantId()); + filtered.setEdgeId(edgeEvent.getEdgeId()); + filtered.setAction(edgeEvent.getAction()); + filtered.setEntityId(edgeEvent.getEntityId()); + filtered.setUid(edgeEvent.getUid()); + filtered.setType(edgeEvent.getType()); + filtered.setBody(filteredBody); + return filtered; + } + + private static List removeDownlinkDuplicates(List edgeEvents) { + Set seen = new HashSet<>(); + return edgeEvents.stream() + .filter(e -> seen.add(new EventKey( + e.getTenantId(), + e.getAction(), + e.getEntityId(), + e.getType().name(), + (e.getBody() != null ? e.getBody().toString() : "null")))) + .collect(Collectors.toList()); + } + + private record EventKey(TenantId tenantId, + EdgeEventActionType action, + UUID entityId, + String type, + String body) { + } + + private record AttrsTs(long ts, List attrs) { + } + + private record AttrUpdateMsg(UUID entityId, JsonNode body) { + } + } 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 14c1b5e30a..39a95d6e1a 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 @@ -117,7 +117,7 @@ public abstract class EdgeGrpcSession implements Closeable { private static final int MAX_DOWNLINK_ATTEMPTS = 3; private static final String RATE_LIMIT_REACHED = "Rate limit reached"; - protected static final ConcurrentLinkedQueue highPriorityQueue = new ConcurrentLinkedQueue<>(); + protected final ConcurrentLinkedQueue highPriorityQueue = new ConcurrentLinkedQueue<>(); protected UUID sessionId; private BiConsumer sessionOpenListener; @@ -306,7 +306,8 @@ public abstract class EdgeGrpcSession implements Closeable { if (isConnected() && !pageData.getData().isEmpty()) { if (fetcher instanceof GeneralEdgeEventFetcher) { long queueSize = pageData.getTotalElements() - ((long) pageLink.getPageSize() * pageLink.getPage()); - ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.setDownlinkMsgsLag(edge.getTenantId(), edge.getId(), queueSize)); + ctx.getStatsCounterService().ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_LAG, tenantId, edge.getId(), queueSize)); } log.trace("[{}][{}][{}] event(s) are going to be processed.", tenantId, edge.getId(), pageData.getData().size()); List downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData()); @@ -504,7 +505,8 @@ public abstract class EdgeGrpcSession implements Closeable { ctx.getRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId()) .customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg) .error("Failed to deliver messages after " + MAX_DOWNLINK_ATTEMPTS + " attempts").build()); - ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED, edge.getTenantId(), edge.getId(), copy.size())); + ctx.getStatsCounterService().ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED, edge.getTenantId(), edge.getId(), copy.size())); stopCurrentSendDownlinkMsgsTask(false); } } else { @@ -544,7 +546,8 @@ public abstract class EdgeGrpcSession implements Closeable { try { if (msg.getSuccess()) { sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId()); - ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PUSHED, edge.getTenantId(), edge.getId(), 1)); + ctx.getStatsCounterService().ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_PUSHED, edge.getTenantId(), edge.getId(), 1)); log.debug("[{}][{}][{}] Msg has been processed successfully! Msg Id: [{}], Msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg); } else { log.debug("[{}][{}][{}] Msg processing failed! Msg Id: [{}], Error msg: {}", tenantId, edge.getId(), sessionId, msg.getDownlinkMsgId(), msg.getErrorMsg()); @@ -651,7 +654,8 @@ public abstract class EdgeGrpcSession implements Closeable { protected List convertToDownlinkMsgsPack(List edgeEvents) { List result = new ArrayList<>(); - for (EdgeEvent edgeEvent : edgeEvents) { + List filtered = EdgeMsgConstructorUtils.mergeAndFilterDownlinkDuplicates(edgeEvents); + for (EdgeEvent edgeEvent : filtered) { log.trace("[{}][{}] converting edge event to downlink msg [{}]", tenantId, edge.getId(), edgeEvent); DownlinkMsg downlinkMsg = null; try { @@ -813,7 +817,8 @@ public abstract class EdgeGrpcSession implements Closeable { } } highPriorityQueue.add(edgeEvent); - ctx.getStatsCounterService().ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edge.getTenantId(), edgeEvent.getEdgeId(), 1)); + ctx.getStatsCounterService().ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edge.getTenantId(), edgeEvent.getEdgeId(), 1)); } protected ListenableFuture> processUplinkMsg(UplinkMsg uplinkMsg) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java index a8960abf0b..31f80b6c96 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java @@ -92,6 +92,7 @@ public class EdgeSyncCursor { fetchers.add(new NotificationTargetEdgeEventFetcher(ctx.getNotificationTargetService())); fetchers.add(new NotificationRuleEdgeEventFetcher(ctx.getNotificationRuleService())); fetchers.add(new OtaPackagesEdgeEventFetcher(ctx.getOtaPackageService())); + // sync device profiles twice to update software and hardware fields fetchers.add(new DeviceProfilesEdgeEventFetcher(ctx.getDeviceProfileService())); fetchers.add(new TenantResourcesEdgeEventFetcher(ctx.getResourceService())); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeEventService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeEventService.java index bc00ef4481..b4f7a7574f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeEventService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeEventService.java @@ -52,7 +52,8 @@ public class KafkaEdgeEventService extends BaseEdgeEventService { TopicPartitionInfo tpi = topicService.getEdgeEventNotificationsTopic(edgeEvent.getTenantId(), edgeEvent.getEdgeId()); ToEdgeEventNotificationMsg msg = ToEdgeEventNotificationMsg.newBuilder().setEdgeEventMsg(ProtoUtils.toProto(edgeEvent)).build(); producerProvider.getTbEdgeEventsMsgProducer().send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), msg), null); - statsCounterService.ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edgeEvent.getTenantId(), edgeEvent.getEdgeId(), 1)); + statsCounterService.ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edgeEvent.getTenantId(), edgeEvent.getEdgeId(), 1)); return Futures.immediateFuture(null); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java index 63669a8e3d..e6eaec23e4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/KafkaEdgeGrpcSession.java @@ -82,16 +82,18 @@ public class KafkaEdgeGrpcSession extends EdgeGrpcSession { edgeEvents.add(edgeEvent); } List downlinkMsgsPack = convertToDownlinkMsgsPack(edgeEvents); + boolean isInterrupted = true; try { - boolean isInterrupted = sendDownlinkMsgsPack(downlinkMsgsPack).get(); + isInterrupted = sendDownlinkMsgsPack(downlinkMsgsPack).get(); if (isInterrupted) { log.debug("[{}][{}] Send downlink messages task was interrupted", tenantId, edge.getId()); - } else { - consumer.commit(); } } catch (Exception e) { log.error("[{}][{}] Failed to process downlink messages", tenantId, edge.getId(), e); } + if (!isInterrupted) { + consumer.commit(); + } } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java index cf1c9d9abd..1b70b8bd32 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/ai/AiModelEdgeProcessor.java @@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.AiModelUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java index a4b35ab541..88e3113039 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/AssetEdgeProcessor.java @@ -33,7 +33,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java index e05b6f41c3..5c09f2507b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/asset/profile/AssetProfileEdgeProcessor.java @@ -33,7 +33,7 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java index cab4b5ecc1..5e7099c9d8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/cf/CalculatedFieldEdgeProcessor.java @@ -36,7 +36,7 @@ import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.CalculatedFieldUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java index b38a9d618e..23c7e3cbcb 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/dashboard/DashboardEdgeProcessor.java @@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DashboardId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index 925e903ab2..2f655d961b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -44,7 +44,7 @@ import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponseActorMsg; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java index 3355bc819f..540fd4b448 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/profile/DeviceProfileEdgeProcessor.java @@ -33,7 +33,7 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java index 3b6aafea74..46923c3434 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/entityview/EntityViewEdgeProcessor.java @@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.EntityViewUpdateMsg; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java index de31bf6a9c..28b6c49bc1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/resource/ResourceEdgeProcessor.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.ResourceUpdateMsg; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java index b949660e70..5b875b1b0f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java @@ -31,7 +31,7 @@ import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java index 3b2d76356b..4a02aeda1d 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java @@ -33,7 +33,7 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java b/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java index 48b2a47cfb..7f55d2817c 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/stats/EdgeStatsService.java @@ -54,7 +54,7 @@ import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_P import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_TMP_FAILED; @TbCoreComponent -@ConditionalOnProperty(prefix = "edges.stats", name = "enabled", havingValue = "true", matchIfMissing = false) +@ConditionalOnProperty(prefix = "edges.stats", name = "enabled", havingValue = "true") @RequiredArgsConstructor @Service @Slf4j @@ -70,7 +70,6 @@ public class EdgeStatsService { @Value("${edges.stats.report-interval-millis:600000}") private long reportIntervalMillis; - @Scheduled( fixedDelayString = "${edges.stats.report-interval-millis:600000}", initialDelayString = "${edges.stats.report-interval-millis:600000}" @@ -80,13 +79,13 @@ public class EdgeStatsService { long now = System.currentTimeMillis(); long ts = now - (now % reportIntervalMillis); - Map countersByEdge = statsCounterService.getCounterByEdge(); - Map lagByEdgeId = kafkaAdmin.isPresent() ? getEdgeLagByEdgeId(countersByEdge) : Collections.emptyMap(); - Map countersByEdgeSnapshot = new HashMap<>(statsCounterService.getCounterByEdge()); + Map countersByEdgeSnapshot = new HashMap<>(statsCounterService.getMsgCountersByEdge()); + boolean isKafkaStats = kafkaAdmin.isPresent(); + Map lagByEdgeId = isKafkaStats ? getLagByEdgeId(countersByEdgeSnapshot) : Collections.emptyMap(); countersByEdgeSnapshot.forEach((edgeId, counters) -> { TenantId tenantId = counters.getTenantId(); - if (kafkaAdmin.isPresent()) { + if (isKafkaStats) { counters.getMsgsLag().set(lagByEdgeId.getOrDefault(edgeId, 0L)); } List statsEntries = List.of( @@ -102,7 +101,7 @@ public class EdgeStatsService { }); } - private Map getEdgeLagByEdgeId(Map countersByEdge) { + private Map getLagByEdgeId(Map countersByEdge) { Map edgeToTopicMap = countersByEdge.entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index a55832c823..16e164877a 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -103,7 +103,7 @@ import org.thingsboard.server.dao.device.DeviceConnectivityConfiguration; 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.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.mobile.MobileAppDao; import org.thingsboard.server.dao.notification.NotificationSettingsService; import org.thingsboard.server.dao.notification.NotificationTargetService; 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 f9b5c282a8..ff813df156 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 @@ -39,7 +39,7 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.dashboard.DashboardService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.resource.ResourceService; @@ -406,7 +406,8 @@ public class InstallScripts { Path resourcesDir = Path.of(getDataDir(), RESOURCES_DIR); loadSystemResources(resourcesDir.resolve("images"), ResourceType.IMAGE, null); - loadSystemResources(resourcesDir.resolve("js_modules"), ResourceType.JS_MODULE, ResourceSubType.EXTENSION); + loadSystemResources(resourcesDir.resolve("js_extensions"), ResourceType.JS_MODULE, ResourceSubType.EXTENSION); + loadSystemResources(resourcesDir.resolve("js_modules"), ResourceType.JS_MODULE, ResourceSubType.MODULE); loadSystemResources(resourcesDir.resolve("dashboards"), ResourceType.DASHBOARD, null); } diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java index aa43d77439..a773d0ab23 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SqlEntityDatabaseSchemaService.java @@ -22,12 +22,13 @@ import org.springframework.stereotype.Service; @Service @Profile("install") @Slf4j -public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaService - implements EntityDatabaseSchemaService { +public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaService implements EntityDatabaseSchemaService { + public static final String SCHEMA_ENTITIES_SQL = "schema-entities.sql"; public static final String SCHEMA_ENTITIES_IDX_SQL = "schema-entities-idx.sql"; public static final String SCHEMA_ENTITIES_IDX_PSQL_ADDON_SQL = "schema-entities-idx-psql-addon.sql"; - public static final String SCHEMA_VIEWS_AND_FUNCTIONS_SQL = "schema-views-and-functions.sql"; + public static final String SCHEMA_VIEWS_SQL = "schema-views.sql"; + public static final String SCHEMA_FUNCTIONS_SQL = "schema-functions.sql"; public SqlEntityDatabaseSchemaService() { super(SCHEMA_ENTITIES_SQL, SCHEMA_ENTITIES_IDX_SQL); @@ -49,8 +50,10 @@ public class SqlEntityDatabaseSchemaService extends SqlAbstractDatabaseSchemaSer @Override public void createOrUpdateViewsAndFunctions() throws Exception { - log.info("Installing SQL DataBase schema views and functions: " + SCHEMA_VIEWS_AND_FUNCTIONS_SQL); - executeQueryFromFile(SCHEMA_VIEWS_AND_FUNCTIONS_SQL); + log.info("Installing SQL DataBase schema views: " + SCHEMA_VIEWS_SQL); + executeQueryFromFile(SCHEMA_VIEWS_SQL); + log.info("Installing SQL DataBase schema functions: " + SCHEMA_FUNCTIONS_SQL); + executeQueryFromFile(SCHEMA_FUNCTIONS_SQL); } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java index f4183dc904..bf9cece710 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java @@ -69,7 +69,7 @@ import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.resource.ResourceService; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java index 63f05f6255..0f2c4f346f 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsValidator.java @@ -21,7 +21,7 @@ import org.apache.commons.lang3.StringUtils; import org.bouncycastle.util.Arrays; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.security.model.JwtSettings; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import java.util.Base64; import java.util.Optional; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java index 3f7d711378..a742b8cd2a 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java @@ -29,11 +29,11 @@ import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoF 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.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.settings.AdminSettingsDao; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.user.UserAuthSettingsDao; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import java.util.Comparator; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java index e43a89f815..d137b8df40 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java @@ -33,7 +33,7 @@ import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; import org.thingsboard.server.dao.customer.CustomerService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.settings.SecuritySettingsService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.queue.util.TbCoreComponent; diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java index 69536f0953..7d4177e5b5 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java @@ -50,7 +50,7 @@ import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; import org.thingsboard.server.dao.audit.AuditLogService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.settings.SecuritySettingsService; import org.thingsboard.server.dao.user.UserService; diff --git a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java index 1a742c6b30..753cc9fd72 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java @@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.service.security.model.SecurityUser; public interface SystemSecurityService { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java index 506b48bf8c..cf0d58e5ba 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java @@ -31,7 +31,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityImportResult; import org.thingsboard.server.common.data.util.ThrowingRunnable; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.TbLogEntityActionService; diff --git a/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java b/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java index 204d2b5d6c..76af940685 100644 --- a/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java +++ b/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java @@ -16,7 +16,9 @@ package org.thingsboard.server.service.system; import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.base.Charsets; import com.google.common.hash.Hashing; +import com.google.common.io.Resources; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -34,6 +36,7 @@ import org.thingsboard.server.service.install.update.DefaultDataUpdateService; import java.io.IOException; import java.io.UncheckedIOException; +import java.net.URL; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; @@ -53,6 +56,8 @@ import java.util.stream.Stream; @RequiredArgsConstructor public class SystemPatchApplier { + private static final String SCHEMA_VIEWS_SQL = "sql/schema-views.sql"; + private static final long ADVISORY_LOCK_ID = 7536891047216478431L; private final JdbcTemplate jdbcTemplate; @@ -86,6 +91,9 @@ public class SystemPatchApplier { } try { + updateSqlViews(); + log.info("Updated sql database views"); + int updated = updateWidgetTypes(); log.info("Updated {} widget types", updated); @@ -124,6 +132,16 @@ public class SystemPatchApplier { && packageVersion.maintenance == dbVersion.maintenance && packageVersion.patch > dbVersion.patch; } + private void updateSqlViews() { + try { + URL schemaViewsUrl = Resources.getResource(SCHEMA_VIEWS_SQL); + String sql = Resources.toString(schemaViewsUrl, Charsets.UTF_8); + jdbcTemplate.execute(sql); + } catch (IOException e) { + throw new RuntimeException("Unable to update database views from schema-views.sql", e); + } + } + private int updateWidgetTypes() { AtomicInteger updated = new AtomicInteger(); Path widgetTypesDir = installScripts.getWidgetTypesDir(); diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 096c1a814a..58a5b951d5 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -22,6 +22,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.protobuf.ByteString; +import org.thingsboard.server.exception.EntitiesLimitExceededException; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; @@ -76,7 +77,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.server.dao.exception.EntitiesLimitExceededException; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java index 7046af3d9c..9b284e983f 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java @@ -33,6 +33,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ArgumentIntervalProt import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldEntityCtxIdProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldIdProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; +import org.thingsboard.server.gen.transport.TransportProtos.EntityIdProto; import org.thingsboard.server.gen.transport.TransportProtos.GeofencingArgumentProto; import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneProto; import org.thingsboard.server.gen.transport.TransportProtos.SingleValueArgumentProto; @@ -61,6 +62,7 @@ import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgume import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationCalculatedFieldState; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.TreeMap; @@ -108,10 +110,15 @@ public class CalculatedFieldUtils { case SINGLE_VALUE -> builder.addSingleValueArguments(toSingleValueArgumentProto(argName, (SingleValueArgumentEntry) argEntry)); case TS_ROLLING -> builder.addRollingValueArguments(toRollingArgumentProto(argName, (TsRollingArgumentEntry) argEntry)); case GEOFENCING -> builder.addGeofencingArguments(toGeofencingArgumentProto(argName, (GeofencingArgumentEntry) argEntry)); + case PROPAGATION -> builder.addAllPropagationEntityIds(toPropagationEntityIdsProto((PropagationArgumentEntry) argEntry)); case RELATED_ENTITIES -> { RelatedEntitiesArgumentEntry relatedEntitiesArgumentEntry = (RelatedEntitiesArgumentEntry) argEntry; - relatedEntitiesArgumentEntry.getEntityInputs() - .forEach((entityId, entry) -> builder.addSingleValueArguments(toSingleValueArgumentProto(argName, (SingleValueArgumentEntry) entry))); + Map entityInputs = relatedEntitiesArgumentEntry.getEntityInputs(); + if (entityInputs.isEmpty()) { + builder.addSingleValueArguments(SingleValueArgumentProto.newBuilder().setArgName(argName).build()); + } else { + entityInputs.forEach((entityId, entry) -> builder.addSingleValueArguments(toSingleValueArgumentProto(argName, (SingleValueArgumentEntry) entry))); + } } case ENTITY_AGGREGATION -> { EntityAggregationArgumentEntry entityAggregationArgumentEntry = (EntityAggregationArgumentEntry) argEntry; @@ -136,6 +143,10 @@ public class CalculatedFieldUtils { return builder.build(); } + private static List toPropagationEntityIdsProto(PropagationArgumentEntry argEntry) { + return argEntry.getEntityIds().stream().map(ProtoUtils::toProto).collect(Collectors.toList()); + } + private static AlarmRuleStateProto toAlarmRuleStateProto(AlarmRuleState ruleState) { return AlarmRuleStateProto.newBuilder() .setSeverity(Optional.ofNullable(ruleState.getSeverity()).map(Enum::name).orElse("")) @@ -234,7 +245,10 @@ public class CalculatedFieldUtils { Map> arguments = new HashMap<>(); proto.getSingleValueArgumentsList().forEach(argProto -> { SingleValueArgumentEntry entry = fromSingleValueArgumentProto(argProto); - arguments.computeIfAbsent(argProto.getArgName(), name -> new HashMap<>()).put(entry.getEntityId(), entry); + Map entityInputs = arguments.computeIfAbsent(argProto.getArgName(), name -> new HashMap<>()); + if (entry.getEntityId() != null) { + entityInputs.put(entry.getEntityId(), entry); + } }); arguments.forEach((argName, entityInputs) -> { relatedEntitiesAggState.getArguments().put(argName, new RelatedEntitiesArgumentEntry(entityInputs, false)); @@ -268,7 +282,10 @@ public class CalculatedFieldUtils { state.getArguments().put(argProto.getKey(), fromRollingArgumentProto(argProto))); case GEOFENCING -> proto.getGeofencingArgumentsList().forEach(argProto -> state.getArguments().put(argProto.getArgName(), fromGeofencingArgumentProto(argProto))); - case PROPAGATION -> state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry()); + case PROPAGATION -> { + List propagationEntityIds = proto.getPropagationEntityIdsList().stream().map(ProtoUtils::fromProto).toList(); + state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(propagationEntityIds)); + } case ALARM -> { AlarmCalculatedFieldState alarmState = (AlarmCalculatedFieldState) state; AlarmStateProto alarmStateProto = proto.getAlarmState(); diff --git a/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java b/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java index 172a3e8e2d..c72620bb8f 100644 --- a/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java @@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.lwm2m.LwM2mInstance; import org.thingsboard.server.common.data.lwm2m.LwM2mObject; import org.thingsboard.server.common.data.lwm2m.LwM2mResourceObserve; import org.thingsboard.server.common.data.util.TbDDFFileParser; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import java.io.ByteArrayInputStream; import java.io.IOException; diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 66669a8280..787111c28e 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -516,14 +516,14 @@ actors: response_timeout_ms: "${ACTORS_RPC_RESPONSE_TIMEOUT_MS:30000}" # Close transport session if RPC delivery timed out. If enabled, RPC will be reverted to the queued state. # Note: - # - For MQTT transport: - # - QoS level 0: This feature does not apply, as no acknowledgment is expected, and therefore no timeout is triggered. - # - QoS level 1: This feature applies, as an acknowledgment is expected. - # - QoS level 2: Unsupported. - # - For CoAP transport: - # - Confirmable requests: This feature applies, as delivery confirmation is expected. - # - Non-confirmable requests: This feature does not apply, as no delivery acknowledgment is expected. - # - For HTTP and SNPM transports: RPC is considered delivered immediately, and there is no logic to await acknowledgment. + #
  • For MQTT transport: + #
    • QoS level 0: This feature does not apply, as no acknowledgment is expected, and therefore no timeout is triggered.
    • + #
    • QoS level 1: This feature applies, as an acknowledgment is expected.
    • + #
    • QoS level 2: Unsupported.
  • + #
  • For CoAP transport: + #
    • Confirmable requests: This feature applies, as delivery confirmation is expected.
    • + #
    • Non-confirmable requests: This feature does not apply, as no delivery acknowledgment is expected.
  • + #
  • For HTTP and SNPM transports: RPC is considered delivered immediately, and there is no logic to await acknowledgment.
close_session_on_rpc_delivery_timeout: "${ACTORS_RPC_CLOSE_SESSION_ON_RPC_DELIVERY_TIMEOUT:false}" statistics: # Enable/disable actor statistics @@ -733,13 +733,13 @@ redis: # if set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" sentinel: - # name of the master node + # Name of the master node master: "${REDIS_MASTER:}" - # comma-separated list of "host:port" pairs of sentinels + # Comma-separated list of "host:port" pairs of sentinels sentinels: "${REDIS_SENTINELS:}" - # password to authenticate with sentinel + # Password to authenticate with sentinel password: "${REDIS_SENTINEL_PASSWORD:}" - # if set false will be used pool config build from values of the pool config section + # If set false will be used pool config build from values of the pool config section useDefaultPoolConfig: "${REDIS_USE_DEFAULT_POOL_CONFIG:true}" # db index db: "${REDIS_DB:0}" @@ -1039,10 +1039,10 @@ transport: activity: # This property specifies the strategy for reporting activity events within each reporting period. # The accepted values are 'FIRST', 'LAST', 'FIRST_AND_LAST' and 'ALL'. - # - 'FIRST': Only the first activity event in each reporting period is reported. - # - 'LAST': Only the last activity event in the reporting period is reported. - # - 'FIRST_AND_LAST': Both the first and last activity events in the reporting period are reported. - # - 'ALL': All activity events in the reporting period are reported. + #
  • 'FIRST': Only the first activity event in each reporting period is reported.
  • + #
  • 'LAST': Only the last activity event in the reporting period is reported.
  • + #
  • 'FIRST_AND_LAST': Both the first and last activity events in the reporting period are reported.
  • + #
  • 'ALL': All activity events in the reporting period are reported.
reporting_strategy: "${TB_TRANSPORT_ACTIVITY_REPORTING_STRATEGY:LAST}" json: # Cast String data types to Numeric if possible when processing Telemetry/Attributes JSON @@ -1160,15 +1160,15 @@ transport: dtls: # RFC7925_RETRANSMISSION_TIMEOUT_IN_MILLISECONDS = 9000 retransmission_timeout: "${LWM2M_DTLS_RETRANSMISSION_TIMEOUT_MS:9000}" - # CoAP DTLS connection ID length for LWM2M. RFC 9146, Connection Identifier for DTLS 1.2 - # Default: off + # LWM2M DTLS connection ID length for LWM2M. RFC 9146, Connection Identifier for DTLS 1.2 + # Default: off.
# Control usage of DTLS connection ID length (CID). - # - 'off' to deactivate it. - # - 'on' to activate Connection ID support (same as CID 0 or more 0). - # - A positive value defines generated CID size in bytes. - # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). - # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used - # - A value that are > 4: MultiNodeConnectionIdGenerator is used + #
  • 'off' to deactivate it.
  • + #
  • 'on' to activate Connection ID support (same as CID 0 or more 0).
  • + #
  • A positive value defines generated CID size in bytes.
  • + #
  • A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic).
  • + #
  • A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
  • + #
  • A value that are > 4: MultiNodeConnectionIdGenerator is used
connection_id_length: "${LWM2M_DTLS_CONNECTION_ID_LENGTH:8}" server: # LwM2M Server ID @@ -1351,14 +1351,14 @@ coap: # CoAP DTLS bind port bind_port: "${COAP_DTLS_BIND_PORT:5684}" # CoAP DTLS connection ID length. RFC 9146, Connection Identifier for DTLS 1.2 - # Default: off + # Default: off.
# Control usage of DTLS connection ID length (CID). - # - 'off' to deactivate it. - # - 'on' to activate Connection ID support (same as CID 0 or more 0). - # - A positive value defines generated CID size in bytes. - # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). - # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used - # - A value that are > 4: MultiNodeConnectionIdGenerator is used + #
  • 'off' to deactivate it.
  • + #
  • 'on' to activate Connection ID support (same as CID 0 or more 0).
  • + #
  • A positive value defines generated CID size in bytes.
  • + #
  • A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic).
  • + #
  • A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
  • + #
  • A value that are > 4: MultiNodeConnectionIdGenerator is used
connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:8}" # Specify the MTU (Maximum Transmission Unit). # Should be used if LAN MTU is not used, e.g. if IP tunnels are used or if the client uses a smaller value than the LAN MTU. @@ -1375,13 +1375,13 @@ coap: # In order to negotiate smaller maximum fragment lengths, # clients MAY include an extension of type "max_fragment_length" in the (extended) client hello. # The "extension_data" field of this extension SHALL contain: - # enum { + #
 enum {
     #   2^9(1) == 512,
     #   2^10(2) == 1024,
     #   2^11(3) == 2048,
     #   2^12(4) == 4096,
     #   (255)
-    # } MaxFragmentLength;
+    # } MaxFragmentLength; 
# TLS already requires clients and servers to support fragmentation of handshake messages. max_fragment_length: "${COAP_DTLS_MAX_FRAGMENT_LENGTH:1024}" # Server DTLS credentials @@ -1745,6 +1745,8 @@ queue: print-interval-ms: "${TB_QUEUE_KAFKA_CONSUMER_STATS_MIN_PRINT_INTERVAL_MS:60000}" # Time to wait for the stats-loading requests to Kafka to finish kafka-response-timeout-ms: "${TB_QUEUE_KAFKA_CONSUMER_STATS_RESPONSE_TIMEOUT_MS:1000}" + # Topics cache TTL in milliseconds. 5 minutes by default + topics_cache_ttl_ms: "${TB_QUEUE_KAFKA_TOPICS_CACHE_TTL_MS:300000}" partitions: hash_function_name: "${TB_QUEUE_PARTITIONS_HASH_FUNCTION_NAME:murmur3_128}" # murmur3_32, murmur3_128 or sha256 transport_api: diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index f0008d6cfb..5996e14f2e 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -1089,7 +1089,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes // Telemetry on device doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", - JacksonUtil.toJsonNode("{\"temperature\":12.5}")).andExpect(status().isOk()); + JacksonUtil.toJsonNode("{\"temperature\":12.5, \"humidity\":85}")).andExpect(status().isOk()); // --- Build CF: PROPAGATION with expression --- CalculatedField cf = new CalculatedField(); @@ -1102,11 +1102,14 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); cfg.setApplyExpressionToResolvedArguments(true); - Argument arg = new Argument(); - arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); - cfg.setArguments(Map.of("t", arg)); + Argument arg1 = new Argument(); + arg1.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); - cfg.setExpression("{\"testResult\": t * 2}"); + Argument arg2 = new Argument(); + arg2.setRefEntityKey(new ReferencedEntityKey("humidity", ArgumentType.TS_LATEST, null)); + + cfg.setArguments(Map.of("t", arg1, "h", arg2)); + cfg.setExpression("return { testResult: (t + h) / 2};"); AttributesOutput output = new AttributesOutput(); output.setScope(AttributeScope.SERVER_SCOPE); @@ -1125,8 +1128,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes ArrayNode attrs2 = getServerAttributes(asset2.getId(), "testResult"); assertThat(attrs1).isNotNull(); assertThat(attrs2).isNotNull(); - assertThat(attrs1.get(0).get("value").asDouble()).isEqualTo(25.0); - assertThat(attrs2.get(0).get("value").asDouble()).isEqualTo(25.0); + assertThat(attrs1.get(0).get("value").asDouble()).isEqualTo(48.75); + assertThat(attrs2.get(0).get("value").asDouble()).isEqualTo(48.75); }); String deleteUrl = String.format("/api/v2/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", @@ -1148,7 +1151,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes ArrayNode attrs2 = getServerAttributes(asset2.getId(), "testResult"); assertThat(attrs1).isNullOrEmpty(); assertThat(attrs2).isNotNull(); - assertThat(attrs2.get(0).get("value").asDouble()).isEqualTo(50); + assertThat(attrs2.get(0).get("value").asDouble()).isEqualTo(55); }); } @@ -1167,7 +1170,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes // Telemetry on device long ts = System.currentTimeMillis() - 300000L; - postTelemetry(device.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":12.5}}", ts)); + postTelemetry(device.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":12.5, \"humidity\":85}}", ts)); // --- Build CF: PROPAGATION without expression --- CalculatedField cf = new CalculatedField(); @@ -1180,9 +1183,12 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes cfg.setRelation(new RelationPathLevel(EntitySearchDirection.TO, EntityRelation.CONTAINS_TYPE)); cfg.setApplyExpressionToResolvedArguments(false); // arguments-only mode - Argument arg = new Argument(); - arg.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); - cfg.setArguments(Map.of("temperatureComputed", arg)); + Argument arg1 = new Argument(); + arg1.setRefEntityKey(new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null)); + Argument arg2 = new Argument(); + arg2.setRefEntityKey(new ReferencedEntityKey("humidity", ArgumentType.TS_LATEST, null)); + + cfg.setArguments(Map.of("temperatureComputed", arg1, "humidityComputed", arg2)); TimeSeriesOutput output = new TimeSeriesOutput(); output.setStrategy(new TimeSeriesImmediateOutputStrategy(0, true, true, true, true)); @@ -1197,14 +1203,18 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes .atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { - ObjectNode telemetry1 = getLatestTelemetry(asset1.getId(), "temperatureComputed"); - ObjectNode telemetry2 = getLatestTelemetry(asset2.getId(), "temperatureComputed"); + ObjectNode telemetry1 = getLatestTelemetry(asset1.getId(), "temperatureComputed,humidityComputed"); + ObjectNode telemetry2 = getLatestTelemetry(asset2.getId(), "temperatureComputed,humidityComputed"); assertThat(telemetry1).isNotNull(); assertThat(telemetry2).isNotNull(); assertThat(telemetry1.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); assertThat(telemetry1.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(12.5); + assertThat(telemetry1.get("humidityComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); + assertThat(telemetry1.get("humidityComputed").get(0).get("value").asDouble()).isEqualTo(85); assertThat(telemetry2.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); assertThat(telemetry2.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(12.5); + assertThat(telemetry2.get("humidityComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); + assertThat(telemetry2.get("humidityComputed").get(0).get("value").asDouble()).isEqualTo(85); }); String deleteUrl = String.format("/api/v2/relation?fromId=%s&fromType=%s&relationType=%s&toId=%s&toType=%s", @@ -1212,10 +1222,10 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes EntityRelation.CONTAINS_TYPE, device.getId().getId(), EntityType.DEVICE ); doDelete(deleteUrl).andExpect(status().isOk()); - doDelete("/api/plugins/telemetry/ASSET/" + asset1.getId() + "/timeseries/delete?keys=temperatureComputed&deleteAllDataForKeys=true").andExpect(status().isOk()); + doDelete("/api/plugins/telemetry/ASSET/" + asset1.getId() + "/timeseries/delete?keys=temperatureComputed,humidityComputed&deleteAllDataForKeys=true").andExpect(status().isOk()); - // Update telemetry on device - long newTs = System.currentTimeMillis() - 300000L; + // Update telemetry on the device + long newTs = ts + 300000L; postTelemetry(device.getId(), String.format("{\"ts\": %s, \"values\": {\"temperature\":25}}", newTs)); // --- Assert propagated calculation (arguments-only mode after update) --- @@ -1223,13 +1233,18 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes .atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { - ObjectNode telemetry1 = getLatestTelemetry(asset1.getId(), "temperatureComputed"); - ObjectNode telemetry2 = getLatestTelemetry(asset2.getId(), "temperatureComputed"); + ObjectNode telemetry1 = getLatestTelemetry(asset1.getId(), "temperatureComputed,humidityComputed"); + ObjectNode telemetry2 = getLatestTelemetry(asset2.getId(), "temperatureComputed,humidityComputed"); assertThat(telemetry1).isNotNull(); assertThat(telemetry2).isNotNull(); assertThat(telemetry1.get("temperatureComputed").get(0).get("value")).isEqualTo(NullNode.instance); + assertThat(telemetry1.get("humidityComputed").get(0).get("value")).isEqualTo(NullNode.instance); + assertThat(telemetry2.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(newTs)); assertThat(telemetry2.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(25); + // TS for humidity is not updated -> expected + assertThat(telemetry2.get("humidityComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(ts)); + assertThat(telemetry2.get("humidityComputed").get(0).get("value").asDouble()).isEqualTo(85); }); Asset asset3 = createAsset("Propagated Asset 3", null); @@ -1241,10 +1256,12 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes .atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { - ObjectNode telemetry = getLatestTelemetry(asset3.getId(), "temperatureComputed"); + ObjectNode telemetry = getLatestTelemetry(asset3.getId(), "temperatureComputed,humidityComputed"); assertThat(telemetry).isNotNull(); assertThat(telemetry.get("temperatureComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(newTs)); assertThat(telemetry.get("temperatureComputed").get(0).get("value").asDouble()).isEqualTo(25); + assertThat(telemetry.get("humidityComputed").get(0).get("ts").asText()).isEqualTo(Long.toString(newTs)); + assertThat(telemetry.get("humidityComputed").get(0).get("value").asDouble()).isEqualTo(85); }); } @@ -1298,6 +1315,83 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); } + @Test + public void testCalculatedFieldWhenBatchOfTelemetrySent() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + long now = System.currentTimeMillis(); + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"a\":5, \"b\":10}}", now - TimeUnit.MINUTES.toMillis(3)))); + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format("{\"ts\": %s, \"values\": {\"b\":20}}", now - TimeUnit.MINUTES.toMillis(1)))); + + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(testDevice.getId()); + calculatedField.setType(CalculatedFieldType.SCRIPT); + calculatedField.setName("Script CF"); + calculatedField.setDebugSettings(DebugSettings.all()); + + ScriptCalculatedFieldConfiguration config = new ScriptCalculatedFieldConfiguration(); + + ReferencedEntityKey refEntityKeyA = new ReferencedEntityKey("a", ArgumentType.TS_LATEST, null); + Argument argumentA = new Argument(); + argumentA.setRefEntityKey(refEntityKeyA); + Argument argumentB = new Argument(); + ReferencedEntityKey refEntityKeyB = new ReferencedEntityKey("b", ArgumentType.TS_ROLLING, null); + argumentB.setTimeWindow(TimeUnit.MINUTES.toMillis(10)); + argumentB.setLimit(1000); + argumentB.setRefEntityKey(refEntityKeyB); + config.setArguments(Map.of("a", argumentA, "b", argumentB)); + config.setExpression(""" + return { + "latestA": a, + "avgB": b.avg + }; + """); + + config.setOutput(new TimeSeriesOutput()); + + calculatedField.setConfiguration(config); + + doPost("/api/calculatedField", calculatedField, CalculatedField.class); + + await().alias("create CF -> perform initial calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode result = getLatestTelemetry(testDevice.getId(), "latestA", "avgB"); + assertThat(result).isNotNull(); + assertThat(result.get("latestA").get(0).get("value").asText()).isEqualTo("5"); + assertThat(result.get("avgB").get(0).get("value").asText()).isEqualTo("15.0"); + }); + + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode(String.format(""" + [{ + "ts": %s, + "values": { + "a": 6, + "b": 100 + } + }, { + "ts": %s, + "values": { + "a": 7, + "b": 200 + } + }, { + "ts": %s, + "values": { + "a": 8, + "b": 300 + } + }]""", now - TimeUnit.MINUTES.toMillis(2), now, now - TimeUnit.MINUTES.toMillis(5)))); + + await().alias("update telemetry -> recalculate state").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode result = getLatestTelemetry(testDevice.getId(), "latestA", "avgB"); + assertThat(result).isNotNull(); + assertThat(result.get("latestA").get(0).get("value").asText()).isEqualTo("7"); + assertThat(result.get("avgB").get(0).get("value").asText()).isEqualTo("126.0"); + }); + } + @Test public void testSimpleCalculatedFieldWhenSkipRuleEngineOutputProcessing() throws Exception { Device testDevice = createDevice("Test device", "1234567890"); 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 80e79280df..b9c6e17ded 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -1209,7 +1209,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { Map statesMap = (Map) ReflectionTestUtils.getField(processor, "states"); Awaitility.await("CF state for entity actor ready to refresh dynamic arguments").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> { CalculatedFieldState calculatedFieldState = statesMap.get(cfId); - boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastDynamicArgumentsRefreshTs() < + boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastScheduledRefreshTs() < System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval); log.warn("entityId {}, cfId {}, state ready to refresh == {}", entityId, cfId, isReady); return isReady; @@ -1441,7 +1441,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { EntityType entityType, List entities, List names) throws Exception { - return doGetTypedWithPageLink("/api/calculatedFields?type=" + type + "&" + + return doGetTypedWithPageLink("/api/calculatedFields?" + + (type != null ? "types=" + type + "&" : "") + (entityType != null ? "entityType=" + entityType + "&" : "") + (entities != null ? "entities=" + String.join(",", entities.stream().map(UUID::toString).toList()) + "&" : "") + diff --git a/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java index e160fff59e..d039d7a26d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AssetControllerTest.java @@ -50,7 +50,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; 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.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.service.DaoSqlTest; diff --git a/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java index 14ac8c91d1..a1efe16381 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AssetProfileControllerTest.java @@ -42,7 +42,7 @@ 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.asset.AssetProfileDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; diff --git a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java index 33f6bfa250..cc9098d492 100644 --- a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java @@ -259,6 +259,8 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { List allCalculatedFields = getCalculatedFields(CalculatedFieldType.SIMPLE, null, null, null); assertThat(allCalculatedFields).contains(deviceCalculatedField, profileCalculatedField); + allCalculatedFields = getCalculatedFields(null, null, null, null); + assertThat(allCalculatedFields).contains(deviceCalculatedField, profileCalculatedField); List profileLevelCalculatedFields = getCalculatedFields(CalculatedFieldType.SIMPLE, EntityType.DEVICE_PROFILE, null, null); diff --git a/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java index 95cf62f904..be99343459 100644 --- a/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java @@ -44,7 +44,7 @@ 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 org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; diff --git a/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java index 052ff8777b..712b9f1472 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java @@ -51,7 +51,7 @@ 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 org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java index 78679948ab..efbf18d5f5 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java @@ -72,7 +72,7 @@ import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportColumn import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportRequest; import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportResult; import org.thingsboard.server.dao.device.DeviceDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.service.DaoSqlTest; diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java index fb0e3fb57a..970461480d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/DeviceProfileControllerTest.java @@ -52,7 +52,7 @@ 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 org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; diff --git a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java index f1d3ef1d52..217bba43bb 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java @@ -72,7 +72,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.dao.edge.EdgeDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.edge.imitator.EdgeImitator; diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java index c9b63b01a6..fdff81799f 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityViewControllerTest.java @@ -63,7 +63,7 @@ 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.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.service.DaoSqlTest; diff --git a/application/src/test/java/org/thingsboard/server/controller/OtaPackageControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/OtaPackageControllerTest.java index 3fc839996c..7df2b84c43 100644 --- a/application/src/test/java/org/thingsboard/server/controller/OtaPackageControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/OtaPackageControllerTest.java @@ -37,7 +37,7 @@ 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.security.Authority; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import java.nio.ByteBuffer; diff --git a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java index 72a6697927..a02332812b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java @@ -47,7 +47,7 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.rule.RuleChainDao; import org.thingsboard.server.dao.service.DaoSqlTest; diff --git a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java index 0875551112..5e880fb957 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbResourceControllerTest.java @@ -48,7 +48,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; diff --git a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java index f5f51a0c6b..8fef7f3e1d 100644 --- a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java @@ -50,7 +50,7 @@ import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.settings.StarredDashboardInfo; import org.thingsboard.server.common.data.settings.UserDashboardsInfo; import org.thingsboard.server.dao.device.DeviceService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.user.UserDao; diff --git a/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java index 0b1c2d282e..f84e5eb6e7 100644 --- a/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java @@ -29,7 +29,7 @@ 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.common.data.widget.WidgetsBundle; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.ArrayList; diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index 84d879c993..ef11a4dcf8 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -22,6 +22,7 @@ import com.google.protobuf.AbstractMessage; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.MessageLite; import lombok.extern.slf4j.Slf4j; +import org.awaitility.Awaitility; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -102,6 +103,7 @@ import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.TreeMap; import java.util.UUID; @@ -738,4 +740,27 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { return rpc; } + protected void verifyEdgeDisconnected() { + verifyEdgeActiveFlag(false); + } + + protected void verifyEdgeConnected() { + verifyEdgeActiveFlag(true); + } + + private void verifyEdgeActiveFlag(boolean value) { + Awaitility.await() + .atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> { + List> values = doGetAsyncTyped("/api/plugins/telemetry/EDGE/" + edge.getId() + + "/values/attributes/SERVER_SCOPE", new TypeReference<>() {}); + Optional> activeAttrOpt = values.stream().filter(att -> att.get("key").equals("active")).findFirst(); + if (activeAttrOpt.isEmpty()) { + return false; + } + Map activeAttr = activeAttrOpt.get(); + return Boolean.toString(value).equals(activeAttr.get("value").toString()); + }); + } + } diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java index 8d79416022..380dec8015 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java @@ -893,18 +893,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { ObjectNode attributes = JacksonUtil.newObjectNode(); attributes.put("active", true); doPost("/api/plugins/telemetry/EDGE/" + edge.getId() + "/attributes/" + DataConstants.SERVER_SCOPE, attributes); - Awaitility.await() - .atMost(TIMEOUT, TimeUnit.SECONDS) - .until(() -> { - List> values = doGetAsyncTyped("/api/plugins/telemetry/EDGE/" + edge.getId() + - "/values/attributes/SERVER_SCOPE", new TypeReference<>() {}); - Optional> activeAttrOpt = values.stream().filter(att -> att.get("key").equals("active")).findFirst(); - if (activeAttrOpt.isEmpty()) { - return false; - } - Map activeAttr = activeAttrOpt.get(); - return "true".equals(activeAttr.get("value").toString()); - }); + verifyEdgeConnected(); } } diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java index a29d7a4672..cbac977835 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DeviceProfileEdgeTest.java @@ -151,14 +151,20 @@ public class DeviceProfileEdgeTest extends AbstractEdgeTest { // delete profile when edge is offline edgeImitator.disconnect(); + verifyEdgeDisconnected(); + doDelete("/api/deviceProfile/" + deviceProfile.getUuidId()) .andExpect(status().isOk()); - edgeImitator.connect(); // 25 sync message - // + 2 RuleChain and RuleChainMetadata - // + 1 delete DeviceProfile + // + 1 RuleChain Added + // + 1 RuleChainMetadata Added + // + 1 DeviceProfile Delete edgeImitator.expectMessageAmount(SYNC_MESSAGE_COUNT + 3); + + edgeImitator.connect(); + verifyEdgeConnected(); + Assert.assertTrue(edgeImitator.waitForMessages()); latestMessage = edgeImitator.getLatestMessage(); diff --git a/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java b/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java new file mode 100644 index 0000000000..71722ad2a8 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/edge/EdgeStatsIntegrationTest.java @@ -0,0 +1,148 @@ +/** + * Copyright © 2016-2025 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.edge; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.dao.edge.stats.EdgeStatsCounterService; +import org.thingsboard.server.dao.edge.stats.EdgeStatsKey; +import org.thingsboard.server.dao.edge.stats.MsgCounters; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.service.edge.stats.EdgeStatsService; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_ADDED; +import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_PERMANENTLY_FAILED; +import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_PUSHED; +import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_TMP_FAILED; + +@DaoSqlTest +@Slf4j +public class EdgeStatsIntegrationTest extends AbstractEdgeTest { + + private static final String STATISTICS_DEVICE_PROFILE = "STATISTICS"; + + private static final long EXPECTED_MSGS_ADDED = 6L; + private static final long EXPECTED_MSGS_PUSHED = 6L; + private static final long EXPECTED_MSGS_PERMANENTLY_FAILED = 0L; + private static final long EXPECTED_MSGS_TMP_FAILED = 0L; + + @Autowired + private EdgeStatsService edgeStatsService; + @Autowired + private EdgeStatsCounterService statsCounterService; + + @Test + public void testReportStats() throws Exception { + // GIVEN + simulateEdgeEventsAddedDownlinkPushed(); + + // Await Edge Counters Updated + await().atMost(10, TimeUnit.SECONDS).pollInterval(Duration.ofMillis(200)).untilAsserted(() -> { + MsgCounters counters = statsCounterService.getMsgCountersByEdge().get(edge.getId()); + assertEquals(EXPECTED_MSGS_ADDED, counters.getMsgsAdded().get()); + assertEquals(EXPECTED_MSGS_PUSHED, counters.getMsgsPushed().get()); + assertEquals(EXPECTED_MSGS_PERMANENTLY_FAILED, counters.getMsgsPermanentlyFailed().get()); + assertEquals(EXPECTED_MSGS_TMP_FAILED, counters.getMsgsTmpFailed().get()); + }); + + Thread.sleep(1000); + + // WHEN + edgeStatsService.reportStats(); + + // THEN + await().atMost(10, TimeUnit.SECONDS).pollInterval(Duration.ofMillis(200)).untilAsserted(() -> { + List actualStats = fetchLatestStats(); + assertEquals(EXPECTED_MSGS_ADDED, getStatsLongValue(actualStats, DOWNLINK_MSGS_ADDED)); + assertEquals(EXPECTED_MSGS_PUSHED, getStatsLongValue(actualStats, DOWNLINK_MSGS_PUSHED)); + assertEquals(EXPECTED_MSGS_PERMANENTLY_FAILED, getStatsLongValue(actualStats, DOWNLINK_MSGS_PERMANENTLY_FAILED)); + assertEquals(EXPECTED_MSGS_TMP_FAILED, getStatsLongValue(actualStats, DOWNLINK_MSGS_TMP_FAILED)); + }); + } + + private long getStatsLongValue(List stats, EdgeStatsKey key) { + return stats.stream().filter(e -> e.getKey().equals(key.getKey())).findFirst().get().getLongValue().orElse(0L); + } + + private List fetchLatestStats() throws ExecutionException, InterruptedException { + return tsService.findLatest( + tenantId, + edge.getId(), + Arrays.stream(EdgeStatsKey.values()).map(EdgeStatsKey::getKey).toList()).get(); + } + + private void simulateEdgeEventsAddedDownlinkPushed() throws InterruptedException, ExecutionException { + statsCounterService.clear(edge.getId()); + + // Save device and assign to edge + // 2 DOWNLINK_MSGS_ADDED, EdgeEvents: [{DEVICE_PROFILE: ADDED}, {DEVICE: ASSIGNED_TO_EDGE}] + // 2 DOWNLINK_MSGS_PUSHED, Downlinks: [{deviceProfileUpdateMsg}, {deviceUpdateMsg, deviceProfileUpdateMsg, deviceCredentialsUpdateMsg}] + edgeImitator.expectMessageAmount(4); + Device savedDevice = saveDevice("StatisticDevice", STATISTICS_DEVICE_PROFILE); + doPost("/api/edge/" + edge.getUuidId() + "/device/" + savedDevice.getUuidId(), Device.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + // Save asset and assign to edge + // 1 DOWNLINK_MSGS_ADDED, EdgeEvents: [{ASSET: ASSIGNED_TO_EDGE}] + // 1 DOWNLINK_MSGS_PUSHED, Downlinks: [{assetUpdateMsg, assetProfileUpdateMsg}] + edgeImitator.expectMessageAmount(2); + Asset savedAsset = saveAsset("Edge Asset"); + doPost("/api/edge/" + edge.getUuidId() + + "/asset/" + savedAsset.getUuidId(), Asset.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + // Create customer and assign edge to the customer + // 2 DOWNLINK_MSGS_ADDED, EdgeEvents: [{CUSTOMER: ADDED}, {EDGE: ASSIGNED_TO_CUSTOMER}] + // 2 DOWNLINK_MSGS_PUSHED, Downlinks: [{customerUpdateMsg}, {edgeConfiguration}] + edgeImitator.expectMessageAmount(2); + Customer customer = new Customer(); + customer.setTitle("Edge Customer"); + Customer savedCustomer = doPost("/api/customer", customer, Customer.class); + doPost("/api/customer/" + savedCustomer.getUuidId() + + "/edge/" + edge.getUuidId(), Edge.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + // Send device telemetry downlink for the device + // 1 DOWNLINK_MSGS_ADDED, EdgeEvents: [{DEVICE: TIMESERIES_UPDATED}] + // 1 DOWNLINK_MSGS_PUSHED, Downlinks: [{entityData}] + edgeImitator.expectMessageAmount(1); + String timeseriesData = "{\"data\":{\"temperature\":25},\"ts\":" + System.currentTimeMillis() + "}"; + JsonNode timeseriesEntityData = JacksonUtil.toJsonNode(timeseriesData); + EdgeEvent edgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.TIMESERIES_UPDATED, savedDevice.getId().getId(), EdgeEventType.DEVICE, timeseriesEntityData); + edgeEventService.saveAsync(edgeEvent).get(); + Assert.assertTrue(edgeImitator.waitForMessages()); + } +} diff --git a/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java index 36d9ed5308..a9ec11e9b1 100644 --- a/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java @@ -223,8 +223,10 @@ public class TelemetryEdgeTest extends AbstractEdgeTest { device.getId().getId(), EdgeEventType.DEVICE, timeseriesEntityData); edgeEventService.saveAsync(failedEdgeEvent).get(); + // add unique body to avoid merge and filter by device id in edge service (mergeAndFilterDownlinkDuplicates) + JsonNode uniqueBody = JacksonUtil.toJsonNode("{\"idx\":" + idx + "}"); EdgeEvent successEdgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.UPDATED, - device.getId().getId(), EdgeEventType.DEVICE, null); + device.getId().getId(), EdgeEventType.DEVICE, uniqueBody); edgeEventService.saveAsync(successEdgeEvent).get(); } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java index bf6a112e72..12f3e4298d 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationArgumentEntryTest.java @@ -104,19 +104,19 @@ public class PropagationArgumentEntryTest { @Test void testUpdateEntryWhenAdded() { var added = new PropagationArgumentEntry(); - added.setAdded(ENTITY_3_ID); + added.setAdded(List.of(ENTITY_3_ID)); boolean changed = entry.updateEntry(added); assertThat(changed).isTrue(); assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID, ENTITY_3_ID); - assertThat(entry.getAdded()).isEqualTo(ENTITY_3_ID); + assertThat(entry.getAdded()).isEqualTo(List.of(ENTITY_3_ID)); } @Test void testUpdateEntryWhenAddedExistingEntity() { var added = new PropagationArgumentEntry(); - added.setAdded(ENTITY_2_ID); + added.setAdded(List.of(ENTITY_2_ID)); boolean changed = entry.updateEntry(added); @@ -149,6 +149,77 @@ public class PropagationArgumentEntryTest { assertThat(entry.getRemoved()).isNull(); } + @Test + void testUpdateEntryWhenPartitionStateRestoreAddsMissingIds() { + var restore = new PropagationArgumentEntry(List.of(ENTITY_1_ID, ENTITY_2_ID, ENTITY_3_ID)); + restore.setIgnoreRemovedEntities(true); + + boolean changed = entry.updateEntry(restore); + + assertThat(changed).isTrue(); + assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID, ENTITY_3_ID); + assertThat(entry.getAdded()).containsExactly(ENTITY_3_ID); + assertThat(entry.getRemoved()).isNull(); + assertThat(entry.isIgnoreRemovedEntities()).isFalse(); + } + + @Test + void testUpdateEntryWhenPartitionStateRestoreRemovesStaleIds() { + var restore = new PropagationArgumentEntry(List.of(ENTITY_1_ID)); + restore.setIgnoreRemovedEntities(true); + + boolean changed = entry.updateEntry(restore); + + assertThat(changed).isFalse(); // expected no change, since we consider the removal of stale ids as no-op + assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID); + assertThat(entry.getAdded()).isNull(); + assertThat(entry.getRemoved()).isNull(); + assertThat(entry.isIgnoreRemovedEntities()).isFalse(); + } + + @Test + void testUpdateEntryWhenPartitionStateRestoreAddsAndRemoves() { + var restore = new PropagationArgumentEntry(List.of(ENTITY_1_ID, ENTITY_3_ID)); + restore.setIgnoreRemovedEntities(true); + + boolean changed = entry.updateEntry(restore); + + assertThat(changed).isTrue(); + assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_3_ID); + assertThat(entry.getAdded()).containsExactly(ENTITY_3_ID); + assertThat(entry.getRemoved()).isNull(); + assertThat(entry.isIgnoreRemovedEntities()).isFalse(); + } + + + @Test + void testUpdateEntryWhenPartitionStateRestoreNoChanges() { + var restore = new PropagationArgumentEntry(List.of(ENTITY_1_ID, ENTITY_2_ID)); + restore.setIgnoreRemovedEntities(true); + + boolean changed = entry.updateEntry(restore); + + assertThat(changed).isFalse(); + assertThat(entry.getEntityIds()).containsExactlyInAnyOrder(ENTITY_1_ID, ENTITY_2_ID); + assertThat(entry.getAdded()).isNull(); + assertThat(entry.getRemoved()).isNull(); + assertThat(entry.isIgnoreRemovedEntities()).isFalse(); + } + + @Test + void testUpdateEntryWhenPartitionStateRestoreEmptySet() { + var restore = new PropagationArgumentEntry(List.of()); + restore.setIgnoreRemovedEntities(true); + + boolean changed = entry.updateEntry(restore); + + assertThat(changed).isFalse(); // expected no change, since we consider the removal of stale ids as no-op + assertThat(entry.getEntityIds()).isEmpty(); + assertThat(entry.getAdded()).isNull(); + assertThat(entry.getRemoved()).isNull(); + assertThat(entry.isIgnoreRemovedEntities()).isFalse(); + } + @Test @SuppressWarnings("unchecked") void testToTbelCfArgWithValues() { diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java index add6c1ee39..a9bf6d54d1 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/PropagationCalculatedFieldStateTest.java @@ -47,6 +47,7 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.stats.DefaultStatsFactory; import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import org.thingsboard.server.service.cf.CalculatedFieldProcessingService; import org.thingsboard.server.service.cf.PropagationCalculatedFieldResult; import org.thingsboard.server.service.cf.TelemetryCalculatedFieldResult; import org.thingsboard.server.service.cf.ctx.state.propagation.PropagationArgumentEntry; @@ -57,12 +58,17 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.thingsboard.server.common.data.cf.configuration.PropagationCalculatedFieldConfiguration.PROPAGATION_CONFIG_ARGUMENT; @@ -70,17 +76,26 @@ import static org.thingsboard.server.common.data.cf.configuration.PropagationCal public class PropagationCalculatedFieldStateTest { private static final String TEMPERATURE_ARGUMENT_NAME = "t"; + private static final String HUMIDITY_ARGUMENT_NAME = "h"; private static final String TEST_RESULT_EXPRESSION_KEY = "testResult"; private static final double TEMPERATURE_VALUE = 12.5; + private static final double HUMIDITY_VALUE = 85; + + private static final PropagationArgumentEntry EMPTY_PROPAGATION_ARGUMENT = new PropagationArgumentEntry(Collections.emptyList()); private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("6c3513cb-85e7-4510-8746-1ba01859a8ce")); private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("be960a50-c029-4698-b2ec-c56a543c561c")); private final AssetId ASSET_ID_1 = new AssetId(UUID.fromString("d26f0e5b-7d7d-4a61-9f5e-08ab97b30734")); private final AssetId ASSET_ID_2 = new AssetId(UUID.fromString("1933a317-4df5-4d36-9800-68aded74579b")); - private final SingleValueArgumentEntry singleValueArgEntry = + private final SingleValueArgumentEntry EMPTY_SINGLE_VALUE_ARGUMENT = new SingleValueArgumentEntry(); + + private final SingleValueArgumentEntry temperatureArgumentEntry = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", TEMPERATURE_VALUE), 99L); + private final SingleValueArgumentEntry humidityArgumentEntry = + new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("humidity", HUMIDITY_VALUE), 99L); + private final PropagationArgumentEntry propagationArgEntry = new PropagationArgumentEntry(new ArrayList<>(List.of(ASSET_ID_2, ASSET_ID_1))); @@ -96,15 +111,19 @@ public class PropagationCalculatedFieldStateTest { @MockitoBean private ActorSystemContext actorSystemContext; + @MockitoBean + private CalculatedFieldProcessingService cfProcessingService; + @BeforeEach void setUp() { when(actorSystemContext.getTbelInvokeService()).thenReturn(tbelInvokeService); when(actorSystemContext.getApiLimitService()).thenReturn(apiLimitService); + when(actorSystemContext.getCalculatedFieldProcessingService()).thenReturn(cfProcessingService); when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); } void initCtxAndState(boolean applyExpressionToResolvedArguments) { - ctx = new CalculatedFieldCtx(getCalculatedField(applyExpressionToResolvedArguments), actorSystemContext); + ctx = spy(new CalculatedFieldCtx(getCalculatedField(applyExpressionToResolvedArguments), actorSystemContext)); ctx.init(); state = new PropagationCalculatedFieldState(ctx.getEntityId()); @@ -121,7 +140,7 @@ public class PropagationCalculatedFieldStateTest { @Test void testInitAddsRequiredArgument() { initCtxAndState(false); - assertThat(state.getRequiredArguments()).containsExactlyInAnyOrder(TEMPERATURE_ARGUMENT_NAME, PROPAGATION_CONFIG_ARGUMENT); + assertThat(state.getRequiredArguments()).containsExactlyInAnyOrder(TEMPERATURE_ARGUMENT_NAME, HUMIDITY_ARGUMENT_NAME, PROPAGATION_CONFIG_ARGUMENT); } @Test @@ -133,7 +152,7 @@ public class PropagationCalculatedFieldStateTest { private static Stream provideInvalidPropagationArgs() { return Stream.of( null, - new PropagationArgumentEntry(Collections.emptyList()) + EMPTY_PROPAGATION_ARGUMENT ); } @@ -143,7 +162,8 @@ public class PropagationCalculatedFieldStateTest { initCtxAndState(false); Map args = new HashMap<>(); - args.put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); // Valid user arg + args.put(TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry); // Valid user arg + args.put(HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry); // Valid user arg if (propagationEntry != null) { args.put(PROPAGATION_CONFIG_ARGUMENT, propagationEntry); @@ -155,19 +175,54 @@ public class PropagationCalculatedFieldStateTest { } @Test - void testIsReadyWhenPropagationArgHasEntities() { + void testIsReadyWithoutExpressionWhenAllArgumentsAreNotEmpty() { initCtxAndState(false); - state.update(Map.of(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry, PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry), ctx); + Map updatedArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry + ); + state.update(updatedArgs, ctx); assertThat(state.isReady()).isTrue(); assertThat(state.getReadinessStatus().errorMsg()).isNull(); } + @Test + void testIsReadyWithoutExpressionWhenAtLeastOneArgumentIsNotEmpty() { + initCtxAndState(false); + Map updatedArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, EMPTY_SINGLE_VALUE_ARGUMENT, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); + state.update(updatedArgs, ctx); + assertThat(state.isReady()).isTrue(); + assertThat(state.getReadinessStatus().errorMsg()).isNull(); + } + + @Test + void testIsNotReadyWithExpressionWhenAtLeastOneArgumentIsEmpty() { + initCtxAndState(true); + Map updatedArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, EMPTY_SINGLE_VALUE_ARGUMENT, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); + state.update(updatedArgs, ctx); + assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()).isEqualTo("Required arguments are missing: h"); + } + @Test void testPerformCalculationWithEmptyPropagationArg() throws Exception { initCtxAndState(false); - state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(Collections.emptyList())); + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry, + PROPAGATION_CONFIG_ARGUMENT, EMPTY_PROPAGATION_ARGUMENT); + state.update(initArgs, ctx); + assertThat(state.isReady()).isFalse(); + // test empty propagation argument calculation PropagationCalculatedFieldResult result = performCalculation(); assertThat(result).isNotNull(); @@ -178,8 +233,12 @@ public class PropagationCalculatedFieldStateTest { @Test void testPerformCalculationWithArgumentsOnlyMode() throws Exception { initCtxAndState(false); - state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); - state.getArguments().put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, EMPTY_SINGLE_VALUE_ARGUMENT, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); + state.update(initArgs, ctx); + assertThat(state.isReady()).isTrue(); PropagationCalculatedFieldResult propagationResult = performCalculation(); @@ -193,7 +252,7 @@ public class PropagationCalculatedFieldStateTest { assertThat(result.getScope()).isEqualTo(AttributeScope.SERVER_SCOPE); ObjectNode expectedNode = JacksonUtil.newObjectNode(); - JacksonUtil.addKvEntry(expectedNode, singleValueArgEntry.getKvEntryValue(), TEMPERATURE_ARGUMENT_NAME); + JacksonUtil.addKvEntry(expectedNode, temperatureArgumentEntry.getKvEntryValue(), TEMPERATURE_ARGUMENT_NAME); assertThat(result.getResult()).isEqualTo(expectedNode); } @@ -201,9 +260,13 @@ public class PropagationCalculatedFieldStateTest { @Test void testPerformCalculationWithExpressionResultMode() throws Exception { initCtxAndState(true); - state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); - state.getArguments().put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); - + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry + ); + state.update(initArgs, ctx); + assertThat(state.isReady()).isTrue(); PropagationCalculatedFieldResult propagationResult = performCalculation(); assertThat(propagationResult).isNotNull(); @@ -216,7 +279,7 @@ public class PropagationCalculatedFieldStateTest { assertThat(result.getScope()).isEqualTo(AttributeScope.SERVER_SCOPE); ObjectNode expectedNode = JacksonUtil.newObjectNode(); - expectedNode.put(TEST_RESULT_EXPRESSION_KEY, TEMPERATURE_VALUE * 2); + expectedNode.put(TEST_RESULT_EXPRESSION_KEY, (TEMPERATURE_VALUE + HUMIDITY_VALUE) / 2); assertThat(result.getResult()).isEqualTo(expectedNode); } @@ -224,18 +287,52 @@ public class PropagationCalculatedFieldStateTest { @Test void testPropagationWithUpdatedPropagationArgument() throws ExecutionException, InterruptedException { initCtxAndState(false); - state.getArguments().put(PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry); - state.getArguments().put(TEMPERATURE_ARGUMENT_NAME, singleValueArgEntry); + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, EMPTY_SINGLE_VALUE_ARGUMENT, + PROPAGATION_CONFIG_ARGUMENT, propagationArgEntry + ); + state.update(initArgs, ctx); + assertThat(state.isReady()).isTrue(); AssetId newEntityId = new AssetId(UUID.fromString("83e2c962-eeae-4708-984e-e6a24760f9c3")); PropagationArgumentEntry propagationArgumentEntry = new PropagationArgumentEntry(); - propagationArgumentEntry.setAdded(newEntityId); + propagationArgumentEntry.setAdded(List.of(newEntityId)); Map updated = state.update(Map.of(PROPAGATION_CONFIG_ARGUMENT, propagationArgumentEntry), ctx); assertThat(updated).isNotNull().containsEntry(PROPAGATION_CONFIG_ARGUMENT, propagationArgumentEntry); PropagationCalculatedFieldResult propagationCalculatedFieldResult = performCalculation(updated); assertThat(propagationCalculatedFieldResult).isNotNull(); assertThat(propagationCalculatedFieldResult.getEntityIds()).isNotNull().containsExactly(newEntityId); + assertThat(propagationCalculatedFieldResult.getResult()).isNotNull(); + assertThat(propagationCalculatedFieldResult.getResult().getResult()).isNotNull(); + assertThat(propagationCalculatedFieldResult.getResult().getResult()).isEqualTo(JacksonUtil.newObjectNode().put(TEMPERATURE_ARGUMENT_NAME, TEMPERATURE_VALUE)); + } + + @Test + void testPropapagationStateInitWithRestoredSetToFalse() { + initCtxAndState(false); + verify(cfProcessingService, never()).fetchPropagationArgumentFromDb(any(), any()); + verify(ctx, never()).scheduleReevaluation(anyLong(), any()); + } + + @Test + void testPropapagationStateInitWithRestoredSetToTrue() { + initCtxAndState(false); + Map initArgs = Map.of( + TEMPERATURE_ARGUMENT_NAME, temperatureArgumentEntry, + HUMIDITY_ARGUMENT_NAME, humidityArgumentEntry, + PROPAGATION_CONFIG_ARGUMENT, new PropagationArgumentEntry(Collections.emptyList()) + ); + state.update(initArgs, ctx); + assertThat(state.isReady()).isFalse(); + + when(cfProcessingService.fetchPropagationArgumentFromDb(any(), any())).thenReturn(Optional.of(propagationArgEntry)); + + state.init(true); + + verify(cfProcessingService).fetchPropagationArgumentFromDb(ctx, state.getEntityId()); + verify(ctx).scheduleReevaluation(0L, state.getActorCtx()); } private CalculatedField getCalculatedField(boolean applyExpressionToResolvedArguments) { @@ -260,8 +357,12 @@ public class PropagationCalculatedFieldStateTest { ReferencedEntityKey tempKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null); temperatureArg.setRefEntityKey(tempKey); - config.setArguments(Map.of(TEMPERATURE_ARGUMENT_NAME, temperatureArg)); - config.setExpression("{" + TEST_RESULT_EXPRESSION_KEY + ": " + TEMPERATURE_ARGUMENT_NAME + " * 2}"); + Argument humidityArg = new Argument(); + ReferencedEntityKey humidityKey = new ReferencedEntityKey("humidity", ArgumentType.TS_LATEST, null); + humidityArg.setRefEntityKey(humidityKey); + + config.setArguments(Map.of(TEMPERATURE_ARGUMENT_NAME, temperatureArg, HUMIDITY_ARGUMENT_NAME, humidityArg)); + config.setExpression("return { " + TEST_RESULT_EXPRESSION_KEY + ": (" + TEMPERATURE_ARGUMENT_NAME + " + " + HUMIDITY_ARGUMENT_NAME + ") / 2};"); AttributesOutput output = new AttributesOutput(); output.setScope(AttributeScope.SERVER_SCOPE); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesAggregationCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesAggregationCalculatedFieldStateTest.java index a93a6e8ddd..5085c64ef2 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesAggregationCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/RelatedEntitiesAggregationCalculatedFieldStateTest.java @@ -113,8 +113,10 @@ public class RelatedEntitiesAggregationCalculatedFieldStateTest { } @Test - void testIsReadyReturnFalseWhenNoArgumentsSet() { + void testIsReadyWhenNoRelatedEntities() { assertThat(state.isReady()).isFalse(); + assertThat(state.getReadinessStatus().errorMsg()) + .isEqualTo("No entities found via 'Aggregation path to related entities'. Verify the configured relation type and direction."); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java index 1ee6e31372..ebb8305871 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeMsgConstructorUtilsTest.java @@ -15,10 +15,12 @@ */ package org.thingsboard.server.service.edge; +import com.fasterxml.jackson.databind.JsonNode; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -36,6 +38,10 @@ import org.thingsboard.rule.engine.rest.TbSendRestApiCallReplyNode; import org.thingsboard.rule.engine.telemetry.TbCalculatedFieldsNode; import org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode; import org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode; +import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; +import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.gen.edge.v1.EdgeVersion; @@ -44,6 +50,7 @@ import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import java.lang.reflect.Constructor; import java.util.List; import java.util.Optional; +import java.util.UUID; import java.util.stream.Stream; import static org.thingsboard.server.service.edge.EdgeMsgConstructorUtils.EXCLUDED_NODES_BY_EDGE_VERSION; @@ -155,4 +162,95 @@ public class EdgeMsgConstructorUtilsTest { String.format("For EdgeVersion '%s', ruleNode '%s' should not be included.", edgeVersion, ruleNode.getType())); } + @Test + @DisplayName("mergeDownlinkDuplicates: latest per attribute key is retained and duplicates removed") + public void testMergeDownlinkDuplicates() { + UUID deviceId = UUID.randomUUID(); + UUID assetId = UUID.randomUUID(); + TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + + var deviceAttrUpdate1 = createEdgeEvent(tenantId, 1, EdgeEventActionType.ATTRIBUTES_UPDATED, + deviceId, EdgeEventType.DEVICE, createAttrBody(1_000L, "{\"a\":1,\"b\":1,\"d\":1}")); + var deviceAttrUpdate2 = createEdgeEvent(tenantId, 2, EdgeEventActionType.ATTRIBUTES_UPDATED, + deviceId, EdgeEventType.DEVICE, createAttrBody(2_000L, "{\"a\":2,\"b\":2,\"c\":2}")); + var deviceAttrUpdate3 = createEdgeEvent(tenantId, 3, EdgeEventActionType.ATTRIBUTES_UPDATED, + deviceId, EdgeEventType.DEVICE, createAttrBody(3_000L, "{\"a\":3,\"d\":3}")); + + var deviceUpdate = createEdgeEvent(tenantId, 4, EdgeEventActionType.UPDATED, + deviceId, EdgeEventType.DEVICE, null); + var deviceUpdateDup = createEdgeEvent(tenantId, 5, EdgeEventActionType.UPDATED, + deviceId, EdgeEventType.DEVICE, null); + + var assetAttrUpdate1 = createEdgeEvent(tenantId, 6, EdgeEventActionType.ATTRIBUTES_UPDATED, + assetId, EdgeEventType.ASSET, createAttrBody(6_000L, "{\"a\":6,\"d\":6}")); + var assetAttrUpdate2 = createEdgeEvent(tenantId, 7, EdgeEventActionType.ATTRIBUTES_UPDATED, + assetId, EdgeEventType.ASSET, createAttrBody(7_000L, "{\"a\":7,\"b\":7,\"c\":7}")); + var assetAttrUpdate3 = createEdgeEvent(tenantId, 8, EdgeEventActionType.ATTRIBUTES_UPDATED, + assetId, EdgeEventType.ASSET, createAttrBody(8_000L, "{\"a\":8,\"d\":8}")); + + List input = List.of(deviceAttrUpdate1, deviceAttrUpdate2, deviceAttrUpdate3, + deviceUpdate, deviceUpdateDup, + assetAttrUpdate1, assetAttrUpdate2, assetAttrUpdate3); + List merged = EdgeMsgConstructorUtils.mergeAndFilterDownlinkDuplicates(input); + + Assertions.assertEquals(5, merged.size()); + + EdgeEvent deviceMergedAttrBC = merged.get(0); + Assertions.assertEquals(2, deviceMergedAttrBC.getSeqId()); + Assertions.assertEquals(deviceId, deviceMergedAttrBC.getEntityId()); + Assertions.assertEquals(2_000L, deviceMergedAttrBC.getBody().get("ts").asLong()); + Assertions.assertEquals(2, getIntValue(deviceMergedAttrBC.getBody(), "b")); + Assertions.assertEquals(2, getIntValue(deviceMergedAttrBC.getBody(), "c")); + Assertions.assertNull(getIntValue(deviceMergedAttrBC.getBody(), "a")); + + EdgeEvent deviceMergedAttrAD = merged.get(1); + Assertions.assertEquals(3, deviceMergedAttrAD.getSeqId()); + Assertions.assertEquals(deviceId, deviceMergedAttrAD.getEntityId()); + Assertions.assertEquals(3_000L, deviceMergedAttrAD.getBody().get("ts").asLong()); + Assertions.assertEquals(3, getIntValue(deviceMergedAttrAD.getBody(), "a")); + Assertions.assertEquals(3, getIntValue(deviceMergedAttrAD.getBody(), "d")); + + EdgeEvent mergedDeviceUpdate = merged.get(2); + Assertions.assertEquals(4, mergedDeviceUpdate.getSeqId()); + Assertions.assertEquals(EdgeEventActionType.UPDATED, mergedDeviceUpdate.getAction()); + + EdgeEvent assetMergedAttrBC = merged.get(3); + Assertions.assertEquals(7, assetMergedAttrBC.getSeqId()); + Assertions.assertEquals(assetId, assetMergedAttrBC.getEntityId()); + Assertions.assertEquals(7_000L, assetMergedAttrBC.getBody().get("ts").asLong()); + Assertions.assertEquals(7, getIntValue(assetMergedAttrBC.getBody(), "b")); + Assertions.assertEquals(7, getIntValue(assetMergedAttrBC.getBody(), "c")); + Assertions.assertNull(getIntValue(assetMergedAttrBC.getBody(), "a")); + + EdgeEvent assetMergedAttrAD = merged.get(4); + Assertions.assertEquals(8, assetMergedAttrAD.getSeqId()); + Assertions.assertEquals(assetId, assetMergedAttrAD.getEntityId()); + Assertions.assertEquals(8_000L, assetMergedAttrAD.getBody().get("ts").asLong()); + Assertions.assertEquals(8, getIntValue(assetMergedAttrAD.getBody(), "a")); + Assertions.assertEquals(8, getIntValue(assetMergedAttrAD.getBody(), "d")); + } + + private Integer getIntValue(JsonNode body, String key) { + return body.get("kv").get(key) != null ? body.get("kv").get(key).asInt() : null; + } + + private static JsonNode createAttrBody(long ts, String kvJson) { + return JacksonUtil.toJsonNode("{\"ts\":" + ts + ",\"kv\":" + kvJson + "}"); + } + + private static EdgeEvent createEdgeEvent(TenantId tenantId, + long seqId, + EdgeEventActionType action, + UUID entityId, + EdgeEventType type, + JsonNode body) { + EdgeEvent edgeEvent = new EdgeEvent(); + edgeEvent.setSeqId(seqId); + edgeEvent.setTenantId(tenantId); + edgeEvent.setAction(action); + edgeEvent.setEntityId(entityId); + edgeEvent.setType(type); + edgeEvent.setBody(body); + return edgeEvent; + } } diff --git a/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java b/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java index 25ff0f1b5d..3cb4bec803 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/EdgeStatsTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.test.util.ReflectionTestUtils; @@ -44,9 +45,11 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_ADDED; @@ -58,6 +61,16 @@ import static org.thingsboard.server.dao.edge.stats.EdgeStatsKey.DOWNLINK_MSGS_T @ExtendWith(MockitoExtension.class) public class EdgeStatsTest { + private static final int TTL_DAYS = 30; + private static final long REPORT_INTERVAL_MILLIS = 600_000L; + + private static final long EXPECTED_MSGS_ADDED = 5L; + private static final long EXPECTED_MSGS_PUSHED = 3L; + private static final long EXPECTED_MSGS_PERMANENTLY_FAILED = 1L; + private static final long EXPECTED_MSGS_TMP_FAILED = 0L; + private static final long EXPECTED_MSGS_LAG = 10L; + private static final long EXPECTED_MSGS_KAFKA_LAG = 15L; + @Mock private TimeseriesService tsService; @Mock @@ -66,108 +79,100 @@ public class EdgeStatsTest { private EdgeStatsCounterService statsCounterService; private EdgeStatsService edgeStatsService; + @Captor + private ArgumentCaptor> captor; + private final TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); private final EdgeId edgeId = new EdgeId(UUID.randomUUID()); @BeforeEach void setUp() { - edgeStatsService = new EdgeStatsService( + edgeStatsService = createEdgeStatsService(Optional.empty()); + } + + private EdgeStatsService createEdgeStatsService(Optional kafkaAdmin) { + EdgeStatsService service = new EdgeStatsService( tsService, statsCounterService, topicService, - Optional.empty() + kafkaAdmin ); - - ReflectionTestUtils.setField(edgeStatsService, "edgesStatsTtlDays", 30); - ReflectionTestUtils.setField(edgeStatsService, "reportIntervalMillis", 600_000L); + ReflectionTestUtils.setField(service, "edgesStatsTtlDays", TTL_DAYS); + ReflectionTestUtils.setField(service, "reportIntervalMillis", REPORT_INTERVAL_MILLIS); + return service; } @Test public void testReportStatsSavesTelemetry() { - // given + // GIVEN + setupCounters(); + + // WHEN + edgeStatsService.reportStats(); + + // THEN + Map counters = verifyCounters(); + Assertions.assertEquals(EXPECTED_MSGS_LAG, counters.get(DOWNLINK_MSGS_LAG.getKey()).longValue()); + } + + @Test + public void testReportStatsWithKafkaLag() { + // GIVEN + setupCounters(); + setupKafkaLag(); + + // WHEN + edgeStatsService.reportStats(); + + // THEN + Map valuesByKey = verifyCounters(); + Assertions.assertEquals(EXPECTED_MSGS_KAFKA_LAG, valuesByKey.get(DOWNLINK_MSGS_LAG.getKey())); + } + + private void setupCounters() { MsgCounters counters = new MsgCounters(tenantId); - counters.getMsgsAdded().set(5); - counters.getMsgsPushed().set(3); - counters.getMsgsPermanentlyFailed().set(1); - counters.getMsgsTmpFailed().set(0); - counters.getMsgsLag().set(10); + counters.getMsgsAdded().set(EXPECTED_MSGS_ADDED); + counters.getMsgsPushed().set(EXPECTED_MSGS_PUSHED); + counters.getMsgsPermanentlyFailed().set(EXPECTED_MSGS_PERMANENTLY_FAILED); + counters.getMsgsTmpFailed().set(EXPECTED_MSGS_TMP_FAILED); + counters.getMsgsLag().set(EXPECTED_MSGS_LAG); ConcurrentHashMap countersByEdge = new ConcurrentHashMap<>(); countersByEdge.put(edgeId, counters); - when(statsCounterService.getCounterByEdge()).thenReturn(countersByEdge); + when(statsCounterService.getMsgCountersByEdge()).thenReturn(countersByEdge); - ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); when(tsService.save(eq(tenantId), eq(edgeId), captor.capture(), anyLong())) .thenReturn(Futures.immediateFuture(mock(TimeseriesSaveResult.class))); + } - // when - edgeStatsService.reportStats(); + private Map verifyCounters() { + verify(tsService, times(1)).save(eq(tenantId), eq(edgeId), anyList(), anyLong()); + verify(statsCounterService, times(1)).clear(edgeId); - // then List entries = captor.getValue(); Assertions.assertEquals(5, entries.size()); Map valuesByKey = entries.stream() .collect(Collectors.toMap(TsKvEntry::getKey, e -> e.getLongValue().orElse(-1L))); - Assertions.assertEquals(5L, valuesByKey.get(DOWNLINK_MSGS_ADDED.getKey()).longValue()); - Assertions.assertEquals(3L, valuesByKey.get(DOWNLINK_MSGS_PUSHED.getKey()).longValue()); - Assertions.assertEquals(1L, valuesByKey.get(DOWNLINK_MSGS_PERMANENTLY_FAILED.getKey()).longValue()); - Assertions.assertEquals(0L, valuesByKey.get(DOWNLINK_MSGS_TMP_FAILED.getKey()).longValue()); - Assertions.assertEquals(10L, valuesByKey.get(DOWNLINK_MSGS_LAG.getKey()).longValue()); - - - verify(statsCounterService).clear(edgeId); + Assertions.assertEquals(EXPECTED_MSGS_ADDED, valuesByKey.get(DOWNLINK_MSGS_ADDED.getKey()).longValue()); + Assertions.assertEquals(EXPECTED_MSGS_PUSHED, valuesByKey.get(DOWNLINK_MSGS_PUSHED.getKey()).longValue()); + Assertions.assertEquals(EXPECTED_MSGS_PERMANENTLY_FAILED, valuesByKey.get(DOWNLINK_MSGS_PERMANENTLY_FAILED.getKey()).longValue()); + Assertions.assertEquals(EXPECTED_MSGS_TMP_FAILED, valuesByKey.get(DOWNLINK_MSGS_TMP_FAILED.getKey()).longValue()); + return valuesByKey; } - @Test - public void testReportStatsWithKafkaLag() { - // given - MsgCounters counters = new MsgCounters(tenantId); - counters.getMsgsAdded().set(2); - counters.getMsgsPushed().set(2); - counters.getMsgsPermanentlyFailed().set(0); - counters.getMsgsTmpFailed().set(1); - counters.getMsgsLag().set(0); - - ConcurrentHashMap countersByEdge = new ConcurrentHashMap<>(); - countersByEdge.put(edgeId, counters); - - // mocks - when(statsCounterService.getCounterByEdge()).thenReturn(countersByEdge); - + private void setupKafkaLag() { String topic = "edge-topic"; TopicPartitionInfo partitionInfo = new TopicPartitionInfo(topic, tenantId, 0, false); when(topicService.buildEdgeEventNotificationsTopicPartitionInfo(tenantId, edgeId)).thenReturn(partitionInfo); KafkaAdmin kafkaAdmin = mock(KafkaAdmin.class); when(kafkaAdmin.getTotalLagForGroupsBulk(Set.of(topic))) - .thenReturn(Map.of(topic, 15L)); - - ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); - when(tsService.save(eq(tenantId), eq(edgeId), captor.capture(), anyLong())) - .thenReturn(Futures.immediateFuture(mock(TimeseriesSaveResult.class))); - - edgeStatsService = new EdgeStatsService( - tsService, - statsCounterService, - topicService, - Optional.of(kafkaAdmin) - ); - ReflectionTestUtils.setField(edgeStatsService, "edgesStatsTtlDays", 30); - ReflectionTestUtils.setField(edgeStatsService, "reportIntervalMillis", 600_000L); - - // when - edgeStatsService.reportStats(); - - // then - List entries = captor.getValue(); - Map valuesByKey = entries.stream() - .collect(Collectors.toMap(TsKvEntry::getKey, e -> e.getLongValue().orElse(-1L))); + .thenReturn(Map.of(topic, EXPECTED_MSGS_KAFKA_LAG)); - Assertions.assertEquals(15L, valuesByKey.get(DOWNLINK_MSGS_LAG.getKey())); - verify(statsCounterService).clear(edgeId); + edgeStatsService = createEdgeStatsService(Optional.of(kafkaAdmin)); } } diff --git a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java index a146730465..aadbb2ebb5 100644 --- a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java @@ -60,7 +60,7 @@ import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.ai.AiModelService; import org.thingsboard.server.dao.dashboard.DashboardService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DaoSqlTest; diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProviderTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProviderTest.java index 3c1b7dda18..bd20299baf 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProviderTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProviderTest.java @@ -15,18 +15,24 @@ */ package org.thingsboard.server.service.security.auth.pat; +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.User; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.pat.ApiKey; import org.thingsboard.server.common.data.pat.ApiKeyInfo; +import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.service.DaoSqlTest; +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; @@ -101,6 +107,62 @@ public class ApiKeyAuthenticationProviderTest extends AbstractControllerTest { doGetWithApiKey("/api/admin/featuresInfo").andExpect(status().isUnauthorized()); } + @Test + public void testUnauthorizedWhenUserCredentialsDisabled() throws Exception { + User newUser = new User(); + newUser.setAuthority(Authority.TENANT_ADMIN); + newUser.setTenantId(tenantId); + newUser.setEmail("testUser" + RandomStringUtils.secure().nextAlphanumeric(10) + "@thingsboard.org"); + newUser.setFirstName("Test"); + newUser.setLastName("User"); + User savedUser = createUser(newUser, "testPassword1"); + + ApiKeyInfo apiKeyInfo = new ApiKeyInfo(); + apiKeyInfo.setDescription("Test API key for user credentials test"); + apiKeyInfo.setEnabled(true); + apiKeyInfo.setUserId(savedUser.getId()); + ApiKey testApiKey = doPost("/api/apiKey", apiKeyInfo, ApiKey.class); + setApiKey(testApiKey.getValue()); + + doGetWithApiKey("/api/admin/repositorySettings/exists").andExpect(status().isOk()); + + doPost("/api/user/" + savedUser.getId().getId() + "/userCredentialsEnabled?userCredentialsEnabled=false").andExpect(status().isOk()); + + await().atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> doGetWithApiKey("/api/admin/repositorySettings/exists").andExpect(status().isUnauthorized())); + + resetApiKey(); + doDelete("/api/apiKey/" + testApiKey.getId()).andExpect(status().isOk()); + loginSysAdmin(); + doDelete("/api/user/" + savedUser.getId().getId()).andExpect(status().isOk()); + } + + @Test + public void testUnauthorizedWhenUserDeleted() throws Exception { + User newUser = new User(); + newUser.setAuthority(Authority.TENANT_ADMIN); + newUser.setTenantId(tenantId); + newUser.setEmail("testUser" + RandomStringUtils.secure().nextAlphanumeric(10) + "@thingsboard.org"); + newUser.setFirstName("Test"); + newUser.setLastName("User"); + User savedUser = createUser(newUser, "testPassword1"); + + ApiKeyInfo apiKeyInfo = new ApiKeyInfo(); + apiKeyInfo.setDescription("Test API key for user deletion test"); + apiKeyInfo.setEnabled(true); + apiKeyInfo.setUserId(savedUser.getId()); + ApiKey testApiKey = doPost("/api/apiKey", apiKeyInfo, ApiKey.class); + setApiKey(testApiKey.getValue()); + + doGetWithApiKey("/api/admin/repositorySettings/exists").andExpect(status().isOk()); + + loginSysAdmin(); + doDelete("/api/user/" + savedUser.getId().getId()).andExpect(status().isOk()); + + await().atMost(5, TimeUnit.SECONDS) + .untilAsserted(() -> doGetWithApiKey("/api/admin/repositorySettings/exists").andExpect(status().isUnauthorized())); + } + private ApiKeyInfo constructApiKeyInfo() { ApiKeyInfo apiKeyInfo = new ApiKeyInfo(); apiKeyInfo.setDescription("New API key description"); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java index cff56694e5..0681f00d27 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java @@ -16,14 +16,10 @@ package org.thingsboard.server.transport.lwm2m.ota; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.github.dockerjava.zerodep.shaded.org.apache.commons.codec.binary.Hex; import lombok.extern.slf4j.Slf4j; -import org.eclipse.leshan.core.ResponseCode; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -35,20 +31,13 @@ import org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest; import java.util.Comparator; import java.util.List; -import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; -import static org.junit.Assert.assertEquals; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.rest.client.utils.RestJsonConverter.toTimeseries; import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE; import static org.thingsboard.server.common.data.ota.OtaPackageType.SOFTWARE; -import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.OTA_INFO_19_FILE_CHECKSUM256; -import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.OTA_INFO_19_FILE_NAME; -import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.OTA_INFO_19_FILE_SIZE; -import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.OTA_INFO_19_TITLE; -import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.OTA_INFO_19_VERSION; @Slf4j @DaoSqlTest @@ -57,7 +46,6 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg protected static final String CLIENT_ENDPOINT_WITHOUT_FW_INFO = "WithoutFirmwareInfoDevice"; protected static final String CLIENT_ENDPOINT_OTA5 = "Ota5_Device"; protected static final String CLIENT_ENDPOINT_OTA9 = "Ota9_Device"; - protected static final String CLIENT_ENDPOINT_OTA9_19 = "Ota9_Device_19"; protected List expectedStatuses; protected final String OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA5 = @@ -88,37 +76,6 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg " \"attributeLwm2m\": {}\n" + " }"; - protected final String OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA5_19 = - - " {\n" + - " \"keyName\": {\n" + - " \"/5_1.2/0/3\": \"state\",\n" + - " \"/5_1.2/0/5\": \"updateResult\",\n" + - " \"/5_1.2/0/6\": \"pkgname\",\n" + - " \"/5_1.2/0/7\": \"pkgversion\",\n" + - " \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\",\n" + - " \"/19_1.1/0/0\": \"dataRead\"\n" + - " },\n" + - " \"observe\": [\n" + - " \"/5_1.2/0/3\",\n" + - " \"/5_1.2/0/5\",\n" + - " \"/5_1.2/0/6\",\n" + - " \"/5_1.2/0/7\",\n" + - " \"/5_1.2/0/9\",\n" + - " \"/19_1.1/0/0\"\n" + - " ],\n" + - " \"attribute\": [],\n" + - " \"telemetry\": [\n" + - " \"/5_1.2/0/3\",\n" + - " \"/5_1.2/0/5\",\n" + - " \"/5_1.2/0/6\",\n" + - " \"/5_1.2/0/7\",\n" + - " \"/5_1.2/0/9\",\n" + - " \"/19_1.1/0/0\"\n" + - " ],\n" + - " \"attributeLwm2m\": {}\n" + - " }"; - public static final String CLIENT_LWM2M_SETTINGS_19 = " {\n" + " \"useObject19ForOtaInfo\": true,\n" + @@ -247,27 +204,4 @@ public abstract class AbstractOtaLwM2MIntegrationTest extends AbstractLwM2MInteg log.warn("{}", statuses); return statuses.containsAll(expectedStatuses); } - - protected void resultReadOtaParams_19(String resourceIdVer, OtaPackageInfo otaPackageInfo) throws Exception { - String actualResult = sendRPCById(resourceIdVer); - ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); - assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); - String valStr = rpcActualResult.get("value").asText(); - String start = "{ id=0 value="; - String valHexDec = valStr.substring(valStr.indexOf(start) + start.length(), (valStr.indexOf("}"))); - String valNode = new String(Hex.decodeHex((valHexDec).toCharArray())); - ObjectNode actualResultVal = JacksonUtil.fromString(valNode, ObjectNode.class); - assert actualResultVal != null; - assertEquals(otaPackageInfo.getTitle(), actualResultVal.get(OTA_INFO_19_TITLE).asText()); - assertEquals(otaPackageInfo.getVersion(), actualResultVal.get(OTA_INFO_19_VERSION).asText()); - assertEquals(otaPackageInfo.getChecksum(), actualResultVal.get(OTA_INFO_19_FILE_CHECKSUM256).asText()); - assertEquals(otaPackageInfo.getFileName(), actualResultVal.get(OTA_INFO_19_FILE_NAME).asText()); - assertEquals(Optional.of(otaPackageInfo.getDataSize()), Optional.of((long) actualResultVal.get(OTA_INFO_19_FILE_SIZE).asInt())); - } - - private String sendRPCById(String path) throws Exception { - String setRpcRequest = "{\"method\": \"Read\", \"params\": {\"id\": \"" + path + "\"}}"; - return doPostAsync("/api/plugins/rpc/twoway/" + lwM2MTestClient.getDeviceIdStr(), setRpcRequest, String.class, status().isOk()); - } - } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota5LwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota5LwM2MIntegrationTest.java index e4db6f70bf..0943491045 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota5LwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota5LwM2MIntegrationTest.java @@ -21,7 +21,6 @@ import org.junit.Assert; import org.junit.Test; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.kv.KvEntry; @@ -46,10 +45,7 @@ import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.QUEU import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATED; import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATING; import static org.thingsboard.server.dao.service.OtaPackageServiceTest.TARGET_FW_VERSION; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; -import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.FW_INSTANCE_ID; @Slf4j public class Ota5LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { @@ -111,51 +107,4 @@ public class Ota5LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { .until(() -> getFwSwStateTelemetryFromAPI(device.getId().getId(), "fw_state"), this::predicateForStatuses); log.warn("Object5: Got the ts: {}", ts); } - - /** - * ObjectId = 19/65533/0 - * { - * "title" : "My firmware", - * "version" : "fw.v.1.5.0-update", - * "checksum" : "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a", - * "fileSize" : 1, - * "fileName" : "filename.txt" - * } - * to base64 - * /5/0/5 -> Update Result (Res); 5/0/3 -> State; - * => ((Res>=0 && Res<=9) && State=0) - * => Write to Package/Write to Package URI -> DOWNLOADING ((Res>=0 && Res<=9) && State=1) - * => Download Finished -> DOWNLOADED ((Res==0 || Res=8) && State=2) - * => Executable resource Update is triggered / Initiate Firmware Update -> UPDATING (Res=0 && State=3) - * => Update Successful [Res==1] - * => Start / Res=0 -> "IDLE" .... - * @throws Exception - */ - @Test - public void testFirmwareUpdateByObject5WithObject19_Ok() throws Exception { - Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration19(OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA5_19, getBootstrapServerCredentialsNoSec(NONE)); - DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + this.CLIENT_ENDPOINT_OTA5 + "19_Ok", transportConfiguration); - String endpoint = this.CLIENT_ENDPOINT_OTA5 + "19_Ok"; - LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(endpoint)); - final Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId()); - createNewClient(SECURITY_NO_SEC, null, false, endpoint, device.getId().getId().toString()); - awaitObserveReadAll(6, device.getId().getId().toString()); - - OtaPackageInfo otaPackageInfo = createFirmware(TARGET_FW_VERSION, deviceProfile.getId()); - device.setFirmwareId(otaPackageInfo.getId()); - final Device savedDevice = doPost("/api/device", device, Device.class); - - assertThat(savedDevice).as("saved device").isNotNull(); - assertThat(getDeviceFromAPI(device.getId().getId())).as("fetched device").isEqualTo(savedDevice); - - expectedStatuses = Arrays.asList(QUEUED, INITIATED, DOWNLOADING, DOWNLOADED, UPDATING, UPDATED); - List ts = await("await on timeseries for FW") - .atMost(TIMEOUT, TimeUnit.SECONDS) - .until(() -> getFwSwStateTelemetryFromAPI(device.getId().getId(), "fw_state"), this::predicateForStatuses); - - String ver_Id_19 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(BINARY_APP_DATA_CONTAINER).version; - String resourceIdVer = "/" + BINARY_APP_DATA_CONTAINER + "_" + ver_Id_19 + "/" + FW_INSTANCE_ID + "/" + RESOURCE_ID_0; - resultReadOtaParams_19(resourceIdVer, otaPackageInfo); - log.warn("Object5 with Object19: Got the ts: {}", ts); - } } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota9LwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota9LwM2MIntegrationTest.java index b2617dd7ba..5371a6a3c0 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota9LwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/sql/Ota9LwM2MIntegrationTest.java @@ -19,7 +19,6 @@ import lombok.extern.slf4j.Slf4j; import org.junit.Test; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.kv.TsKvEntry; @@ -36,10 +35,7 @@ import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.INIT import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.QUEUED; import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.UPDATED; import static org.thingsboard.server.common.data.ota.OtaPackageUpdateStatus.VERIFIED; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.BINARY_APP_DATA_CONTAINER; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_0; -import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.SW_INSTANCE_ID; @Slf4j public class Ota9LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { @@ -51,7 +47,8 @@ public class Ota9LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { * => PKG integrity verified -> DELIVERED (Res=3 (Successfully Downloaded and package integrity verified) && State=3) -> INSTALLED; * => Install -> INSTALLED (Res=2 SW successfully installed) && State=4) -> Start * - * */ + * + */ @Test public void testSoftwareUpdateByObject9() throws Exception { String clientEndpoint = this.CLIENT_ENDPOINT_OTA9; @@ -75,47 +72,4 @@ public class Ota9LwM2MIntegrationTest extends AbstractOtaLwM2MIntegrationTest { .until(() -> getFwSwStateTelemetryFromAPI(device.getId().getId(), "sw_state"), this::predicateForStatuses); log.warn("Object9: Got the ts: {}", ts); } - /** - * ObjectId = 19/65534/0 - * { - * "title" : "My sw", - * "version" : "v1.0.19", - * "checksum" : "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7cce23c7785459a", - * "fileSize" : 1, - * "fileName" : "filename.txt" - * } - * => Start -> INITIAL (State=0) -> DOWNLOAD STARTED; - * => PKG / URI Write -> DOWNLOAD STARTED (Res=1 (Downloading) && State=1) -> DOWNLOADED - * => PKG Written -> DOWNLOADED (Res=1 Initial && State=2) -> DELIVERED; - * => PKG integrity verified -> DELIVERED (Res=3 (Successfully Downloaded and package integrity verified) && State=3) -> INSTALLED; - * => Install -> INSTALLED (Res=2 SW successfully installed) && State=4) -> Start - * - * */ - @Test - public void testSoftwareUpdateByObject9WithObject19_Ok() throws Exception { - String clientEndpoint = this.CLIENT_ENDPOINT_OTA9_19; - Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration19(OBSERVE_ATTRIBUTES_WITH_PARAMS_OTA9_19, getBootstrapServerCredentialsNoSec(NONE)); - DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + clientEndpoint, transportConfiguration); - LwM2MDeviceCredentials deviceCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); - final Device device = createLwm2mDevice(deviceCredentials, clientEndpoint, deviceProfile.getId()); - createNewClient(SECURITY_NO_SEC, null, false, clientEndpoint, device.getId().getId().toString()); - awaitObserveReadAll(5, device.getId().getId().toString()); - OtaPackageInfo otaPackageInfo = createSoftware(deviceProfile.getId(), "v1.0.19"); - device.setSoftwareId(otaPackageInfo.getId()); - final Device savedDevice = doPost("/api/device", device, Device.class); //sync call - - assertThat(savedDevice).as("saved device").isNotNull(); - assertThat(getDeviceFromAPI(device.getId().getId())).as("fetched device").isEqualTo(savedDevice); - - expectedStatuses = List.of( - QUEUED, INITIATED, DOWNLOADING, DOWNLOADING, DOWNLOADING, DOWNLOADED, VERIFIED, UPDATED); - List ts = await("await on timeseries") - .atMost(TIMEOUT, TimeUnit.SECONDS) - .until(() -> getFwSwStateTelemetryFromAPI(device.getId().getId(), "sw_state"), this::predicateForStatuses); - - String ver_Id_19 = lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(BINARY_APP_DATA_CONTAINER).version; - String resourceIdVer = "/" + BINARY_APP_DATA_CONTAINER + "_" + ver_Id_19 + "/" + SW_INSTANCE_ID + "/" + RESOURCE_ID_0; - resultReadOtaParams_19(resourceIdVer, otaPackageInfo); - log.warn("Object9: Got the ts: {}", ts); - } } diff --git a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java index 83538fe07c..f4bb9bc4c9 100644 --- a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java @@ -119,7 +119,7 @@ class CalculatedFieldUtilsTest { } @Test - void toProtoAndFromProto_shouldCreatePropagationStateWithEmptyPropagationArgument() { + void toProtoAndFromProto_shouldCreatePropagationStateWithNotEmptyPropagationArgument() { // given CalculatedFieldEntityCtxId stateId = mock(CalculatedFieldEntityCtxId.class); given(stateId.tenantId()).willReturn(TENANT_ID); @@ -158,8 +158,7 @@ class CalculatedFieldUtilsTest { assertThat(propagationState.getEntityId()).isEqualTo(DEVICE_ID); assertThat(propagationState.getArguments()).isNotNull(); - assertThat(propagationState.getArguments().get(PROPAGATION_CONFIG_ARGUMENT)).isNotNull(); - assertThat(propagationState.getArguments().get(PROPAGATION_CONFIG_ARGUMENT).isEmpty()).isTrue(); + assertThat(propagationState.getArguments().get(PROPAGATION_CONFIG_ARGUMENT)).isEqualTo(propagationArgumentEntry); assertThat(propagationState.getArguments().get("state")).isNotNull().isEqualTo(singleValueArgumentEntry); assertThat(propagationState.getRequiredArguments()).isNull(); assertThat(propagationState.getReadinessStatus()).isNull(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java index 70032327b9..66f673ebaf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java @@ -41,7 +41,6 @@ public class AlarmInfo extends Alarm { @Schema(description = "Alarm originator label", example = "Thermostat label") private String originatorLabel; - @Getter @Setter @Schema(description = "Originator display name", example = "Thermostat") private String originatorDisplayName; @@ -59,10 +58,15 @@ public class AlarmInfo extends Alarm { super(alarm); } + public String getOriginatorDisplayName() { + return originatorDisplayName != null ? originatorDisplayName : (originatorLabel != null ? originatorLabel : originatorName); + } + public AlarmInfo(AlarmInfo alarmInfo) { super(alarmInfo); this.originatorName = alarmInfo.getOriginatorName(); this.originatorLabel = alarmInfo.getOriginatorLabel(); + this.originatorDisplayName = alarmInfo.getOriginatorDisplayName(); this.assignee = alarmInfo.getAssignee(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldFilter.java index 40283cbaa3..babe2b58ad 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldFilter.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldFilter.java @@ -21,7 +21,6 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.thingsboard.server.common.data.EntityType; -import java.util.List; import java.util.Set; import java.util.UUID; @@ -30,12 +29,12 @@ import java.util.UUID; public class CalculatedFieldFilter { @NonNull - private final CalculatedFieldType type; + private final Set types; @NonNull private final Set entityTypes; @Nullable - private final List entityIds; + private final Set entityIds; @Nullable - private final List names; + private final Set names; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/Lwm2mDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/Lwm2mDeviceProfileTransportConfiguration.java index 614da6cde6..be37a089cd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/Lwm2mDeviceProfileTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/Lwm2mDeviceProfileTransportConfiguration.java @@ -17,10 +17,15 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.device.data.PowerMode; 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.LwM2MBootstrapServerCredential; +import static org.eclipse.leshan.core.LwM2m.Version.V1_0; +import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.SINGLE; + +import java.util.Collections; import java.util.List; @Data @@ -33,9 +38,18 @@ public class Lwm2mDeviceProfileTransportConfiguration implements DeviceProfileTr private List bootstrap; private OtherConfiguration clientLwM2mSettings; + public Lwm2mDeviceProfileTransportConfiguration() { + updateDefault(); + } + @Override public DeviceTransportType getType() { return DeviceTransportType.LWM2M; } + private void updateDefault(){ + this.setBootstrap(Collections.emptyList()); + this.setClientLwM2mSettings(new OtherConfiguration(false,1, 1, 1, PowerMode.DRX, null, null, null, null, null, V1_0.toString())); + this.setObserveAttr(new TelemetryMappingConfiguration(Collections.emptyMap(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptyMap(), false, SINGLE)); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/exception/DataValidationException.java b/common/data/src/main/java/org/thingsboard/server/exception/DataValidationException.java similarity index 88% rename from dao/src/main/java/org/thingsboard/server/dao/exception/DataValidationException.java rename to common/data/src/main/java/org/thingsboard/server/exception/DataValidationException.java index 3fb1e0dc10..6db98312e7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/exception/DataValidationException.java +++ b/common/data/src/main/java/org/thingsboard/server/exception/DataValidationException.java @@ -13,12 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.exception; +package org.thingsboard.server.exception; public class DataValidationException extends RuntimeException { - private static final long serialVersionUID = 7659985660312721830L; - public DataValidationException(String message) { super(message); } @@ -26,4 +24,5 @@ public class DataValidationException extends RuntimeException { public DataValidationException(String message, Throwable cause) { super(message, cause); } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitExceededException.java b/common/data/src/main/java/org/thingsboard/server/exception/EntitiesLimitExceededException.java similarity index 91% rename from dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitExceededException.java rename to common/data/src/main/java/org/thingsboard/server/exception/EntitiesLimitExceededException.java index b2907c9104..39b900cb98 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitExceededException.java +++ b/common/data/src/main/java/org/thingsboard/server/exception/EntitiesLimitExceededException.java @@ -13,14 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.dao.exception; +package org.thingsboard.server.exception; import lombok.Getter; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; public class EntitiesLimitExceededException extends DataValidationException { - private static final long serialVersionUID = -9211462514373279196L; @Getter private final TenantId tenantId; @@ -36,4 +35,5 @@ public class EntitiesLimitExceededException extends DataValidationException { this.entityType = entityType; this.limit = limit; } + } diff --git a/common/data/src/main/java/org/thingsboard/server/exception/ThingsboardRuntimeException.java b/common/data/src/main/java/org/thingsboard/server/exception/ThingsboardRuntimeException.java new file mode 100644 index 0000000000..0a0b8ca718 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/exception/ThingsboardRuntimeException.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2025 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.exception; + +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; + +public class ThingsboardRuntimeException extends RuntimeException { + + private ThingsboardErrorCode errorCode; + + public ThingsboardRuntimeException() { + super(); + } + + public ThingsboardRuntimeException(ThingsboardErrorCode errorCode) { + this.errorCode = errorCode; + } + + public ThingsboardRuntimeException(String message, ThingsboardErrorCode errorCode) { + super(message); + this.errorCode = errorCode; + } + + public ThingsboardRuntimeException(String message, Throwable cause, ThingsboardErrorCode errorCode) { + super(message, cause); + this.errorCode = errorCode; + } + + public ThingsboardRuntimeException(Throwable cause, ThingsboardErrorCode errorCode) { + super(cause); + this.errorCode = errorCode; + } + + public ThingsboardErrorCode getErrorCode() { + return errorCode; + } + +} diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java index 423c59251b..ed0d5b603e 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeRpcClient.java @@ -41,4 +41,5 @@ public interface EdgeRpcClient { void sendDownlinkResponseMsg(DownlinkResponseMsg downlinkResponseMsg); int getServerMaxInboundMessageSize(); + } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 9fb8528bce..3cbc84ba1a 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -935,6 +935,7 @@ message CalculatedFieldStateProto { int64 lastArgsUpdateTs = 7; int64 lastMetricsEvalTs = 8; repeated ArgumentIntervalProto aggregationArguments = 9; + repeated EntityIdProto propagationEntityIds = 10; } //Used to report session state to tb-Service and persist this state in the cache on the tb-Service level. diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/KafkaAdmin.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/KafkaAdmin.java index 6261e81497..3e2c64de69 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/KafkaAdmin.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/KafkaAdmin.java @@ -58,15 +58,12 @@ public class KafkaAdmin { private final TbKafkaSettings settings; - @Value("${queue.kafka.request.timeout.ms:30000}") - private int requestTimeoutMs; - @Value("${queue.kafka.topics_cache_ttl_ms:300000}") // 5 minutes by default - private int topicsCacheTtlMs; - private final LazyInitializer adminClient; private final CachedValue> topics; - public KafkaAdmin(@Lazy TbKafkaSettings settings) { + public KafkaAdmin(@Lazy TbKafkaSettings settings, + @Value("${queue.kafka.topics_cache_ttl_ms:300000}") + int topicsCacheTtlMs) { this.settings = settings; this.adminClient = LazyInitializer.builder() .setInitializer(() -> AdminClient.create(settings.toAdminProps())) @@ -91,7 +88,7 @@ public class KafkaAdmin { NewTopic newTopic = new NewTopic(topic, partitions, settings.getReplicationFactor()).configs(properties); try { - getClient().createTopics(List.of(newTopic)).all().get(requestTimeoutMs, TimeUnit.MILLISECONDS); + getClient().createTopics(List.of(newTopic)).all().get(settings.getRequestTimeoutMs(), TimeUnit.MILLISECONDS); topics.add(topic); } catch (ExecutionException ee) { log.trace("Failed to create topic {} with properties {}", topic, properties, ee); @@ -110,7 +107,7 @@ public class KafkaAdmin { public void deleteTopic(String topic) { log.debug("Deleting topic {}", topic); try { - getClient().deleteTopics(List.of(topic)).all().get(requestTimeoutMs, TimeUnit.MILLISECONDS); + getClient().deleteTopics(List.of(topic)).all().get(settings.getRequestTimeoutMs(), TimeUnit.MILLISECONDS); } catch (Exception e) { log.error("Failed to delete kafka topic [{}].", topic, e); } @@ -122,7 +119,7 @@ public class KafkaAdmin { public Set listTopics() { try { - Set topics = getClient().listTopics().names().get(requestTimeoutMs, TimeUnit.MILLISECONDS); + Set topics = getClient().listTopics().names().get(settings.getRequestTimeoutMs(), TimeUnit.MILLISECONDS); log.trace("Listed topics: {}", topics); return topics; } catch (Exception e) { @@ -150,7 +147,7 @@ public class KafkaAdmin { .collect(Collectors.toMap(tp -> tp, tp -> OffsetSpec.latest())); Map endOffsets = - getClient().listOffsets(latestOffsetsSpec).all().get(requestTimeoutMs, TimeUnit.MILLISECONDS); + getClient().listOffsets(latestOffsetsSpec).all().get(settings.getRequestTimeoutMs(), TimeUnit.MILLISECONDS); return committedOffsets.entrySet().stream() .mapToLong(entry -> { @@ -169,7 +166,7 @@ public class KafkaAdmin { @SneakyThrows public Map getConsumerGroupOffsets(String groupId) { - return getClient().listConsumerGroupOffsets(groupId).partitionsToOffsetAndMetadata().get(requestTimeoutMs, TimeUnit.MILLISECONDS); + return getClient().listConsumerGroupOffsets(groupId).partitionsToOffsetAndMetadata().get(settings.getRequestTimeoutMs(), TimeUnit.MILLISECONDS); } /** @@ -212,7 +209,7 @@ public class KafkaAdmin { } else { log.info("[{}] SHOULD alter topic offset [{}] less than old node group offset [{}]", tp, existingOffset.offset(), om.offset()); } - getClient().alterConsumerGroupOffsets(newGroupId, Map.of(tp, om)).all().get(requestTimeoutMs, TimeUnit.MILLISECONDS); + getClient().alterConsumerGroupOffsets(newGroupId, Map.of(tp, om)).all().get(settings.getRequestTimeoutMs(), TimeUnit.MILLISECONDS); log.info("[{}] altered new consumer groupId {}", tp, newGroupId); break; } @@ -229,7 +226,7 @@ public class KafkaAdmin { return true; } - List allPartitions = getClient().describeTopics(existingTopics).allTopicNames().get(requestTimeoutMs, TimeUnit.MILLISECONDS) + List allPartitions = getClient().describeTopics(existingTopics).allTopicNames().get(settings.getRequestTimeoutMs(), TimeUnit.MILLISECONDS) .entrySet().stream() .flatMap(entry -> { String topic = entry.getKey(); @@ -239,9 +236,9 @@ public class KafkaAdmin { .toList(); Map beginningOffsets = getClient().listOffsets(allPartitions.stream() - .collect(Collectors.toMap(partition -> partition, partition -> OffsetSpec.earliest()))).all().get(requestTimeoutMs, TimeUnit.MILLISECONDS); + .collect(Collectors.toMap(partition -> partition, partition -> OffsetSpec.earliest()))).all().get(settings.getRequestTimeoutMs(), TimeUnit.MILLISECONDS); Map endOffsets = getClient().listOffsets(allPartitions.stream() - .collect(Collectors.toMap(partition -> partition, partition -> OffsetSpec.latest()))).all().get(requestTimeoutMs, TimeUnit.MILLISECONDS); + .collect(Collectors.toMap(partition -> partition, partition -> OffsetSpec.latest()))).all().get(settings.getRequestTimeoutMs(), TimeUnit.MILLISECONDS); for (TopicPartition partition : allPartitions) { long beginningOffset = beginningOffsets.get(partition).offset(); @@ -261,7 +258,7 @@ public class KafkaAdmin { public void deleteConsumerGroup(String consumerGroupId) { try { - getClient().deleteConsumerGroups(List.of(consumerGroupId)).all().get(requestTimeoutMs, TimeUnit.MILLISECONDS); + getClient().deleteConsumerGroups(List.of(consumerGroupId)).all().get(settings.getRequestTimeoutMs(), TimeUnit.MILLISECONDS); } catch (Exception e) { log.warn("Failed to delete consumer group {}", consumerGroupId, e); } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java index 11736f68cf..3dbcff1863 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java @@ -112,6 +112,7 @@ public class TbKafkaSettings { @Value("${queue.kafka.fetch_max_bytes:134217728}") private int fetchMaxBytes; + @Getter @Value("${queue.kafka.request.timeout.ms:30000}") private int requestTimeoutMs; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java index 4cf8d825b9..36cf1639db 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java @@ -360,20 +360,25 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { @Override public Lwm2mDeviceProfileTransportConfiguration getProfile(Registration registration) { UUID profileId = getClientByEndpoint(registration.getEndpoint()).getProfileId(); - return doGetAndCache(profileId); + return profileId != null ? doGetAndCache(profileId) : null; } private Lwm2mDeviceProfileTransportConfiguration doGetAndCache(UUID profileId) { - - Lwm2mDeviceProfileTransportConfiguration result = profiles.get(profileId); - if (result == null) { - log.debug("Fetching profile [{}]", profileId); - DeviceProfile deviceProfile = deviceProfileCache.get(new DeviceProfileId(profileId)); - if (deviceProfile != null) { - result = profileUpdate(deviceProfile); - } else { - log.warn("Device profile was not found! Most probably device profile [{}] has been removed from the database.", profileId); + Lwm2mDeviceProfileTransportConfiguration result; + if (profileId != null) { + result = profiles.get(profileId); + if (result == null) { + log.debug("Fetching profile [{}]", profileId); + DeviceProfile deviceProfile = deviceProfileCache.get(new DeviceProfileId(profileId)); + if (deviceProfile != null) { + result = profileUpdate(deviceProfile); + } else { + log.warn("Device profile was not found! Most probably device profile [{}] has been removed from the database.", profileId); + } } + } else { + log.warn("Device profile not found! The device profile ID is null. Return Lwm2mDeviceProfileTransportConfiguration with default."); + result = new Lwm2mDeviceProfileTransportConfiguration(); } return result; } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java index 79f077a71e..325f1e2280 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java @@ -201,35 +201,39 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl } var clientSettings = clientContext.getProfile(client.getRegistration()).getClientLwM2mSettings(); - initFwStrategy(client, clientSettings); - initSwStrategy(client, clientSettings); - - if (!attributesToFetch.isEmpty()) { - var future = attributesService.getSharedAttributes(client, attributesToFetch); - DonAsynchron.withCallback(future, attrs -> { - if (fwInfo.isSupported()) { - Optional newFwTitle = getAttributeValue(attrs, FIRMWARE_TITLE); - Optional newFwVersion = getAttributeValue(attrs, FIRMWARE_VERSION); - Optional newFwTag = getAttributeValue(attrs, FIRMWARE_TAG); - Optional newFwUrl = getAttributeValue(attrs, FIRMWARE_URL); - if (newFwTitle.isPresent() && newFwVersion.isPresent() && !isOtaDownloading(client) && !UPDATING.equals(fwInfo.status)) { - onTargetFirmwareUpdate(client, newFwTitle.get(), newFwVersion.get(), newFwUrl, newFwTag); + if (clientSettings != null) { + initFwStrategy(client, clientSettings); + initSwStrategy(client, clientSettings); + + + if (!attributesToFetch.isEmpty()) { + var future = attributesService.getSharedAttributes(client, attributesToFetch); + DonAsynchron.withCallback(future, attrs -> { + if (fwInfo.isSupported()) { + Optional newFwTitle = getAttributeValue(attrs, FIRMWARE_TITLE); + Optional newFwVersion = getAttributeValue(attrs, FIRMWARE_VERSION); + Optional newFwTag = getAttributeValue(attrs, FIRMWARE_TAG); + Optional newFwUrl = getAttributeValue(attrs, FIRMWARE_URL); + if (newFwTitle.isPresent() && newFwVersion.isPresent() && !isOtaDownloading(client) && !UPDATING.equals(fwInfo.status)) { + onTargetFirmwareUpdate(client, newFwTitle.get(), newFwVersion.get(), newFwUrl, newFwTag); + } } - } - if (swInfo.isSupported()) { - Optional newSwTitle = getAttributeValue(attrs, SOFTWARE_TITLE); - Optional newSwVersion = getAttributeValue(attrs, SOFTWARE_VERSION); - Optional newSwTag = getAttributeValue(attrs, SOFTWARE_TAG); - Optional newSwUrl = getAttributeValue(attrs, SOFTWARE_URL); - if (newSwTitle.isPresent() && newSwVersion.isPresent()) { - onTargetSoftwareUpdate(client, newSwTitle.get(), newSwVersion.get(), newSwUrl, newSwTag); + if (swInfo.isSupported()) { + Optional newSwTitle = getAttributeValue(attrs, SOFTWARE_TITLE); + Optional newSwVersion = getAttributeValue(attrs, SOFTWARE_VERSION); + Optional newSwTag = getAttributeValue(attrs, SOFTWARE_TAG); + Optional newSwUrl = getAttributeValue(attrs, SOFTWARE_URL); + if (newSwTitle.isPresent() && newSwVersion.isPresent()) { + onTargetSoftwareUpdate(client, newSwTitle.get(), newSwVersion.get(), newSwUrl, newSwTag); + } } - } - }, throwable -> { - if (fwInfo.isSupported()) { - update(fwInfo); - } - }, executor); + + }, throwable -> { + if (fwInfo.isSupported()) { + update(fwInfo); + } + }, executor); + } } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java index df432732b2..3c9828af22 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/TbLwM2mRedisRegistrationStore.java @@ -750,11 +750,14 @@ public class TbLwM2mRedisRegistrationStore implements RegistrationStore, Startab System.currentTimeMillis(), 0, cleanLimit); for (byte[] endpoint : endpointsExpired) { - Registration r = deserializeReg(connection.get(toEndpointKey(endpoint))); - if (!r.isAlive(gracePeriod)) { - Deregistration dereg = removeRegistration(connection, r.getId(), true); - if (dereg != null) - expirationListener.registrationExpired(dereg.getRegistration(), dereg.getObservations()); + byte[] data = connection.get(toEndpointKey(endpoint)); + if (data != null && data.length > 0) { + Registration r = deserializeReg(data); + if (!r.isAlive(gracePeriod)) { + Deregistration dereg = removeRegistration(connection, r.getId(), true); + if (dereg != null) + expirationListener.registrationExpired(dereg.getRegistration(), dereg.getObservations()); + } } } } catch (Exception e) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java index b7e56c139a..0ea3dbcced 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java @@ -486,13 +486,18 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl */ private void initClientTelemetry(LwM2mClient lwM2MClient) { Lwm2mDeviceProfileTransportConfiguration profile = clientContext.getProfile(lwM2MClient.getRegistration()); - Set supportedObjects = clientContext.getSupportedIdVerInClient(lwM2MClient); - if (supportedObjects != null && !supportedObjects.isEmpty()) { - this.sendInitObserveRequests(lwM2MClient, profile, supportedObjects); - this.sendReadRequests(lwM2MClient, profile, supportedObjects); - this.sendWriteAttributeRequests(lwM2MClient, profile, supportedObjects); + if (profile != null) { + Set supportedObjects = clientContext.getSupportedIdVerInClient(lwM2MClient); + if (supportedObjects != null && !supportedObjects.isEmpty()) { + this.sendInitObserveRequests(lwM2MClient, profile, supportedObjects); + this.sendReadRequests(lwM2MClient, profile, supportedObjects); + this.sendWriteAttributeRequests(lwM2MClient, profile, supportedObjects); // Removed. Used only for debug. // this.sendDiscoverRequests(lwM2MClient, profile, supportedObjects); + } + } else { + log.warn("[{}] Failed to process initClientTelemetry! Profile is null. Update procedure may not have completed after reboot yet", lwM2MClient.getEndpoint()); + logService.log(lwM2MClient, "Failed to process initClientTelemetry. Profile is null. Update procedure may not have completed after reboot yet"); } } @@ -1028,7 +1033,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl }); } - private void updateValueOta(List clients, Lwm2mDeviceProfileTransportConfiguration oldProfile, Lwm2mDeviceProfileTransportConfiguration newProfile) { + private void updateValueOta(List clients, Lwm2mDeviceProfileTransportConfiguration newProfile, Lwm2mDeviceProfileTransportConfiguration oldProfile) { OtherConfiguration newLwM2mSettings = newProfile.getClientLwM2mSettings(); OtherConfiguration oldLwM2mSettings = oldProfile.getClientLwM2mSettings(); if (!newLwM2mSettings.getFwUpdateStrategy().equals(oldLwM2mSettings.getFwUpdateStrategy()) @@ -1110,6 +1115,9 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl v -> attributesService.onAttributesUpdate(lwM2MClient, v, logFailedUpdateOfNonChangedValue), t -> log.error("[{}] Failed to get attributes", lwM2MClient.getEndpoint(), t), executor); + } else { + log.warn("[{}] Failed to process initAttributes! Profile is null. Update procedure may not have completed after reboot yet", lwM2MClient.getEndpoint()); + logService.log(lwM2MClient, "Failed to process initAttributes. Profile is null. Update procedure may not have completed after reboot yet"); } } @@ -1119,7 +1127,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl private Map getNamesFromProfileForSharedAttributes(LwM2mClient lwM2MClient) { Lwm2mDeviceProfileTransportConfiguration profile = clientContext.getProfile(lwM2MClient.getRegistration()); - return profile.getObserveAttr().getKeyName(); + return profile != null ? profile.getObserveAttr().getKeyName() : Collections.emptyMap(); } public LwM2MTransportServerConfig getConfig() { 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 867b39cd23..77396c69fa 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 @@ -20,7 +20,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Component; @@ -48,7 +47,7 @@ import java.util.concurrent.TimeUnit; @Slf4j @Component("MqttSslHandlerProvider") -@ConditionalOnProperty(prefix = "transport.mqtt.ssl", value = "enabled", havingValue = "true", matchIfMissing = false) +@TbMqttSslTransportComponent public class MqttSslHandlerProvider { @Value("${transport.mqtt.ssl.protocol}") diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TbMqttSslTransportComponent.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TbMqttSslTransportComponent.java new file mode 100644 index 0000000000..2a0e8ef42f --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TbMqttSslTransportComponent.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Same as @TbMqttTransportComponent with additional condition by `transport.mqtt.ssl.enabled == true` + */ + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.mqtt.enabled:true}'=='true' && '${transport.mqtt.ssl.enabled:false}'=='true')") +public @interface TbMqttSslTransportComponent {} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TbMqttTransportComponent.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TbMqttTransportComponent.java index b517cda103..0d034aebb2 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TbMqttTransportComponent.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/TbMqttTransportComponent.java @@ -21,7 +21,11 @@ import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +/** + * See also @TbMqttSslTransportComponent + */ + @Inherited @Retention(RetentionPolicy.RUNTIME) -@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.mqtt.enabled}'=='true')") +@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.mqtt.enabled:true}'=='true')") public @interface TbMqttTransportComponent {} 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 8e5f50da1d..517bc349a1 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 @@ -64,7 +64,7 @@ import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.tenant.TenantService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java index 86f3cfe130..6b985a5977 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java @@ -37,7 +37,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.CachedVersionedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; 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 7d8442fd3a..64cb66f6a8 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 @@ -53,7 +53,7 @@ import org.thingsboard.server.dao.entity.EntityCountService; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.sql.JpaExecutorService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java b/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java index 2c5d6fb3c5..7708e08f31 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java @@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentScope; import org.thingsboard.server.common.data.plugin.ComponentType; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.Validator; diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index 28e39ae4e7..d6811b4812 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -45,7 +45,7 @@ import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.entity.EntityCountService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index 71e912c215..cc2579d645 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -50,7 +50,7 @@ import org.thingsboard.server.dao.entity.EntityCountService; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.service.DataValidator; diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java index 3423a74543..ceb35109c2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java @@ -43,9 +43,9 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.dao.service.validator.DeviceCredentialsDataValidator; +import org.thingsboard.server.exception.DataValidationException; import java.util.Objects; diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 612be215e5..e377adf9ff 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -46,11 +46,11 @@ import org.thingsboard.server.common.msg.EncryptionUtil; import org.thingsboard.server.dao.entity.CachedVersionedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.resource.ImageService; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.validator.DeviceProfileDataValidator; +import org.thingsboard.server.exception.DataValidationException; import java.io.ByteArrayInputStream; import java.security.cert.Certificate; diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index 5af2198911..fc8c7918a6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -80,7 +80,7 @@ import org.thingsboard.server.dao.event.EventService; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.validator.DeviceDataValidator; diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index 708876c393..43a5b9d36b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -66,7 +66,7 @@ import org.thingsboard.server.dao.entity.EntityCountService; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DataValidator; diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/PostgresEdgeEventService.java b/dao/src/main/java/org/thingsboard/server/dao/edge/PostgresEdgeEventService.java index ecf1376a73..e38b115e88 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/PostgresEdgeEventService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/PostgresEdgeEventService.java @@ -68,7 +68,8 @@ public class PostgresEdgeEventService extends BaseEdgeEventService { Futures.addCallback(saveFuture, new FutureCallback<>() { @Override public void onSuccess(Void result) { - statsCounterService.ifPresent(statsCounterService -> statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edgeEvent.getTenantId(), edgeEvent.getEdgeId(), 1)); + statsCounterService.ifPresent(statsCounterService -> + statsCounterService.recordEvent(EdgeStatsKey.DOWNLINK_MSGS_ADDED, edgeEvent.getTenantId(), edgeEvent.getEdgeId(), 1)); eventPublisher.publishEvent(SaveEntityEvent.builder() .tenantId(edgeEvent.getTenantId()) .entityId(edgeEvent.getEdgeId()) diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStatsCounterService.java b/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStatsCounterService.java index 16111cf514..ea8b85a287 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStatsCounterService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/stats/EdgeStatsCounterService.java @@ -24,13 +24,13 @@ import org.thingsboard.server.common.data.id.TenantId; import java.util.concurrent.ConcurrentHashMap; -@ConditionalOnProperty(prefix = "edges.stats", name = "enabled", havingValue = "true", matchIfMissing = false) +@ConditionalOnProperty(prefix = "edges.stats", name = "enabled", havingValue = "true") @Service @Slf4j @Getter public class EdgeStatsCounterService { - private final ConcurrentHashMap counterByEdge = new ConcurrentHashMap<>(); + private final ConcurrentHashMap msgCountersByEdge = new ConcurrentHashMap<>(); public void recordEvent(EdgeStatsKey type, TenantId tenantId, EdgeId edgeId, long value) { MsgCounters counters = getOrCreateCounters(tenantId, edgeId); @@ -39,19 +39,16 @@ public class EdgeStatsCounterService { case DOWNLINK_MSGS_PUSHED -> counters.getMsgsPushed().addAndGet(value); case DOWNLINK_MSGS_PERMANENTLY_FAILED -> counters.getMsgsPermanentlyFailed().addAndGet(value); case DOWNLINK_MSGS_TMP_FAILED -> counters.getMsgsTmpFailed().addAndGet(value); + case DOWNLINK_MSGS_LAG -> counters.getMsgsLag().set(value); } } - public void setDownlinkMsgsLag(TenantId tenantId, EdgeId edgeId, long value) { - getOrCreateCounters(tenantId, edgeId).getMsgsLag().set(value); + public MsgCounters getOrCreateCounters(TenantId tenantId, EdgeId edgeId) { + return msgCountersByEdge.computeIfAbsent(edgeId, id -> new MsgCounters(tenantId)); } public void clear(EdgeId edgeId) { - counterByEdge.remove(edgeId); - } - - public MsgCounters getOrCreateCounters(TenantId tenantId, EdgeId edgeId) { - return counterByEdge.computeIfAbsent(edgeId, id -> new MsgCounters(tenantId)); + msgCountersByEdge.remove(edgeId); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java index 5e0a0df5ad..2a695d5724 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java @@ -44,7 +44,7 @@ import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.housekeeper.CleanUpService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; diff --git a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java index e7edc93343..11518aaaae 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entityview/EntityViewServiceImpl.java @@ -51,7 +51,7 @@ import org.thingsboard.server.dao.entity.CachedVersionedEntityService; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.validator.EntityViewDataValidator; import org.thingsboard.server.dao.sql.JpaExecutorService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/exception/DeviceCredentialsValidationException.java b/dao/src/main/java/org/thingsboard/server/dao/exception/DeviceCredentialsValidationException.java index b2cd2cc7bf..3dcbfe2806 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/exception/DeviceCredentialsValidationException.java +++ b/dao/src/main/java/org/thingsboard/server/dao/exception/DeviceCredentialsValidationException.java @@ -15,8 +15,12 @@ */ package org.thingsboard.server.dao.exception; +import org.thingsboard.server.exception.DataValidationException; + public class DeviceCredentialsValidationException extends DataValidationException { + public DeviceCredentialsValidationException(String message) { super(message); } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 0cd80c0f3b..16493b90fd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -257,6 +257,7 @@ public class ModelConstants { public static final String ALARM_ORIGINATOR_NAME_PROPERTY = "originator_name"; public static final String ALARM_ORIGINATOR_LABEL_PROPERTY = "originator_label"; public static final String ALARM_ORIGINATOR_TYPE_PROPERTY = "originator_type"; + public static final String ALARM_ORIGINATOR_DISPLAY_NAME_PROPERTY = "originator_display_name"; public static final String ALARM_SEVERITY_PROPERTY = "severity"; public static final String ALARM_ASSIGNEE_ID_PROPERTY = "assignee_id"; public static final String ALARM_ASSIGNEE_FIRST_NAME_PROPERTY = "assignee_first_name"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateServiceImpl.java index 21dd339c8a..03cc68876a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ConfigTemplateServiceImpl.java @@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.id.OAuth2ClientRegistrationTemplateId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; import org.thingsboard.server.dao.entity.AbstractEntityService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import java.util.List; diff --git a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java index 16894fe56c..e20b6b872f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ota/BaseOtaPackageService.java @@ -41,7 +41,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java index 248326a722..991193aa78 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java @@ -34,7 +34,7 @@ import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; 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 90dd63f7da..9e212c6d96 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 @@ -52,7 +52,7 @@ import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.entity.EntityService; import org.thingsboard.server.dao.eventsourcing.RelationActionEvent; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.sql.JpaExecutorService; import org.thingsboard.server.dao.sql.relation.JpaRelationQueryExecutorService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index 2177d493e3..b18580544a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -58,7 +58,7 @@ import org.thingsboard.server.dao.dashboard.DashboardInfoDao; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.rule.RuleChainDao; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; 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 ea93df5665..d225be8015 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 @@ -63,11 +63,11 @@ import org.thingsboard.server.dao.entity.EntityCountService; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.validator.RuleChainDataValidator; +import org.thingsboard.server.exception.DataValidationException; import java.util.ArrayList; import java.util.Collection; diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleNodeStateService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleNodeStateService.java index e534596e34..2d7b70a39f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleNodeStateService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleNodeStateService.java @@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleNodeState; import org.thingsboard.server.dao.entity.AbstractEntityService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; @Service @Slf4j diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java index 0fd55bb494..e354638b0d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java @@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.validation.NoNullChar; import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.RateLimit; import org.thingsboard.server.common.data.validation.ValidJsonSchema; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import java.util.Collection; import java.util.Set; 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 89ab237ed3..4376aa5c64 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 @@ -16,6 +16,8 @@ package org.thingsboard.server.dao.service; import com.fasterxml.jackson.databind.JsonNode; +import org.thingsboard.server.exception.DataValidationException; +import org.thingsboard.server.exception.EntitiesLimitExceededException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -27,8 +29,6 @@ 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.dao.TenantEntityWithDataDao; -import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.dao.exception.EntitiesLimitExceededException; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import java.util.HashSet; @@ -51,7 +51,8 @@ public abstract class DataValidator> { private static final String NAME = "name"; private static final String TOPIC = "topic"; - @Autowired @Lazy + @Autowired + @Lazy private ApiLimitService apiLimitService; // Returns old instance of the same object that is fetched during validation. diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AbstractHasOtaPackageValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AbstractHasOtaPackageValidator.java index eb5fa340e0..5e7382c0ce 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AbstractHasOtaPackageValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AbstractHasOtaPackageValidator.java @@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.OtaPackage; import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.ota.OtaPackageType; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.service.DataValidator; 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 779c52c222..5b158dd998 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 @@ -19,7 +19,7 @@ import lombok.AllArgsConstructor; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.settings.AdminSettingsService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelDataValidator.java index fdccf2955f..8a4680b70d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AiModelDataValidator.java @@ -20,7 +20,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.ai.AiModel; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.ai.AiModelDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmCommentDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmCommentDataValidator.java index e8192cb7b4..084b672efa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmCommentDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AlarmCommentDataValidator.java @@ -19,7 +19,7 @@ import lombok.AllArgsConstructor; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @Component 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 b6d6ba2537..1e37ed4b2f 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 @@ -19,7 +19,7 @@ import lombok.AllArgsConstructor; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ApiKeyDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ApiKeyDataValidator.java index e539c1e721..7b2ebe9d40 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ApiKeyDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ApiKeyDataValidator.java @@ -19,7 +19,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.pat.ApiKey; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.pat.ApiKeyDao; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ApiUsageDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ApiUsageDataValidator.java index 1e03f5793c..30161fe978 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ApiUsageDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ApiUsageDataValidator.java @@ -21,7 +21,7 @@ import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; 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 8f3fe62d03..71d1b2d960 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 @@ -25,7 +25,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.asset.AssetDao; import org.thingsboard.server.dao.customer.CustomerDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java index 83211a3b86..a7eece823a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AssetProfileDataValidator.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.dao.asset.AssetProfileDao; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.dashboard.DashboardService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DataValidator; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AuditLogDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AuditLogDataValidator.java index 14cc77f7a1..f98e38f967 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/AuditLogDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/AuditLogDataValidator.java @@ -18,7 +18,7 @@ package org.thingsboard.server.dao.service.validator; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.audit.AuditLog; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @Component diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/BaseOtaPackageDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/BaseOtaPackageDataValidator.java index 761a99c621..e3d4c9bb90 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/BaseOtaPackageDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/BaseOtaPackageDataValidator.java @@ -23,9 +23,9 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.dao.device.DeviceProfileDao; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.exception.DataValidationException; import java.util.Objects; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java index 6c333f36ad..7926d8c8ab 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.cf.configuration.aggregation.single.En import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.cf.CalculatedFieldDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.usagerecord.ApiLimitService; 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 830c602ccb..1701f064dc 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 @@ -19,7 +19,7 @@ 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.oauth2.OAuth2ClientRegistrationTemplate; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @Component 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 f834f6bce5..8275a7e4b6 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 @@ -19,7 +19,7 @@ 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; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; @Component 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 c6e547adbf..ba80bda39f 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 @@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.customer.CustomerServiceImpl; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; 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 aa205e9646..22f27cd260 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 @@ -20,7 +20,7 @@ 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.id.TenantId; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; 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 d61f9d455d..de1646e44b 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 @@ -25,7 +25,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.device.DeviceDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.tenant.TenantService; import java.util.Optional; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java index 401b199598..9fac76704c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DeviceProfileDataValidator.java @@ -52,11 +52,11 @@ import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.device.DeviceDao; import org.thingsboard.server.dao.device.DeviceProfileDao; import org.thingsboard.server.dao.device.DeviceProfileService; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.exception.DataValidationException; import java.io.FileInputStream; import java.security.KeyStore; @@ -341,7 +341,7 @@ public class DeviceProfileDataValidator extends AbstractHasOtaPackageValidator { @@ -43,18 +44,18 @@ public interface CalculatedFieldRepository extends JpaRepository findByTenantIdAndEntityIdAndTypes(UUID tenantId, UUID entityId, List types, String textSearch, Pageable pageable); @Query("SELECT cf FROM CalculatedFieldEntity cf WHERE cf.tenantId = :tenantId " + - "AND cf.type = :type " + + "AND cf.type IN :types " + "AND cf.entityType IN :entityTypes " + "AND (:entityIds IS NULL OR cf.entityId IN :entityIds) " + "AND (:names IS NULL OR cf.name IN :names) " + "AND (:textSearch IS NULL OR ilike(cf.name, CONCAT('%', :textSearch, '%')) = true)") - Page findByTenantIdAndFilter(UUID tenantId, String type, List entityTypes, - List entityIds, List names, String textSearch, Pageable pageable); + Page findByTenantIdAndFilter(UUID tenantId, List types, List entityTypes, + Set entityIds, Set names, String textSearch, Pageable pageable); @Query("SELECT DISTINCT cf.name FROM CalculatedFieldEntity cf " + "WHERE cf.tenantId = :tenantId AND cf.type = :type AND " + "(:textSearch IS NULL OR ilike(cf.name, CONCAT('%', :textSearch, '%')) = true)") - Page findNamesByTenantIdAndType(UUID tenantId, String type,String textSearch, Pageable pageable); + Page findNamesByTenantIdAndType(UUID tenantId, String type, String textSearch, Pageable pageable); List findAllByTenantId(UUID tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java index 94aa11a920..5093c51ba7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java @@ -107,7 +107,8 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao findByTenantIdAndFilter(TenantId tenantId, CalculatedFieldFilter filter, PageLink pageLink) { - return DaoUtil.toPageData(calculatedFieldRepository.findByTenantIdAndFilter(tenantId.getId(), filter.getType().name(), + return DaoUtil.toPageData(calculatedFieldRepository.findByTenantIdAndFilter(tenantId.getId(), + filter.getTypes().stream().map(Enum::name).toList(), filter.getEntityTypes().stream().map(Enum::name).toList(), CollectionUtils.isNotEmpty(filter.getEntityIds()) ? filter.getEntityIds() : null, CollectionUtils.isNotEmpty(filter.getNames()) ? filter.getNames() : null, 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 f3e2ba83a6..f701188064 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 @@ -117,11 +117,13 @@ public class AlarmDataAdapter { String originatorName = originatorNameObj != null ? originatorNameObj.toString() : null; Object originatorLabelObj = row.get(ModelConstants.ALARM_ORIGINATOR_LABEL_PROPERTY); String originatorLabel = originatorLabelObj != null ? originatorLabelObj.toString() : null; + Object originatorDisplayNameObj = row.get(ModelConstants.ALARM_ORIGINATOR_DISPLAY_NAME_PROPERTY); + String originatorDisplayName = originatorDisplayNameObj != null ? originatorDisplayNameObj.toString() : null; AlarmData alarmData = new AlarmData(alarm, entityId); alarmData.setOriginatorName(originatorName); alarmData.setOriginatorLabel(originatorLabel); - alarmData.setOriginatorDisplayName(StringUtils.isBlank(originatorLabel) ? originatorName : originatorLabel); + alarmData.setOriginatorDisplayName(originatorDisplayName); if (alarm.getAssigneeId() != null) { alarmData.setAssignee(new AlarmAssignee(alarm.getAssigneeId(), assigneeFirstName, assigneeLastName, assigneeEmail)); } 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 62636a9ed0..a41961768a 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 @@ -83,6 +83,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { alarmFieldColumnMap.put(ASSIGNEE_FIRST_NAME_KEY, ModelConstants.ALARM_ASSIGNEE_FIRST_NAME_PROPERTY); alarmFieldColumnMap.put(ASSIGNEE_LAST_NAME_KEY, ModelConstants.ALARM_ASSIGNEE_LAST_NAME_PROPERTY); alarmFieldColumnMap.put(ASSIGNEE_EMAIL_KEY, ModelConstants.ALARM_ASSIGNEE_EMAIL_PROPERTY); + alarmFieldColumnMap.put("originatorDisplayName", ModelConstants.ALARM_ORIGINATOR_DISPLAY_NAME_PROPERTY); } private static final String FIELDS_SELECTION = "select a.id as id," + @@ -106,6 +107,7 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { " a.type as type, " + " a.originator_name as originator_name, " + " a.originator_label as originator_label, " + + " coalesce(a.originator_label, a.originator_name) as originator_display_name, " + " a.assignee_first_name as assignee_first_name, " + " a.assignee_last_name as assignee_last_name, " + " a.assignee_email as assignee_email, " + 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 b9172306bb..c9f93dfbc5 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 @@ -35,10 +35,10 @@ import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; +import org.thingsboard.server.exception.DataValidationException; import java.util.ArrayList; import java.util.List; diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java index 431bcd8bc3..407f52df38 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserSettingsServiceImpl.java @@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.settings.UserSettings; import org.thingsboard.server.common.data.settings.UserSettingsCompositeKey; import org.thingsboard.server.common.data.settings.UserSettingsType; import org.thingsboard.server.dao.entity.AbstractCachedService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.ConstraintValidator; import java.util.Iterator; diff --git a/dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java b/dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java index 92f65c6e6d..18630a272e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java +++ b/dao/src/main/java/org/thingsboard/server/dao/util/KvUtils.java @@ -23,9 +23,9 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.IncorrectParameterException; import org.thingsboard.server.dao.service.NoXssValidator; +import org.thingsboard.server.exception.DataValidationException; import java.util.ArrayList; import java.util.List; diff --git a/dao/src/main/resources/sql/schema-views-and-functions.sql b/dao/src/main/resources/sql/schema-functions.sql similarity index 73% rename from dao/src/main/resources/sql/schema-views-and-functions.sql rename to dao/src/main/resources/sql/schema-functions.sql index a6a7ae43cb..e763210a3d 100644 --- a/dao/src/main/resources/sql/schema-views-and-functions.sql +++ b/dao/src/main/resources/sql/schema-functions.sql @@ -14,84 +14,6 @@ -- limitations under the License. -- -DROP VIEW IF EXISTS device_info_active_attribute_view CASCADE; -CREATE OR REPLACE VIEW device_info_active_attribute_view AS -SELECT d.* - , c.title as customer_title - , COALESCE((c.additional_info::json->>'isPublic')::bool, FALSE) as customer_is_public - , d.type as device_profile_name - , COALESCE(da.bool_v, FALSE) as active -FROM device d - LEFT JOIN customer c ON c.id = d.customer_id - LEFT JOIN attribute_kv da ON da.entity_id = d.id AND da.attribute_type = 2 AND da.attribute_key = (select key_id from key_dictionary where key = 'active'); - -DROP VIEW IF EXISTS device_info_active_ts_view CASCADE; -CREATE OR REPLACE VIEW device_info_active_ts_view AS -SELECT d.* - , c.title as customer_title - , COALESCE((c.additional_info::json->>'isPublic')::bool, FALSE) as customer_is_public - , d.type as device_profile_name - , COALESCE(dt.bool_v, FALSE) as active -FROM device d - LEFT JOIN customer c ON c.id = d.customer_id - LEFT JOIN ts_kv_latest dt ON dt.entity_id = d.id and dt.key = (select key_id from key_dictionary where key = 'active'); - -DROP VIEW IF EXISTS device_info_view CASCADE; -CREATE OR REPLACE VIEW device_info_view AS SELECT * FROM device_info_active_attribute_view; - -DROP VIEW IF EXISTS alarm_info CASCADE; -CREATE VIEW alarm_info AS -SELECT a.*, -(CASE WHEN a.acknowledged AND a.cleared THEN 'CLEARED_ACK' - WHEN NOT a.acknowledged AND a.cleared THEN 'CLEARED_UNACK' - WHEN a.acknowledged AND NOT a.cleared THEN 'ACTIVE_ACK' - WHEN NOT a.acknowledged AND NOT a.cleared THEN 'ACTIVE_UNACK' END) as status, -COALESCE(CASE WHEN a.originator_type = 0 THEN (select title from tenant where id = a.originator_id) - WHEN a.originator_type = 1 THEN (select title from customer where id = a.originator_id) - WHEN a.originator_type = 2 THEN (select email from tb_user where id = a.originator_id) - WHEN a.originator_type = 3 THEN (select title from dashboard where id = a.originator_id) - WHEN a.originator_type = 4 THEN (select name from asset where id = a.originator_id) - WHEN a.originator_type = 5 THEN (select name from device where id = a.originator_id) - WHEN a.originator_type = 9 THEN (select name from entity_view where id = a.originator_id) - WHEN a.originator_type = 13 THEN (select name from device_profile where id = a.originator_id) - WHEN a.originator_type = 14 THEN (select name from asset_profile where id = a.originator_id) - WHEN a.originator_type = 18 THEN (select name from edge where id = a.originator_id) END - , 'Deleted') originator_name, -COALESCE(CASE WHEN a.originator_type = 0 THEN (select title from tenant where id = a.originator_id) - WHEN a.originator_type = 1 THEN (select COALESCE(NULLIF(title, ''), email) from customer where id = a.originator_id) - WHEN a.originator_type = 2 THEN (select email from tb_user where id = a.originator_id) - WHEN a.originator_type = 3 THEN (select title from dashboard where id = a.originator_id) - WHEN a.originator_type = 4 THEN (select COALESCE(NULLIF(label, ''), name) from asset where id = a.originator_id) - WHEN a.originator_type = 5 THEN (select COALESCE(NULLIF(label, ''), name) from device where id = a.originator_id) - WHEN a.originator_type = 9 THEN (select name from entity_view where id = a.originator_id) - WHEN a.originator_type = 13 THEN (select name from device_profile where id = a.originator_id) - WHEN a.originator_type = 14 THEN (select name from asset_profile where id = a.originator_id) - WHEN a.originator_type = 18 THEN (select COALESCE(NULLIF(label, ''), name) from edge where id = a.originator_id) END - , 'Deleted') as originator_label, -u.first_name as assignee_first_name, u.last_name as assignee_last_name, u.email as assignee_email -FROM alarm a -LEFT JOIN tb_user u ON u.id = a.assignee_id; - -DROP VIEW IF EXISTS edge_active_attribute_view CASCADE; -CREATE OR REPLACE VIEW edge_active_attribute_view AS -SELECT ee.id - , ee.created_time - , ee.additional_info - , ee.customer_id - , ee.root_rule_chain_id - , ee.type - , ee.name - , ee.label - , ee.routing_key - , ee.secret - , ee.tenant_id - , ee.version -FROM edge ee - JOIN attribute_kv ON ee.id = attribute_kv.entity_id - JOIN key_dictionary ON attribute_kv.attribute_key = key_dictionary.key_id -WHERE attribute_kv.bool_v = true AND key_dictionary.key = 'active' -ORDER BY ee.id; - CREATE OR REPLACE FUNCTION create_or_update_active_alarm( t_id uuid, c_id uuid, a_id uuid, a_created_ts bigint, a_o_id uuid, a_o_type integer, a_type varchar, @@ -306,19 +228,6 @@ BEGIN END $$; -DROP VIEW IF EXISTS widget_type_info_view CASCADE; -CREATE OR REPLACE VIEW widget_type_info_view AS -SELECT t.*, - COALESCE((t.descriptor::json->>'type')::text, '') as widget_type, - array_to_json(ARRAY( - SELECT json_build_object('id', wb.widgets_bundle_id, 'name', b.title) - FROM widgets_bundle_widget wb - JOIN widgets_bundle b ON wb.widgets_bundle_id = b.id - WHERE wb.widget_type_id = t.id - ORDER BY b.title - )) AS bundles -FROM widget_type t; - CREATE OR REPLACE PROCEDURE cleanup_timeseries_by_ttl(IN null_uuid uuid, IN system_ttl bigint, INOUT deleted bigint) LANGUAGE plpgsql AS @@ -389,4 +298,4 @@ BEGIN FETCH tenant_cursor INTO tenant_id_record; END LOOP; END -$$; \ No newline at end of file +$$; diff --git a/dao/src/main/resources/sql/schema-views.sql b/dao/src/main/resources/sql/schema-views.sql new file mode 100644 index 0000000000..c8f7ea6483 --- /dev/null +++ b/dao/src/main/resources/sql/schema-views.sql @@ -0,0 +1,106 @@ +-- +-- Copyright © 2016-2025 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. +-- + +DROP VIEW IF EXISTS device_info_active_attribute_view CASCADE; +CREATE OR REPLACE VIEW device_info_active_attribute_view AS +SELECT d.* + , c.title as customer_title + , COALESCE((c.additional_info::json->>'isPublic')::bool, FALSE) as customer_is_public + , d.type as device_profile_name + , COALESCE(da.bool_v, FALSE) as active +FROM device d + LEFT JOIN customer c ON c.id = d.customer_id + LEFT JOIN attribute_kv da ON da.entity_id = d.id AND da.attribute_type = 2 AND da.attribute_key = (select key_id from key_dictionary where key = 'active'); + +DROP VIEW IF EXISTS device_info_active_ts_view CASCADE; +CREATE OR REPLACE VIEW device_info_active_ts_view AS +SELECT d.* + , c.title as customer_title + , COALESCE((c.additional_info::json->>'isPublic')::bool, FALSE) as customer_is_public + , d.type as device_profile_name + , COALESCE(dt.bool_v, FALSE) as active +FROM device d + LEFT JOIN customer c ON c.id = d.customer_id + LEFT JOIN ts_kv_latest dt ON dt.entity_id = d.id and dt.key = (select key_id from key_dictionary where key = 'active'); + +DROP VIEW IF EXISTS device_info_view CASCADE; +CREATE OR REPLACE VIEW device_info_view AS SELECT * FROM device_info_active_attribute_view; + +DROP VIEW IF EXISTS alarm_info CASCADE; +CREATE VIEW alarm_info AS +SELECT a.*, +(CASE WHEN a.acknowledged AND a.cleared THEN 'CLEARED_ACK' + WHEN NOT a.acknowledged AND a.cleared THEN 'CLEARED_UNACK' + WHEN a.acknowledged AND NOT a.cleared THEN 'ACTIVE_ACK' + WHEN NOT a.acknowledged AND NOT a.cleared THEN 'ACTIVE_UNACK' END) as status, +COALESCE(CASE WHEN a.originator_type = 0 THEN (select title from tenant where id = a.originator_id) + WHEN a.originator_type = 1 THEN (select title from customer where id = a.originator_id) + WHEN a.originator_type = 2 THEN (select email from tb_user where id = a.originator_id) + WHEN a.originator_type = 3 THEN (select title from dashboard where id = a.originator_id) + WHEN a.originator_type = 4 THEN (select name from asset where id = a.originator_id) + WHEN a.originator_type = 5 THEN (select name from device where id = a.originator_id) + WHEN a.originator_type = 9 THEN (select name from entity_view where id = a.originator_id) + WHEN a.originator_type = 13 THEN (select name from device_profile where id = a.originator_id) + WHEN a.originator_type = 14 THEN (select name from asset_profile where id = a.originator_id) + WHEN a.originator_type = 18 THEN (select name from edge where id = a.originator_id) END + , 'Deleted') originator_name, +COALESCE(CASE WHEN a.originator_type = 0 THEN (select title from tenant where id = a.originator_id) + WHEN a.originator_type = 1 THEN (select COALESCE(NULLIF(title, ''), email) from customer where id = a.originator_id) + WHEN a.originator_type = 2 THEN (select email from tb_user where id = a.originator_id) + WHEN a.originator_type = 3 THEN (select title from dashboard where id = a.originator_id) + WHEN a.originator_type = 4 THEN (select COALESCE(NULLIF(label, ''), name) from asset where id = a.originator_id) + WHEN a.originator_type = 5 THEN (select COALESCE(NULLIF(label, ''), name) from device where id = a.originator_id) + WHEN a.originator_type = 9 THEN (select name from entity_view where id = a.originator_id) + WHEN a.originator_type = 13 THEN (select name from device_profile where id = a.originator_id) + WHEN a.originator_type = 14 THEN (select name from asset_profile where id = a.originator_id) + WHEN a.originator_type = 18 THEN (select COALESCE(NULLIF(label, ''), name) from edge where id = a.originator_id) END + , 'Deleted') as originator_label, +u.first_name as assignee_first_name, u.last_name as assignee_last_name, u.email as assignee_email +FROM alarm a +LEFT JOIN tb_user u ON u.id = a.assignee_id; + +DROP VIEW IF EXISTS edge_active_attribute_view CASCADE; +CREATE OR REPLACE VIEW edge_active_attribute_view AS +SELECT ee.id + , ee.created_time + , ee.additional_info + , ee.customer_id + , ee.root_rule_chain_id + , ee.type + , ee.name + , ee.label + , ee.routing_key + , ee.secret + , ee.tenant_id + , ee.version +FROM edge ee + JOIN attribute_kv ON ee.id = attribute_kv.entity_id + JOIN key_dictionary ON attribute_kv.attribute_key = key_dictionary.key_id +WHERE attribute_kv.bool_v = true AND key_dictionary.key = 'active' +ORDER BY ee.id; + +DROP VIEW IF EXISTS widget_type_info_view CASCADE; +CREATE OR REPLACE VIEW widget_type_info_view AS +SELECT t.*, + COALESCE((t.descriptor::json->>'type')::text, '') as widget_type, + array_to_json(ARRAY( + SELECT json_build_object('id', wb.widgets_bundle_id, 'name', b.title) + FROM widgets_bundle_widget wb + JOIN widgets_bundle b ON wb.widgets_bundle_id = b.id + WHERE wb.widget_type_id = t.id + ORDER BY b.title + )) AS bundles +FROM widget_type t; diff --git a/dao/src/test/java/org/thingsboard/server/dao/PostgreSqlInitializer.java b/dao/src/test/java/org/thingsboard/server/dao/PostgreSqlInitializer.java index b2bb965380..49956bd6d4 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/PostgreSqlInitializer.java +++ b/dao/src/test/java/org/thingsboard/server/dao/PostgreSqlInitializer.java @@ -33,7 +33,8 @@ public class PostgreSqlInitializer { "sql/schema-entities.sql", "sql/schema-entities-idx.sql", "sql/schema-entities-idx-psql-addon.sql", - "sql/schema-views-and-functions.sql", + "sql/schema-views.sql", + "sql/schema-functions.sql", "sql/system-data.sql", "sql/system-test-psql.sql"); private static final String dropAllTablesSqlFile = "sql/psql/drop-all-tables.sql"; diff --git a/dao/src/test/java/org/thingsboard/server/dao/TimescaleSqlInitializer.java b/dao/src/test/java/org/thingsboard/server/dao/TimescaleSqlInitializer.java index c9d13b6926..511afd1eca 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/TimescaleSqlInitializer.java +++ b/dao/src/test/java/org/thingsboard/server/dao/TimescaleSqlInitializer.java @@ -33,7 +33,8 @@ public class TimescaleSqlInitializer { "sql/schema-entities.sql", "sql/schema-entities-idx.sql", "sql/schema-entities-idx-psql-addon.sql", - "sql/schema-views-and-functions.sql", + "sql/schema-views.sql", + "sql/schema-functions.sql", "sql/system-data.sql", "sql/system-test-psql.sql"); private static final String dropAllTablesSqlFile = "sql/psql/drop-all-tables.sql"; @@ -63,4 +64,5 @@ public class TimescaleSqlInitializer { 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/AdminSettingsServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AdminSettingsServiceTest.java index 67042792b4..0f629315aa 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AdminSettingsServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AdminSettingsServiceTest.java @@ -25,7 +25,7 @@ import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.settings.AdminSettingsService; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/ApiKeyServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/ApiKeyServiceTest.java index 64444a0d31..2ccf658b61 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/ApiKeyServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/ApiKeyServiceTest.java @@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.pat.ApiKey; import org.thingsboard.server.common.data.pat.ApiKeyInfo; import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.pat.ApiKeyService; import org.thingsboard.server.dao.user.UserService; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AssetProfileServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AssetProfileServiceTest.java index 4efe029c21..ed38935208 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AssetProfileServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AssetProfileServiceTest.java @@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import java.util.ArrayList; import java.util.Collections; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java index 92ba9380c4..63a7167340 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java @@ -58,7 +58,7 @@ import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.customer.CustomerService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.tenant.TenantProfileService; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index 2adfc3076c..5cbc3c11f7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java @@ -36,7 +36,7 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.device.DeviceService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import java.util.ArrayList; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/ConstraintValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/ConstraintValidatorTest.java index 835f31f082..81073d4bbe 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/ConstraintValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/ConstraintValidatorTest.java @@ -19,7 +19,7 @@ import org.junit.Assert; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.thingsboard.server.common.data.kv.StringDataEntry; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; class ConstraintValidatorTest { @@ -47,4 +47,4 @@ class ConstraintValidatorTest { Assertions.assertTrue(MIN_IN_MS > end - start); } -} \ No newline at end of file +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceTest.java index e69685f429..b2432ccc7c 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CustomerServiceTest.java @@ -44,7 +44,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.customer.CustomerService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import java.util.ArrayList; import java.util.List; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DashboardServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DashboardServiceTest.java index 6c81d31e57..333e3ce4ff 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DashboardServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DashboardServiceTest.java @@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.edge.EdgeService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import java.io.IOException; import java.util.ArrayList; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DataValidatorTest.java index c8ad03f79c..a59d5dc43f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DataValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DataValidatorTest.java @@ -20,7 +20,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.spy; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceTest.java index d5c969b6f3..641b8465fd 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceTest.java @@ -31,7 +31,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; @DaoSqlTest public class DeviceCredentialsServiceTest extends AbstractServiceTest { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceProfileServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceProfileServiceTest.java index c89902befd..cce711e061 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceProfileServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceProfileServiceTest.java @@ -36,7 +36,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.ota.OtaPackageService; import java.nio.ByteBuffer; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java index 5157bb1b63..ed791ec27e 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java @@ -68,7 +68,7 @@ import org.thingsboard.server.dao.customer.CustomerService; 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.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.service.validator.DeviceCredentialsDataValidator; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/EdgeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/EdgeServiceTest.java index 32d0c2cb84..8f4977ad8c 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/EdgeServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/EdgeServiceTest.java @@ -40,7 +40,7 @@ import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.edge.EdgeService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TenantProfileService; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/OAuth2ConfigTemplateServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/OAuth2ConfigTemplateServiceTest.java index 460a1424fc..0451b740aa 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/OAuth2ConfigTemplateServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/OAuth2ConfigTemplateServiceTest.java @@ -26,7 +26,7 @@ import org.thingsboard.server.common.data.oauth2.MapperType; import org.thingsboard.server.common.data.oauth2.OAuth2BasicMapperConfig; import org.thingsboard.server.common.data.oauth2.OAuth2ClientRegistrationTemplate; import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; import java.util.Arrays; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java index 70f2ff119d..48a8792452 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/OtaPackageServiceTest.java @@ -36,7 +36,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.tenant.TenantProfileService; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/QueueServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/QueueServiceTest.java index 3a92a817f7..a65b36a657 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/QueueServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/QueueServiceTest.java @@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.queue.SubmitStrategy; import org.thingsboard.server.common.data.queue.SubmitStrategyType; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.tenant.TenantProfileService; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/QueueStatsServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/QueueStatsServiceTest.java index 685efbe2f6..9886841c69 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/QueueStatsServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/QueueStatsServiceTest.java @@ -27,7 +27,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.queue.QueueStats; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.queue.QueueStatsService; import static org.assertj.core.api.Assertions.assertThat; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java index e3827d6a6e..a0f27be4e2 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java @@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java index 11fa38dc19..fc26ab96cc 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java @@ -34,7 +34,7 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.edge.EdgeService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/TenantProfileServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/TenantProfileServiceTest.java index 5887f6868e..51a2f6a09d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/TenantProfileServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/TenantProfileServiceTest.java @@ -38,7 +38,7 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration; import org.thingsboard.server.common.util.ProtoUtils; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.gen.transport.TransportProtos; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceTest.java index def4644b91..cb337d9c62 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/TenantServiceTest.java @@ -62,7 +62,7 @@ import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.entityview.EntityViewService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rpc.RpcService; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/UserServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/UserServiceTest.java index ad87e73f79..25d01c40ad 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/UserServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/UserServiceTest.java @@ -33,7 +33,7 @@ import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.settings.UserSettings; import org.thingsboard.server.dao.customer.CustomerService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.user.UserService; import java.util.ArrayList; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/WidgetTypeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/WidgetTypeServiceTest.java index 0e6c7beadb..aef0a17a38 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/WidgetTypeServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/WidgetTypeServiceTest.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.id.WidgetTypeId; import org.thingsboard.server.common.data.widget.WidgetType; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; import org.thingsboard.server.common.data.widget.WidgetsBundle; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetsBundleService; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceTest.java index 0165d3d164..eff2a15f9c 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/WidgetsBundleServiceTest.java @@ -25,7 +25,7 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.common.data.widget.WidgetsBundleFilter; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.widget.WidgetsBundleService; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/install/sql/EntitiesSchemaSqlTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/install/sql/EntitiesSchemaSqlTest.java index 9f10ecfb7b..b28b410a12 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/install/sql/EntitiesSchemaSqlTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/install/sql/EntitiesSchemaSqlTest.java @@ -35,8 +35,10 @@ public class EntitiesSchemaSqlTest extends AbstractServiceTest { @Value("${classpath:sql/schema-entities.sql}") private Path installEntitiesPath; - @Value("${classpath:sql/schema-views-and-functions.sql}") + @Value("${classpath:sql/schema-views.sql}") private Path installViewsPath; + @Value("${classpath:sql/schema-functions.sql}") + private Path installFunctionsPath; @Autowired private JdbcTemplate jdbcTemplate; @@ -45,10 +47,12 @@ public class EntitiesSchemaSqlTest extends AbstractServiceTest { public void testRepeatedInstall() throws IOException { String entitiesScript = Files.readString(installEntitiesPath); String viewsScript = Files.readString(installViewsPath); + String functionsScript = Files.readString(installFunctionsPath); try { for (int i = 1; i <= 2; i++) { jdbcTemplate.execute(entitiesScript); jdbcTemplate.execute(viewsScript); + jdbcTemplate.execute(functionsScript); } } catch (Exception e) { Assertions.fail("Failed to execute reinstall", e); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/AssetDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/AssetDataValidatorTest.java index 82659ead60..c3db7a251b 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/validator/AssetDataValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/AssetDataValidatorTest.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.asset.AssetDao; import org.thingsboard.server.dao.customer.CustomerDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.tenant.TenantService; import java.util.UUID; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java index 0d52f999de..38f097616d 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidatorTest.java @@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.cf.CalculatedFieldDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.usagerecord.DefaultApiLimitService; import java.util.UUID; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceDataValidatorTest.java index 687c3e67ac..12b2e26746 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceDataValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/DeviceDataValidatorTest.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.customer.CustomerDao; import org.thingsboard.server.dao.device.DeviceDao; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.tenant.TenantService; import java.util.UUID; diff --git a/edqs/src/main/resources/edqs.yml b/edqs/src/main/resources/edqs.yml index f6b611d5a9..9bdee541a3 100644 --- a/edqs/src/main/resources/edqs.yml +++ b/edqs/src/main/resources/edqs.yml @@ -177,6 +177,8 @@ queue: print-interval-ms: "${TB_QUEUE_KAFKA_CONSUMER_STATS_MIN_PRINT_INTERVAL_MS:60000}" # Time to wait for the stats-loading requests to Kafka to finish kafka-response-timeout-ms: "${TB_QUEUE_KAFKA_CONSUMER_STATS_RESPONSE_TIMEOUT_MS:1000}" + # Topics cache TTL in milliseconds. 5 minutes by default + topics_cache_ttl_ms: "${TB_QUEUE_KAFKA_TOPICS_CACHE_TTL_MS:300000}" partitions: hash_function_name: "${TB_QUEUE_PARTITIONS_HASH_FUNCTION_NAME:murmur3_128}" # murmur3_32, murmur3_128 or sha256 diff --git a/monitoring/src/main/conf/logback.xml b/monitoring/src/main/conf/logback.xml index 53caafe8bb..43001ffd43 100644 --- a/monitoring/src/main/conf/logback.xml +++ b/monitoring/src/main/conf/logback.xml @@ -19,20 +19,6 @@ - - ${pkg.logFolder}/${pkg.name}.log - - ${pkg.logFolder}/${pkg.name}.%d{yyyy-MM-dd}.%i.log - 100MB - 30 - 3GB - - - %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n - - %d{ISO8601} [%thread] %-5level %logger{36} - %msg%n @@ -40,11 +26,11 @@ - - + + + - diff --git a/monitoring/src/main/conf/tb-monitoring.conf b/monitoring/src/main/conf/tb-monitoring.conf index 493d498cd8..cd2947563f 100644 --- a/monitoring/src/main/conf/tb-monitoring.conf +++ b/monitoring/src/main/conf/tb-monitoring.conf @@ -14,9 +14,10 @@ # limitations under the License. # -export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-monitoring/gc.log:time,uptime,level,tags:filecount=10,filesize=10M" +export JAVA_OPTS="$JAVA_OPTS -Xlog:gc*,heap*,age*,safepoint=debug:file=/var/log/tb-monitoring/gc.log:time,uptime,level,tags:filecount=3,filesize=10M" export JAVA_OPTS="$JAVA_OPTS -XX:+IgnoreUnrecognizedVMOptions -XX:+HeapDumpOnOutOfMemoryError" -export JAVA_OPTS="$JAVA_OPTS -XX:-UseBiasedLocking -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" -export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=500 -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" +export JAVA_OPTS="$JAVA_OPTS -XX:+UseTLAB -XX:+ResizeTLAB -XX:+PerfDisableSharedMem -XX:+UseCondCardMark" +export JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:MaxGCPauseMillis=50 -XX:+ParallelRefProcEnabled -XX:MaxTenuringThreshold=10" +export JAVA_OPTS="$JAVA_OPTS -XX:+ExitOnOutOfMemoryError" export LOG_FILENAME=tb-monitoring.out export LOADER_PATH=/usr/share/tb-monitoring/conf diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java b/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java index c8e8d7070b..88e9ac1fa3 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java @@ -15,36 +15,43 @@ */ package org.thingsboard.monitoring; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; +import org.springframework.context.event.ContextClosedEvent; import org.springframework.scheduling.annotation.EnableScheduling; import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.monitoring.data.notification.InfoNotification; +import org.thingsboard.monitoring.notification.NotificationService; import org.thingsboard.monitoring.service.BaseMonitoringService; import org.thingsboard.monitoring.service.MonitoringEntityService; +import jakarta.annotation.PreDestroy; import java.util.List; import java.util.Map; +import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @SpringBootApplication @EnableScheduling @Slf4j +@RequiredArgsConstructor public class ThingsboardMonitoringApplication { - @Autowired - private List> monitoringServices; - @Autowired - private MonitoringEntityService entityService; + private final List> monitoringServices; + private final MonitoringEntityService entityService; + private final NotificationService notificationService; @Value("${monitoring.monitoring_rate_ms}") private int monitoringRateMs; + ScheduledExecutorService scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("monitoring"); + public static void main(String[] args) { new SpringApplicationBuilder(ThingsboardMonitoringApplication.class) .properties(Map.of("spring.config.name", "tb-monitoring")) @@ -56,12 +63,33 @@ public class ThingsboardMonitoringApplication { entityService.checkEntities(); monitoringServices.forEach(BaseMonitoringService::init); - ScheduledExecutorService scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("monitoring-executor"); - scheduler.scheduleWithFixedDelay(() -> { - monitoringServices.forEach(monitoringService -> { - monitoringService.runChecks(); - }); - }, 0, monitoringRateMs, TimeUnit.MILLISECONDS); + for (int i = 0; i < monitoringServices.size(); i++) { + int initialDelay = (monitoringRateMs / monitoringServices.size()) * i; + BaseMonitoringService service = monitoringServices.get(i); + log.info("Scheduling initialDelay {}, fixedDelay {} for monitoring '{}' ", initialDelay, monitoringRateMs, service.getClass().getSimpleName()); + scheduler.scheduleWithFixedDelay(service::runChecks, initialDelay, monitoringRateMs, TimeUnit.MILLISECONDS); + } + + String publicDashboardUrl = entityService.getDashboardPublicLink(); + notificationService.sendNotification(new InfoNotification(":rocket: <"+publicDashboardUrl+"|Monitoring> started")); + } + + @EventListener(ContextClosedEvent.class) + public void onShutdown(ContextClosedEvent event) { + log.info("Shutting down monitoring service"); + try { + var futures = notificationService.sendNotification(new InfoNotification(":warning: Monitoring is shutting down")); + for (Future future : futures) { + future.get(5, TimeUnit.SECONDS); + } + } catch (Exception e) { + log.warn("Failed to send shutdown notification", e); + } + } + + @PreDestroy + public void shutdownScheduler() { + scheduler.shutdown(); } } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/InfoNotification.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/InfoNotification.java new file mode 100644 index 0000000000..6aa42cbf2b --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/notification/InfoNotification.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2025 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.monitoring.data.notification; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class InfoNotification implements Notification { + private final String message; + @Override + public String getText() { + return message; + } +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java b/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java index 5e4dcb2f34..36529822b1 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/notification/NotificationService.java @@ -22,10 +22,13 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.thingsboard.monitoring.data.notification.Notification; import org.thingsboard.monitoring.notification.channels.NotificationChannel; +import jakarta.annotation.PreDestroy; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; @Component @RequiredArgsConstructor @@ -38,22 +41,38 @@ public class NotificationService { @Value("${monitoring.notifications.message_prefix}") private String messagePrefix; - public void sendNotification(Notification notification) { + public List> sendNotification(Notification notification) { String message; if (StringUtils.isEmpty(messagePrefix)) { message = notification.getText(); } else { - message = messagePrefix + System.lineSeparator() + notification.getText(); + message = messagePrefix + " " + notification.getText(); } - notificationChannels.forEach(notificationChannel -> { + return notificationChannels.stream().map(notificationChannel -> notificationExecutor.submit(() -> { try { notificationChannel.sendNotification(message); } catch (Exception e) { log.error("Failed to send notification to {}", notificationChannel.getClass().getSimpleName(), e); } - }); - }); + }) + ).toList(); } + @PreDestroy + public void shutdownExecutor() { + try { + notificationExecutor.shutdown(); + if (!notificationExecutor.awaitTermination(10, TimeUnit.SECONDS)) { + var dropped = notificationExecutor.shutdownNow(); + log.warn("Notification executor did not terminate in time. Forced shutdown; {} task(s) will not be executed.", dropped.size()); + } + } catch (InterruptedException e) { + var dropped = notificationExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + log.warn("Interrupted during notification executor shutdown. Forced shutdown; {} task(s) will not be executed.", dropped.size()); + } catch (Exception e) { + log.warn("Unexpected error while shutting down notification executor", e); + } + } } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java index d2cf076516..f667192cf2 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java @@ -30,13 +30,19 @@ import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget; import org.thingsboard.monitoring.config.transport.TransportType; import org.thingsboard.monitoring.util.ResourceUtils; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.ShortCustomerInfo; import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.cf.CalculatedField; +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.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; @@ -56,7 +62,6 @@ import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTra import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.kv.KvEntry; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -65,6 +70,8 @@ import org.thingsboard.server.common.data.security.DeviceCredentialsType; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import static org.thingsboard.monitoring.service.BaseHealthChecker.TEST_CF_TELEMETRY_KEY; @@ -75,11 +82,16 @@ import static org.thingsboard.monitoring.service.BaseHealthChecker.TEST_TELEMETR @RequiredArgsConstructor public class MonitoringEntityService { + private static final String DASHBOARD_TITLE = "[Monitoring] Cloud monitoring"; + private static final String DASHBOARD_RESOURCE_PATH = "dashboard_cloud_monitoring.json"; + private final TbClient tbClient; @Value("${monitoring.calculated_fields.enabled:true}") private boolean calculatedFieldsMonitoringEnabled; + DashboardId dashboardId = null; + public void checkEntities() { RuleChain ruleChain = tbClient.getRuleChains(RuleChainType.CORE, new PageLink(10)).getData().stream() .filter(RuleChain::isRoot) @@ -94,27 +106,34 @@ public class MonitoringEntityService { int currentVersion = Integer.parseInt(attributes.getOrDefault("version", "0")); int newVersion = ruleChainDescriptor.get("version").asInt(); if (currentVersion == newVersion) { - log.info("Not updating rule chain, version is the same ({})", currentVersion); - return; + log.debug("Not updating rule chain, version is the same ({})", currentVersion); } else { log.info("Updating rule chain '{}' from version {} to {}", ruleChain.getName(), currentVersion, newVersion); + + String metadataJson = RegexUtils.replace(ruleChainDescriptor.get("metadata").toString(), + "\\$\\{MONITORING:(.+?)}", matchResult -> { + String key = matchResult.group(1); + String value = attributes.get(key); + if (value == null) { + throw new IllegalArgumentException("No attribute found for key " + key); + } + log.info("Using {}: {}", key, value); + return value; + }); + RuleChainMetaData metaData = JacksonUtil.fromString(metadataJson, RuleChainMetaData.class); + metaData.setRuleChainId(ruleChainId); + tbClient.saveRuleChainMetaData(metaData); + tbClient.saveEntityAttributesV2(ruleChainId, DataConstants.SERVER_SCOPE, JacksonUtil.newObjectNode() + .put("version", newVersion)); } - String metadataJson = RegexUtils.replace(ruleChainDescriptor.get("metadata").toString(), - "\\$\\{MONITORING:(.+?)}", matchResult -> { - String key = matchResult.group(1); - String value = attributes.get(key); - if (value == null) { - throw new IllegalArgumentException("No attribute found for key " + key); - } - log.info("Using {}: {}", key, value); - return value; - }); - RuleChainMetaData metaData = JacksonUtil.fromString(metadataJson, RuleChainMetaData.class); - metaData.setRuleChainId(ruleChainId); - tbClient.saveRuleChainMetaData(metaData); - tbClient.saveEntityAttributesV2(ruleChainId, DataConstants.SERVER_SCOPE, JacksonUtil.newObjectNode() - .put("version", newVersion)); + Asset asset = getOrCreateMonitoringAsset(); + Dashboard dashboard = getOrCreateMonitoringDashboard(); + + tbClient.assignAssetToPublicCustomer(asset.getId()); + tbClient.assignDashboardToPublicCustomer(dashboard.getId()); + + this.dashboardId = Optional.ofNullable(dashboard).map(Dashboard::getId).orElse(null); } public Asset getOrCreateMonitoringAsset() { @@ -246,4 +265,71 @@ public class MonitoringEntityService { tbClient.saveCalculatedField(calculatedField); } + public String getDashboardPublicLink() { + String link = ""; + try { + Optional infoOpt = tbClient.getDashboardInfoById(dashboardId); + if (infoOpt.isPresent()) { + String publicCustomerId = null; + Set customers = infoOpt.get().getAssignedCustomers(); + if (customers != null) { + publicCustomerId = customers.stream() + .filter(ShortCustomerInfo::isPublic) + .map(c -> c.getCustomerId().getId().toString()) + .findFirst().orElse(null); + } + if (publicCustomerId != null) { + link = buildPublicDashboardLink(dashboardId, publicCustomerId); + log.info("Public Monitoring dashboard link: {}", link); + } else { + log.warn("Dashboard is not assigned to public customer. Public link can't be generated."); + } + } + } catch (Exception e) { + log.error("Failed to get a public link to Monitoring dashboard ", e); + } + return link; + } + + private Dashboard getOrCreateMonitoringDashboard() { + Dashboard existing = findDashboardByTitle(DASHBOARD_TITLE).orElse(null); + if (existing != null) { + log.debug("Found Monitoring dashboard '{}' with id {}", existing.getTitle(), existing.getId()); + return existing; + } + + Dashboard dashboardFromResource = ResourceUtils.getResource(DASHBOARD_RESOURCE_PATH, Dashboard.class); + dashboardFromResource.setTitle(DASHBOARD_TITLE); + //Optional.ofNullable(existing).map(Dashboard::getId).ifPresent(dashboardFromResource::setId); + Dashboard saved = tbClient.saveDashboard(dashboardFromResource); + log.info("Created Monitoring dashboard '{}' with id {}", saved.getTitle(), saved.getId()); + return saved; + } + + private Optional findDashboardByTitle(String title) { + // Use text search first and then filter by exact title + PageData page = tbClient.getTenantDashboards(new PageLink(10, 0, title)); + return page.getData().stream() + .filter(info -> title.equals(info.getTitle())) + .findFirst() + .flatMap(info -> tbClient.getDashboardById(info.getId())); + } + + private String buildPublicDashboardLink(DashboardId dashboardId, String publicCustomerId) { + String base = getBaseUrl(); + return String.format("%s/dashboard/%s?publicId=%s", base, dashboardId.getId().toString(), publicCustomerId); + } + + private String getBaseUrl() { + // TbClient.baseURL contains the root url, without trailing slash + try { + var baseUrlField = tbClient.getClass().getSuperclass().getDeclaredField("baseURL"); + baseUrlField.setAccessible(true); + return (String) baseUrlField.get(tbClient); + } catch (Exception e) { + log.warn("Unable to access baseURL from RestClient. Falling back to http://localhost:8080"); + return "http://localhost:8080"; + } + } + } diff --git a/monitoring/src/main/resources/dashboard_cloud_monitoring.json b/monitoring/src/main/resources/dashboard_cloud_monitoring.json new file mode 100644 index 0000000000..8be9677c61 --- /dev/null +++ b/monitoring/src/main/resources/dashboard_cloud_monitoring.json @@ -0,0 +1,580 @@ +{ + "title": "Cloud - Monitoring", + "image": null, + "mobileHide": false, + "mobileOrder": null, + "configuration": { + "description": "", + "widgets": { + "7db7f580-2aac-d7ee-20f6-3d9315e7003f": { + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "6451461f-d748-9528-e3cd-64078c7fae05", + "filterId": null, + "dataKeys": [ + { + "name": "arrivalLatency", + "type": "timeseries", + "label": "Arrival latency", + "color": "#ffc107", + "settings": {}, + "_hash": 0.7424467450112592, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "processingTime", + "type": "timeseries", + "label": "Processing time", + "color": "#607d8b", + "settings": {}, + "_hash": 0.6065128737901203, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "mqttTransportWsUpdateLatency", + "type": "timeseries", + "label": "MQTT - overall", + "color": "#f44336", + "settings": {}, + "_hash": 0.19638031054010696, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "coapTransportWsUpdateLatency", + "type": "timeseries", + "label": "CoAP - overall", + "color": "#9c27b0", + "settings": {}, + "_hash": 0.8167481665043521, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "httpTransportWsUpdateLatency", + "type": "timeseries", + "label": "HTTP - overall", + "color": "#8bc34a", + "settings": {}, + "_hash": 0.45369210935861803, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "lwm2mTransportWsUpdateLatency", + "type": "timeseries", + "label": "LwM2M - overall", + "color": "#3f51b5", + "settings": {}, + "_hash": 0.3477627917073993, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "coapIntegrationWsUpdateLatency", + "type": "timeseries", + "label": "CoAP integration - overall", + "color": "#e91e63", + "settings": {}, + "_hash": 0.8332881819050183, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "wsSubscribeLatency", + "type": "timeseries", + "label": "WS subscribe latency", + "color": "#03a9f4", + "settings": {}, + "_hash": 0.7843180730206556, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "wsConnectLatency", + "type": "timeseries", + "label": "WS connect latency", + "color": "#4caf50", + "settings": { + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "excludeFromStacking": false, + "showLines": true, + "lineWidth": 1, + "fillLines": false, + "showPoints": false, + "tooltipValueFormatter": "", + "showSeparateAxis": false, + "axisTitle": "", + "axisMin": null, + "axisMax": null, + "axisPosition": "left", + "axisTickSize": null, + "axisTickDecimals": null, + "axisTicksFormatter": "", + "comparisonSettings": { + "showValuesForComparison": true, + "comparisonValuesLabel": "", + "color": "" + }, + "thresholds": [] + }, + "_hash": 0.23140687573220564, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "logInLatency", + "type": "timeseries", + "label": "Log in latency", + "color": "#2196f3", + "settings": { + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "excludeFromStacking": false, + "showLines": true, + "lineWidth": 1, + "fillLines": false, + "showPoints": false, + "tooltipValueFormatter": "", + "showSeparateAxis": false, + "axisTitle": "", + "axisMin": null, + "axisMax": null, + "axisPosition": "left", + "axisTickSize": null, + "axisTickDecimals": null, + "axisTicksFormatter": "", + "comparisonSettings": { + "showValuesForComparison": true, + "comparisonValuesLabel": "", + "color": "" + }, + "thresholds": [] + }, + "_hash": 0.6130919996800452, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + }, + "latestDataKeys": [] + } + ], + "timewindow": { + "realtime": { + "timewindowMs": 60000 + } + }, + "showTitle": true, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "8px", + "settings": { + "stack": false, + "fontSize": 10, + "fontColor": "#545454", + "showTooltip": true, + "tooltipIndividual": false, + "tooltipCumulative": false, + "hideZeros": false, + "grid": { + "verticalLines": true, + "horizontalLines": true, + "outlineWidth": 1, + "color": "#545454", + "backgroundColor": null, + "tickColor": "#DDDDDD" + }, + "xaxis": { + "title": null, + "showLabels": true, + "color": "#545454" + }, + "yaxis": { + "min": null, + "max": null, + "title": null, + "showLabels": true, + "color": "#545454", + "tickSize": null, + "tickDecimals": 0, + "ticksFormatter": "" + }, + "shadowSize": 4, + "smoothLines": false, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "xaxisSecond": { + "axisPosition": "top", + "title": null, + "showLabels": true + }, + "customLegendEnabled": false, + "dataKeysListForLabels": [], + "showLegend": true, + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": true, + "showAvg": true, + "showTotal": false, + "showLatest": true + } + }, + "title": "General latencies", + "dropShadow": false, + "enableFullscreen": true, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "useDashboardTimewindow": true, + "showTitleIcon": false, + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "", + "enableDataExport": true, + "displayTimewindow": true + }, + "row": 0, + "col": 0, + "id": "7db7f580-2aac-d7ee-20f6-3d9315e7003f", + "typeFullFqn": "system.charts.basic_timeseries" + }, + "e0a2df43-2f9a-efcf-80f7-bf51b0a174e1": { + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "6451461f-d748-9528-e3cd-64078c7fae05", + "filterId": null, + "dataKeys": [ + { + "name": "mqttTransportRequestLatency", + "type": "timeseries", + "label": "MQTT transport request latency", + "color": "#2196f3", + "settings": {}, + "_hash": 0.8576659620523571, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "httpTransportRequestLatency", + "type": "timeseries", + "label": "HTTP transport request latency", + "color": "#4caf50", + "settings": {}, + "_hash": 0.9749033105491403, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "coapTransportRequestLatency", + "type": "timeseries", + "label": "CoAP transport request latency", + "color": "#f44336", + "settings": {}, + "_hash": 0.6977575200148037, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "coapIntegrationRequestLatency", + "type": "timeseries", + "label": "CoAP integration request latency", + "color": "#9c27b0", + "settings": {}, + "_hash": 0.18839685494200875, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + }, + { + "name": "lwm2mTransportRequestLatency", + "type": "timeseries", + "label": "LwM2M transport request latency", + "color": "#ffc107", + "settings": { + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "excludeFromStacking": false, + "showLines": true, + "lineWidth": 1, + "fillLines": false, + "showPoints": false, + "tooltipValueFormatter": "", + "showSeparateAxis": false, + "axisTitle": "", + "axisMin": null, + "axisMax": null, + "axisPosition": "left", + "axisTickSize": null, + "axisTickDecimals": null, + "axisTicksFormatter": "", + "comparisonSettings": { + "showValuesForComparison": true, + "comparisonValuesLabel": "", + "color": "" + }, + "thresholds": [] + }, + "_hash": 0.85549409179514, + "aggregationType": null, + "units": "ms", + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + }, + "latestDataKeys": [] + } + ], + "timewindow": { + "realtime": { + "timewindowMs": 60000 + } + }, + "showTitle": true, + "backgroundColor": "#fff", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "8px", + "settings": { + "shadowSize": 4, + "fontColor": "#545454", + "fontSize": 10, + "xaxis": { + "showLabels": true, + "color": "#545454" + }, + "yaxis": { + "showLabels": true, + "color": "#545454" + }, + "grid": { + "color": "#545454", + "tickColor": "#DDDDDD", + "verticalLines": true, + "horizontalLines": true, + "outlineWidth": 1 + }, + "legend": { + "show": true, + "position": "nw", + "backgroundColor": "#f0f0f0", + "backgroundOpacity": 0.85, + "labelBoxBorderColor": "rgba(1, 1, 1, 0.45)" + }, + "decimals": 1, + "stack": false, + "tooltipIndividual": false, + "showLegend": true, + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": true, + "showAvg": true, + "showTotal": false, + "showLatest": true + } + }, + "title": "Transport latencies", + "dropShadow": false, + "enableFullscreen": true, + "titleStyle": { + "fontSize": "16px", + "fontWeight": 400 + }, + "useDashboardTimewindow": true, + "showTitleIcon": false, + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "noDataDisplayMessage": "", + "enableDataExport": true, + "displayTimewindow": true + }, + "row": 0, + "col": 0, + "id": "e0a2df43-2f9a-efcf-80f7-bf51b0a174e1", + "typeFullFqn": "system.charts.basic_timeseries" + } + }, + "states": { + "default": { + "name": "Cloud - Monitoring", + "root": true, + "layouts": { + "main": { + "widgets": { + "7db7f580-2aac-d7ee-20f6-3d9315e7003f": { + "sizeX": 12, + "sizeY": 11, + "row": 0, + "col": 0 + }, + "e0a2df43-2f9a-efcf-80f7-bf51b0a174e1": { + "sizeX": 12, + "sizeY": 11, + "row": 0, + "col": 12 + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "columns": 24, + "margin": 10, + "backgroundSizeMode": "100%", + "outerMargin": true, + "layoutType": "default" + } + } + } + } + }, + "entityAliases": { + "6451461f-d748-9528-e3cd-64078c7fae05": { + "id": "6451461f-d748-9528-e3cd-64078c7fae05", + "alias": "Monitoring stats asset", + "filter": { + "type": "entityName", + "resolveMultiple": true, + "entityType": "ASSET", + "entityNameFilter": "[Monitoring] Latencies" + } + } + }, + "filters": {}, + "timewindow": { + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 120000, + "timewindowMs": 18000000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 600000, + "timewindowMs": 43200000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "aggregation": { + "type": "AVG", + "limit": 2500 + }, + "timezone": null + }, + "settings": { + "stateControllerId": "entity", + "showTitle": false, + "showDashboardsSelect": true, + "showEntitiesSelect": true, + "showDashboardTimewindow": true, + "showDashboardExport": true, + "toolbarAlwaysOpen": true + } + }, + "name": "Cloud - Monitoring", + "resources": null +} \ No newline at end of file diff --git a/monitoring/src/main/resources/logback.xml b/monitoring/src/main/resources/logback.xml index df3a7224c6..43001ffd43 100644 --- a/monitoring/src/main/resources/logback.xml +++ b/monitoring/src/main/resources/logback.xml @@ -17,7 +17,7 @@ --> - + @@ -34,5 +34,4 @@ - diff --git a/msa/edqs/docker/Dockerfile b/msa/edqs/docker/Dockerfile index e9099c09c5..2ecd46fa45 100644 --- a/msa/edqs/docker/Dockerfile +++ b/msa/edqs/docker/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/openjdk17:bookworm-slim +FROM ${docker.base.image} COPY start-tb-edqs.sh ${pkg.name}.deb /tmp/ diff --git a/msa/monitoring/docker/Dockerfile b/msa/monitoring/docker/Dockerfile index a0b38bb3bd..d32f4d06d6 100644 --- a/msa/monitoring/docker/Dockerfile +++ b/msa/monitoring/docker/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/openjdk17:bookworm-slim +FROM ${docker.base.image} COPY start-tb-monitoring.sh ${pkg.name}.deb /tmp/ diff --git a/msa/pom.xml b/msa/pom.xml index 7fbc5d3c02..6ec4ffe1b9 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -32,6 +32,7 @@ ${basedir}/.. thingsboard + thingsboard/openjdk17:bookworm-slim true true 1.4.13 diff --git a/msa/tb-node/docker/Dockerfile b/msa/tb-node/docker/Dockerfile index 013a37ef9c..c084c9c15b 100644 --- a/msa/tb-node/docker/Dockerfile +++ b/msa/tb-node/docker/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/openjdk17:bookworm-slim +FROM ${docker.base.image} COPY logback.xml start-tb-node.sh ${pkg.name}.deb /tmp/ diff --git a/msa/transport/coap/docker/Dockerfile b/msa/transport/coap/docker/Dockerfile index 1d88541096..f5aa18898c 100644 --- a/msa/transport/coap/docker/Dockerfile +++ b/msa/transport/coap/docker/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/openjdk17:bookworm-slim +FROM ${docker.base.image} COPY start-tb-coap-transport.sh ${pkg.name}.deb /tmp/ diff --git a/msa/transport/http/docker/Dockerfile b/msa/transport/http/docker/Dockerfile index d7c8622ace..f224a776ad 100644 --- a/msa/transport/http/docker/Dockerfile +++ b/msa/transport/http/docker/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/openjdk17:bookworm-slim +FROM ${docker.base.image} COPY start-tb-http-transport.sh ${pkg.name}.deb /tmp/ diff --git a/msa/transport/lwm2m/docker/Dockerfile b/msa/transport/lwm2m/docker/Dockerfile index ec65d9a8a5..6b35776fce 100644 --- a/msa/transport/lwm2m/docker/Dockerfile +++ b/msa/transport/lwm2m/docker/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/openjdk17:bookworm-slim +FROM ${docker.base.image} COPY start-tb-lwm2m-transport.sh ${pkg.name}.deb /tmp/ diff --git a/msa/transport/mqtt/docker/Dockerfile b/msa/transport/mqtt/docker/Dockerfile index 1502b9e3c0..270d357e4a 100644 --- a/msa/transport/mqtt/docker/Dockerfile +++ b/msa/transport/mqtt/docker/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/openjdk17:bookworm-slim +FROM ${docker.base.image} COPY start-tb-mqtt-transport.sh ${pkg.name}.deb /tmp/ diff --git a/msa/transport/snmp/docker/Dockerfile b/msa/transport/snmp/docker/Dockerfile index 0ec79e5148..713ff11607 100644 --- a/msa/transport/snmp/docker/Dockerfile +++ b/msa/transport/snmp/docker/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/openjdk17:bookworm-slim +FROM ${docker.base.image} COPY start-tb-snmp-transport.sh ${pkg.name}.deb /tmp/ diff --git a/msa/vc-executor-docker/docker/Dockerfile b/msa/vc-executor-docker/docker/Dockerfile index 3c65098c42..ae03092daf 100644 --- a/msa/vc-executor-docker/docker/Dockerfile +++ b/msa/vc-executor-docker/docker/Dockerfile @@ -14,7 +14,7 @@ # limitations under the License. # -FROM thingsboard/openjdk17:bookworm-slim +FROM ${docker.base.image} ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update \ diff --git a/msa/vc-executor/src/main/resources/tb-vc-executor.yml b/msa/vc-executor/src/main/resources/tb-vc-executor.yml index 7d1166e512..ea89fb7d23 100644 --- a/msa/vc-executor/src/main/resources/tb-vc-executor.yml +++ b/msa/vc-executor/src/main/resources/tb-vc-executor.yml @@ -151,6 +151,8 @@ queue: print-interval-ms: "${TB_QUEUE_KAFKA_CONSUMER_STATS_MIN_PRINT_INTERVAL_MS:60000}" # Time to wait for the stats-loading requests to Kafka to finis kafka-response-timeout-ms: "${TB_QUEUE_KAFKA_CONSUMER_STATS_RESPONSE_TIMEOUT_MS:1000}" + # Topics cache TTL in milliseconds. 5 minutes by default + topics_cache_ttl_ms: "${TB_QUEUE_KAFKA_TOPICS_CACHE_TTL_MS:300000}" partitions: hash_function_name: "${TB_QUEUE_PARTITIONS_HASH_FUNCTION_NAME:murmur3_128}" # murmur3_32, murmur3_128 or sha256 core: diff --git a/packaging/java/build.gradle b/packaging/java/build.gradle index 499c34c412..bb0c8a94de 100644 --- a/packaging/java/build.gradle +++ b/packaging/java/build.gradle @@ -92,7 +92,11 @@ buildRpm { archiveVersion = projectVersion.replace('-', '') archiveFileName = "${pkgName}.rpm" - requires("(java-17 or java-17-headless or jre-17 or jre-17-headless)") // .or() notation does work in RPM plugin + // Support Java 17 (existing), plus Java 21 and Java 25 for RPM-based distros + // Keep using RPM boolean expression syntax since .or() chaining is for DEB only + requires("(java-17 or java-17-headless or jre-17 or jre-17-headless or " + + "java-21 or java-21-headless or jre-21 or jre-21-headless or " + + "java-25 or java-25-headless or jre-25 or jre-25-headless)") from("${buildDir}/conf") { include "${pkgName}.conf" @@ -132,6 +136,8 @@ buildDeb { archiveFileName = "${pkgName}.deb" requires("openjdk-17-jre").or("java17-runtime").or("oracle-java17-installer").or("openjdk-17-jre-headless") + .or("openjdk-21-jre").or("java21-runtime").or("oracle-java21-installer").or("openjdk-21-jre-headless") + .or("openjdk-25-jre").or("java25-runtime").or("oracle-java25-installer").or("openjdk-25-jre-headless") from("${buildDir}/conf") { include "${pkgName}.conf" 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 7686949980..83029c4f78 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 @@ -4499,7 +4499,7 @@ public class RestClient implements Closeable { public Optional getAiModel(AiModelId aiModelId) { try { ResponseEntity response = restTemplate.getForEntity( - baseURL + "/api/aiModel/{aiModelId}", AiModel.class, aiModelId.getId()); + baseURL + "/api/ai/model/{aiModelId}", AiModel.class, aiModelId.getId()); return Optional.ofNullable(response.getBody()); } catch (HttpClientErrorException exception) { if (exception.getStatusCode() == HttpStatus.NOT_FOUND) { @@ -4511,7 +4511,7 @@ public class RestClient implements Closeable { } public void deleteAiModel(AiModelId aiModelId) { - restTemplate.delete(baseURL + "/api/aiModel/{aiModelId}", aiModelId.getId()); + restTemplate.delete(baseURL + "/api/ai/model/{aiModelId}", aiModelId.getId()); } 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 ad9ce41ea3..c6e67bd65c 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 @@ -31,7 +31,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import java.util.EnumSet; import java.util.NoSuchElementException; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java index 83b777d153..17ab7e4022 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAbstractRelationActionNode.java @@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import java.util.EnumSet; import java.util.NoSuchElementException; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index a9ebb7ecb7..4908ca942a 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -54,7 +54,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.TbMsg; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.resource.TbResourceDataCache; import java.nio.charset.StandardCharsets; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java index c4e448e2ee..e5bdf734bd 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java @@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java index a786cdd536..cadaacda68 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java @@ -67,7 +67,7 @@ import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.ai.AiModelService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.resource.TbResourceDataCache; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeTest.java index 941dfb92d7..194d498d9a 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNodeTest.java @@ -46,7 +46,7 @@ import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.attributes.AttributesService; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.ConstraintValidator; import java.util.List; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java index 1141ff09d1..4af1234ded 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNodeTest.java @@ -49,7 +49,7 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; -import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.exception.DataValidationException; import org.thingsboard.server.dao.service.ConstraintValidator; import java.util.ArrayList; diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index 2f3942f847..d9a3dd6e72 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -185,14 +185,14 @@ coap: # CoAP DTLS bind port bind_port: "${COAP_DTLS_BIND_PORT:5684}" # CoAP DTLS connection ID length. RFC 9146, Connection Identifier for DTLS 1.2 - # Default: off + # Default: off.
# Control usage of DTLS connection ID length (CID). - # - 'off' to deactivate it. - # - 'on' to activate Connection ID support (same as CID 0 or more 0). - # - A positive value defines generated CID size in bytes. - # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). - # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used - # - A value that are > 4: MultiNodeConnectionIdGenerator is used + #
  • 'off' to deactivate it.
  • + #
  • 'on' to activate Connection ID support (same as CID 0 or more 0).
  • + #
  • A positive value defines generated CID size in bytes.
  • + #
  • A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic).
  • + #
  • A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
  • + #
  • A value that are > 4: MultiNodeConnectionIdGenerator is used
connection_id_length: "${COAP_DTLS_CONNECTION_ID_LENGTH:8}" # Specify the MTU (Maximum Transmission Unit). # Should be used if LAN MTU is not used, e.g. if IP tunnels are used or if the client uses a smaller value than the LAN MTU. @@ -332,6 +332,8 @@ queue: notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Housekeeper tasks topic housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" + # Topics cache TTL in milliseconds. 5 minutes by default + topics_cache_ttl_ms: "${TB_QUEUE_KAFKA_TOPICS_CACHE_TTL_MS:300000}" partitions: hash_function_name: "${TB_QUEUE_PARTITIONS_HASH_FUNCTION_NAME:murmur3_128}" # murmur3_32, murmur3_128 or sha256 transport_api: diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index 587894d5ce..7fe35a57d5 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -281,6 +281,8 @@ queue: notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Housekeeper tasks topic housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" + # Topics cache TTL in milliseconds. 5 minutes by default + topics_cache_ttl_ms: "${TB_QUEUE_KAFKA_TOPICS_CACHE_TTL_MS:300000}" partitions: hash_function_name: "${TB_QUEUE_PARTITIONS_HASH_FUNCTION_NAME:murmur3_128}" # murmur3_32, murmur3_128 or sha256 transport_api: diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 323f80b999..7e609d7a10 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -164,15 +164,15 @@ transport: dtls: # RFC7925_RETRANSMISSION_TIMEOUT_IN_MILLISECONDS = 9000 retransmission_timeout: "${LWM2M_DTLS_RETRANSMISSION_TIMEOUT_MS:9000}" - # CoAP DTLS connection ID length for LWM2M. RFC 9146, Connection Identifier for DTLS 1.2 - # Default: off + # LWM2M DTLS connection ID length for LWM2M. RFC 9146, Connection Identifier for DTLS 1.2 + # Default: off.
# Control usage of DTLS connection ID length (CID). - # - 'off' to deactivate it. - # - 'on' to activate Connection ID support (same as CID 0 or more 0). - # - A positive value defines generated CID size in bytes. - # - A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic). - # - A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used - # - A value that are > 4: MultiNodeConnectionIdGenerator is used + #
  • 'off' to deactivate it.
  • + #
  • 'on' to activate Connection ID support (same as CID 0 or more 0).
  • + #
  • A positive value defines generated CID size in bytes.
  • + #
  • A value of 0 means we accept using CID but will not generate one for foreign peer (enables support but not for incoming traffic).
  • + #
  • A value between 0 and <= 4: SingleNodeConnectionIdGenerator is used
  • + #
  • A value that are > 4: MultiNodeConnectionIdGenerator is used
connection_id_length: "${LWM2M_DTLS_CONNECTION_ID_LENGTH:8}" server: # LwM2M Server ID @@ -382,6 +382,8 @@ queue: notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Housekeeper tasks topic housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" + # Topics cache TTL in milliseconds. 5 minutes by default + topics_cache_ttl_ms: "${TB_QUEUE_KAFKA_TOPICS_CACHE_TTL_MS:300000}" partitions: hash_function_name: "${TB_QUEUE_PARTITIONS_HASH_FUNCTION_NAME:murmur3_128}" # murmur3_32, murmur3_128 or sha256 transport_api: diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index fae10cc892..86e5dc5a5b 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -315,6 +315,8 @@ queue: notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" # Kafka properties for Housekeeper tasks topic housekeeper: "${TB_QUEUE_KAFKA_HOUSEKEEPER_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:52428800;retention.bytes:1048576000;partitions:10;min.insync.replicas:1}" + # Topics cache TTL in milliseconds. 5 minutes by default + topics_cache_ttl_ms: "${TB_QUEUE_KAFKA_TOPICS_CACHE_TTL_MS:300000}" partitions: hash_function_name: "${TB_QUEUE_PARTITIONS_HASH_FUNCTION_NAME:murmur3_128}" # murmur3_32, murmur3_128 or sha256 transport_api: diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index 567654cce4..9dfb9f0b41 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -270,6 +270,8 @@ queue: print-interval-ms: "${TB_QUEUE_KAFKA_CONSUMER_STATS_MIN_PRINT_INTERVAL_MS:60000}" # Time to wait for the stats-loading requests to Kafka to finis kafka-response-timeout-ms: "${TB_QUEUE_KAFKA_CONSUMER_STATS_RESPONSE_TIMEOUT_MS:1000}" + # Topics cache TTL in milliseconds. 5 minutes by default + topics_cache_ttl_ms: "${TB_QUEUE_KAFKA_TOPICS_CACHE_TTL_MS:300000}" partitions: hash_function_name: "${TB_QUEUE_PARTITIONS_HASH_FUNCTION_NAME:murmur3_128}" # murmur3_32, murmur3_128 or sha256 transport_api: diff --git a/ui-ngx/src/app/core/services/menu.models.ts b/ui-ngx/src/app/core/services/menu.models.ts index 555717ffc0..f42943cd93 100644 --- a/ui-ngx/src/app/core/services/menu.models.ts +++ b/ui-ngx/src/app/core/services/menu.models.ts @@ -98,6 +98,7 @@ export enum MenuId { device_profiles = 'device_profiles', asset_profiles = 'asset_profiles', customers = 'customers', + calculated_fields = 'calculated_fields', rule_chains = 'rule_chains', edge_management = 'edge_management', edges = 'edges', @@ -626,6 +627,16 @@ export const menuSectionMap = new Map([ icon: 'supervisor_account' } ], + [ + MenuId.calculated_fields, + { + id: MenuId.calculated_fields, + name: 'entity.type-calculated-fields', + type: 'link', + path: '/calculatedFields', + icon: 'mdi:function-variant', + } + ], [ MenuId.rule_chains, { @@ -839,6 +850,7 @@ const defaultUserMenuMap = new Map([ ] }, {id: MenuId.customers}, + {id: MenuId.calculated_fields}, {id: MenuId.rule_chains}, { id: MenuId.edge_management, diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 2640845264..028f81a17d 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -421,7 +421,7 @@ export function mergeDeep(target: T, ...sources: T[]): T { function ignoreArrayMergeFunc(target: any, sources: any) { if (_.isArray(target)) { - return sources; + return deepClone(sources); } } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html index 7e706bfca9..869b707d98 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.html @@ -19,7 +19,9 @@

{{ 'alarm-rule.alarm-rule' | translate}}

+
- alarm-rule.no-clear-alarm-rule + alarm-rule.no-clear-alarm-rule
- + {{ 'alarm-rule.advanced-settings' | translate }} @@ -171,13 +173,14 @@
diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts index ac4d0f9ed6..a84aa52693 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-dialog.component.ts @@ -30,21 +30,22 @@ import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { EntityId } from '@shared/models/id/entity-id'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; import { COMMA, ENTER, SEMICOLON } from "@angular/cdk/keycodes"; -import { MatChipInputEvent } from "@angular/material/chips"; import { AlarmRule, AlarmRuleConditionType, + alarmRuleEntityTypeList, AlarmRuleExpressionType, AlarmRuleTestScriptFn } from "@shared/models/alarm-rule.models"; import { deepTrim } from "@core/utils"; -import { Observable } from "rxjs"; -import { switchMap } from "rxjs/operators"; +import { combineLatest, Observable } from "rxjs"; +import { debounceTime, startWith, switchMap } from "rxjs/operators"; import { EntityTypeSelectComponent } from "@shared/components/entity/entity-type-select.component"; import { EntityAutocompleteComponent } from "@shared/components/entity/entity-autocomplete.component"; import { EntityService } from "@core/http/entity.service"; import { RelationTypes } from "@shared/models/relation.models"; import { StringItemsOption } from "@shared/components/string-items-list.component"; +import { BaseData } from "@shared/models/base-data"; export interface AlarmRuleDialogData { value?: CalculatedField; @@ -92,7 +93,7 @@ export class AlarmRuleDialogComponent extends DialogComponent { this.updateRulesValidators(); }); - if (!this.entityName) { - this.fieldFormGroup.get('entityId.id').valueChanges.pipe( - takeUntilDestroyed(this.destroyRef) - ).subscribe((entityId) => { - if (entityId && (this.fieldFormGroup.get('entityId.entityType').value === EntityType.DEVICE_PROFILE || - this.fieldFormGroup.get('entityId.entityType').value === EntityType.ASSET_PROFILE)) { - this.entityService.getEntity(this.fieldFormGroup.get('entityId.entityType').value as EntityType, entityId, {ignoreLoading: true, ignoreErrors: true}).subscribe( - value => { - this.entityName = value.name; - } - ) + if (!this.data.entityId) { + combineLatest([ + this.fieldFormGroup.get('entityId.id')!.valueChanges.pipe(startWith(this.fieldFormGroup.get('entityId.id')!.value)), + this.fieldFormGroup.get('name')!.valueChanges.pipe(startWith(this.fieldFormGroup.get('name')!.value)) + ]).pipe( + debounceTime(50), + takeUntilDestroyed() + ).subscribe(([entityId, name]) => { + this.disabledArguments = !entityId || !name?.length; + const argsControl = this.fieldFormGroup.get('configuration.arguments')!; + if (this.disabledArguments) { + argsControl.disable({ emitEvent: false }); + } else { + argsControl.enable({ emitEvent: false }); } }); } @@ -174,12 +184,16 @@ export class AlarmRuleDialogComponent extends DialogComponent 0) { + this.isLoading = true; const alarmRule = { entityId: this.data.entityId, ...(this.data.value ?? {}), ...this.fromGroupValue}; alarmRule.configuration.type = CalculatedFieldType.ALARM; this.calculatedFieldsService.saveCalculatedField(alarmRule) .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(calculatedField => this.dialogRef.close(calculatedField)); + .subscribe({ + next: calculatedField => this.dialogRef.close(calculatedField), + error: () => this.isLoading = false + }); } else { this.fieldFormGroup.get('name').markAsTouched(); this.entityTypeSelect?.markAsTouched(); @@ -215,7 +229,7 @@ export class AlarmRuleDialogComponent extends DialogComponent): void { + this.entityName = entity.name; + } + } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html index 5deddffd87..c9629f00a3 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rule-filter-config.component.html @@ -66,7 +66,7 @@
-
alarm-rule.target-entity-type
+
entity.entity-type
{{ 'alarm-rule.any-type' | translate }} @@ -78,7 +78,7 @@
@if (alarmRuleFilterConfigForm.get('entityType').value) {
-
alarm-rule.target-entities
+
entity.entities
('AlarmRuleFilterConfigData'); @@ -73,14 +73,6 @@ export class AlarmRuleFilterConfigComponent implements OnInit, ControlValueAcces @Input() buttonMode = true; - @coerceBoolean() - @Input() - userMode = false; - - @coerceBoolean() - @Input() - propagatedFilter = true; - @Input() initialAlarmRuleFilterConfig: AlarmRuleFilterConfig = { name: [], @@ -100,7 +92,7 @@ export class AlarmRuleFilterConfigComponent implements OnInit, ControlValueAcces entityType = EntityType; - listEntityTypes = [EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE]; + listEntityTypes = alarmRuleEntityTypeList; entityTypeTranslations = entityTypeTranslations; private alarmRuleFilterConfig: AlarmRuleFilterConfig; @@ -124,7 +116,6 @@ export class AlarmRuleFilterConfigComponent implements OnInit, ControlValueAcces ngOnInit(): void { if (this.data) { this.panelMode = this.data.panelMode; - this.userMode = this.data.userMode; this.alarmRuleFilterConfig = this.data.alarmRuleFilterConfig; this.initialAlarmRuleFilterConfig = this.data.initialAlarmRuleFilterConfig; if (this.panelMode && !this.initialAlarmRuleFilterConfig) { @@ -248,7 +239,7 @@ export class AlarmRuleFilterConfigComponent implements OnInit, ControlValueAcces if (!isArraysEqualIgnoreUndefined(filter1.entities, filter2.entities)) { return false; } - return filter1.entityType !== filter2.entityType; + return filter1.entityType === filter2.entityType; } return false; }; @@ -287,7 +278,7 @@ export class AlarmRuleFilterConfigComponent implements OnInit, ControlValueAcces if (!filterTextParts.length) { this.buttonDisplayValue = this.translate.instant('alarm-rule.alarm-rule-filter-title'); } else { - this.buttonDisplayValue = this.translate.instant('alarm-rule.alarm-rule-filter-title') + `: ${filterTextParts.join(', ')}`; + this.buttonDisplayValue = this.translate.instant('alarm-rule.filter-title') + `: ${filterTextParts.join(', ')}`; } } } diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts index 0cb6069ee9..f1bd30d853 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table-config.ts @@ -60,7 +60,7 @@ import { UtilsService } from "@core/services/utils.service"; import { deepClone, getEntityDetailsPageURL, isObject } from "@core/utils"; import { AlarmRuleTableHeaderComponent } from "@home/components/alarm-rules/alarm-rule-table-header.component"; import { EventsDialogComponent, EventsDialogData } from '@home/dialogs/events-dialog.component'; -import { DebugEventType, Event as DebugEvent, EventType } from '@shared/models/event.models'; +import { DebugEventType, EventType } from '@shared/models/event.models'; import { ActionNotificationShow } from "@core/notification/notification.actions"; import { CalculatedFieldScriptTestDialogComponent, @@ -71,7 +71,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { readonly tenantId = getCurrentAuthUser(this.store).tenantId; additionalDebugActionConfig = { - title: this.translate.instant('calculated-fields.see-debug-events'), + title: this.translate.instant('action.see-debug-events'), action: (calculatedField: CalculatedField) => this.openDebugEventsDialog.call(this, calculatedField), }; @@ -95,6 +95,13 @@ export class AlarmRulesTableConfig extends EntityTableConfig { super(); if (this.pageMode) { this.headerComponent = AlarmRuleTableHeaderComponent; + + this.rowPointer = true; + + this.handleRowClick = ($event, model) => { + this.editCalculatedField($event, model); + return true; + }; } this.tableTitle = this.pageMode ? '' : this.translate.instant('alarm-rule.alarm-rules'); this.detailsPanelEnabled = false; @@ -133,16 +140,16 @@ export class AlarmRulesTableConfig extends EntityTableConfig { this.defaultSortOrder = {property: 'createdTime', direction: Direction.DESC}; this.columns.push(new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px')); - this.columns.push(new EntityTableColumn('name', 'alarm-rule.alarm-type', this.pageMode ? '30%' :'33%', + this.columns.push(new EntityTableColumn('name', 'alarm-rule.alarm-type', '33%', entity => this.utilsService.customTranslation(entity.name, entity.name))); if (this.pageMode) { - this.columns.push(new EntityTableColumn('entityType', 'alarm-rule.target-entity-type', '15%', + this.columns.push(new EntityTableColumn('entityType', 'entity.entity-type', '10%', entity => this.translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type))); - this.columns.push(new EntityLinkTableColumn('entityName', 'alarm-rule.target-entity', '30%', + this.columns.push(new EntityLinkTableColumn('entityName', 'entity.entity', '33%', entity => this.utilsService.customTranslation(entity['entityName'], entity['entityName']), entity => getEntityDetailsPageURL(entity.entityId?.id, entity.entityId?.entityType as EntityType), false)); } - this.columns.push(new EntityTableColumn('createRule', 'alarm-rule.severities', this.pageMode ? '15%' :'67%', + this.columns.push(new EntityTableColumn('createRule', 'alarm-rule.severities', this.pageMode ? '23%' : '67%', entity => Object.keys(entity.configuration.createRules).map((severity) => this.translate.instant(alarmSeverityTranslations.get(severity as AlarmSeverity))).join(', '), () => ({}), false)); this.columns.push(new EntityTableColumn('clearRule', 'alarm-rule.cleared', '90px', @@ -153,7 +160,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { name: this.translate.instant('alarm-rule.copy'), icon: 'content_copy', isEnabled: () => true, - onAction: ($event, entity) => this.copyCalculatedField(entity) + onAction: ($event, entity) => this.copyCalculatedField($event, entity) } ); this.cellActionDescriptors.push( @@ -161,13 +168,13 @@ export class AlarmRulesTableConfig extends EntityTableConfig { name: this.translate.instant('action.export'), icon: 'file_download', isEnabled: () => true, - onAction: (event$, entity) => this.exportAlarmRule(event$, entity), + onAction: ($event, entity) => this.exportAlarmRule($event, entity), }, { name: this.translate.instant('entity-view.events'), icon: 'mdi:clipboard-text-clock', isEnabled: () => true, - onAction: (_, entity) => this.openDebugEventsDialog(entity), + onAction: ($event, entity) => this.openDebugEventsDialog($event, entity), }, { name: '', @@ -181,26 +188,24 @@ export class AlarmRulesTableConfig extends EntityTableConfig { name: this.translate.instant('action.edit'), icon: 'edit', isEnabled: () => true, - onAction: (_, entity) => this.editCalculatedField(entity), + onAction: ($event, entity) => this.editCalculatedField($event, entity), } ); } fetchCalculatedFields(pageLink: PageLink): Observable> { return this.pageMode ? - this.calculatedFieldsService.getCalculatedFieldsFilter(pageLink, {type: CalculatedFieldType.ALARM, ...this.alarmRuleFilterConfig}) : + this.calculatedFieldsService.getCalculatedFieldsFilter(pageLink, {types: [CalculatedFieldType.ALARM], ...this.alarmRuleFilterConfig}) : this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink, CalculatedFieldType.ALARM); } onOpenDebugConfig($event: Event, calculatedField: CalculatedField): void { + $event?.stopPropagation(); const { debugSettings = {}, id } = calculatedField; const additionalActionConfig = { ...this.additionalDebugActionConfig, - action: () => this.openDebugEventsDialog(calculatedField) + action: () => this.openDebugEventsDialog($event, calculatedField) }; - if ($event) { - $event.stopPropagation(); - } const { viewContainerRef, renderer } = this.entityDebugSettingsService; if (!viewContainerRef || !renderer) { @@ -219,7 +224,8 @@ export class AlarmRulesTableConfig extends EntityTableConfig { }, $event.target as Element); } - private editCalculatedField(calculatedField: CalculatedField, isDirty = false): void { + private editCalculatedField($event: Event, calculatedField: CalculatedField, isDirty = false): void { + $event?.stopPropagation(); this.getCalculatedAlarmDialog(calculatedField, 'action.apply', isDirty) .subscribe((res) => { if (res) { @@ -228,7 +234,8 @@ export class AlarmRulesTableConfig extends EntityTableConfig { }); } - private copyCalculatedField(calculatedField: CalculatedField, isDirty = false): void { + private copyCalculatedField($event: Event, calculatedField: CalculatedField, isDirty = false): void { + $event?.stopPropagation(); const copyCalculatedAlarmRule = deepClone(calculatedField); if (this.pageMode) { copyCalculatedAlarmRule.entityId = null; @@ -243,13 +250,14 @@ export class AlarmRulesTableConfig extends EntityTableConfig { } private getCalculatedAlarmDialog(value?: CalculatedField, buttonTitle = 'action.add', isDirty = false): Observable { + const entityId = this.entityId || value?.entityId; return this.dialog.open(AlarmRuleDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { value, buttonTitle, - entityId: this.entityId, + entityId, tenantId: this.tenantId, entityName: this.entityName, ownerId: this.ownerId ?? {entityType: EntityType.TENANT, id: this.tenantId}, @@ -263,7 +271,8 @@ export class AlarmRulesTableConfig extends EntityTableConfig { .pipe(filter(Boolean)); } - private openDebugEventsDialog(calculatedField: CalculatedField): void { + private openDebugEventsDialog($event: Event, calculatedField: CalculatedField): void { + $event?.stopPropagation(); this.dialog.open(EventsDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], @@ -282,9 +291,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { } private exportAlarmRule($event: Event, calculatedField: CalculatedField): void { - if ($event) { - $event.stopPropagation(); - } + $event?.stopPropagation(); this.importExportService.exportCalculatedField(calculatedField.id.id); } @@ -361,7 +368,7 @@ export class AlarmRulesTableConfig extends EntityTableConfig { filter(Boolean), tap(expression => { if (openCalculatedFieldEdit) { - this.editCalculatedField({ + this.editCalculatedField(null, { entityId: this.entityId, ...calculatedField, configuration: {...calculatedField.configuration, expression} as any }, true) diff --git a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts index de0dc87c12..7f4c0b4ee7 100644 --- a/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm-rules/alarm-rules-table.component.ts @@ -88,7 +88,7 @@ export class AlarmRulesTableComponent { this.importExportService, this.entityDebugSettingsService, this.utilsService, - this.pageMode + this.pageMode, ); this.cd.markForCheck(); } diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts index e90071299c..093858a500 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-filter-config.component.ts @@ -107,7 +107,7 @@ export class AlarmFilterConfigComponent implements OnInit, OnDestroy, ControlVal alarmSeverityTranslationMap = alarmSeverityTranslations; - buttonDisplayValue = this.translate.instant('alarm.alarm-filter'); + buttonDisplayValue = this.translate.instant('alarm.alarm-filter-title'); alarmFilterConfigForm: UntypedFormGroup; diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.html b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.html index b97285ba07..8d53bdd99b 100644 --- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.html +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.html @@ -24,14 +24,14 @@ filter_list{{buttonDisplayValue}} -
-
-
+ +
+
audit-log.filter-types
diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss index 77bd5aea60..ca52472aa1 100644 --- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.scss @@ -33,3 +33,12 @@ } } } + +::ng-deep { + .tb-audit-log-config-component { + max-width: 100%; + width: 600px; + min-width: 100%; + flex: 1; + } +} diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts index e78b38965d..f810eb5df7 100644 --- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-filter.component.ts @@ -16,16 +16,27 @@ import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; import { TemplatePortal } from '@angular/cdk/portal'; -import { ChangeDetectorRef, Component, DestroyRef, ElementRef, forwardRef, Input, OnInit, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core'; +import { + ChangeDetectorRef, + Component, + DestroyRef, + ElementRef, + forwardRef, + Input, + OnInit, + TemplateRef, + ViewChild, + ViewContainerRef +} from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; import { isDefined, isDefinedAndNotNull } from '@app/core/utils'; import { StringItemsOption } from '@app/shared/components/string-items-list.component'; import { ActionType, actionTypeTranslations, - auditLogFilterEquals, AuditLogFilter, + auditLogFilterEquals, POSITION_MAP, } from '@app/shared/public-api'; import { TranslateService } from '@ngx-translate/core'; @@ -56,9 +67,9 @@ export class AuditLogFilterComponent implements OnInit, ControlValueAccessor { actionTypeTranslations = actionTypeTranslations; - buttonDisplayValue = this.translate.instant('audit-log.filter'); + buttonDisplayValue = this.translate.instant('audit-log.audit-log-filter-title'); - auditLogFilterForm: UntypedFormGroup; + auditLogFilterForm: FormGroup; auditLogOverlayRef: OverlayRef; @@ -68,7 +79,7 @@ export class AuditLogFilterComponent implements OnInit, ControlValueAccessor { private propagateChange = (_: any) => {}; - constructor(private fb: UntypedFormBuilder, + constructor(private fb: FormBuilder, private translate: TranslateService, private overlay: Overlay, private nativeElement: ElementRef, @@ -93,7 +104,7 @@ export class AuditLogFilterComponent implements OnInit, ControlValueAccessor { this.propagateChange = fn; } - registerOnTouched(fn: any): void { + registerOnTouched(_fn: any): void { } setDisabledState(isDisabled: boolean): void { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts index e9bf146bfd..e3f5ac9cc2 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-field.module.ts @@ -44,12 +44,20 @@ import { import { EntityAggregationComponentModule } from '@home/components/calculated-fields/components/entity-aggregation-configuration/entity-aggregation-component.module'; +import { + CalculatedFieldsHeaderComponent +} from '@home/components/calculated-fields/table-header/calculated-fields-header.component'; +import { + CalculatedFieldsFilterConfigComponent +} from '@home/components/calculated-fields/table-header/calculated-fields-filter-config.component'; @NgModule({ declarations: [ CalculatedFieldDialogComponent, CalculatedFieldScriptTestDialogComponent, CalculatedFieldTestArgumentsComponent, + CalculatedFieldsHeaderComponent, + CalculatedFieldsFilterConfigComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts index 60c2878802..e5c57aae2e 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table-config.ts @@ -16,6 +16,7 @@ import { DateEntityTableColumn, + EntityLinkTableColumn, EntityTableColumn, EntityTableConfig } from '@home/models/entity/entities-table-config.models'; @@ -39,8 +40,10 @@ import { ArgumentEntityType, ArgumentType, CalculatedField, + CalculatedFieldAlarmRule, CalculatedFieldEventArguments, CalculatedFieldScriptConfiguration, + CalculatedFieldsQuery, CalculatedFieldType, CalculatedFieldTypeTranslations, getCalculatedFieldArgumentsEditorCompleter, @@ -54,22 +57,27 @@ import { CalculatedFieldTestScriptDialogData } from './components/public-api'; import { ImportExportService } from '@shared/import-export/import-export.service'; -import { isObject } from '@core/utils'; +import { deepClone, getEntityDetailsPageURL, isObject } from '@core/utils'; import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { DatePipe } from '@angular/common'; import { UtilsService } from "@core/services/utils.service"; import { ActionNotificationShow } from "@core/notification/notification.actions"; import { CalculatedFieldEventBody, DebugEventType, Event as DebugEvent, EventType } from '@shared/models/event.models'; import { EventsDialogComponent, EventsDialogData } from '@home/dialogs/events-dialog.component'; +import { + CalculatedFieldsHeaderComponent +} from '@home/components/calculated-fields/table-header/calculated-fields-header.component'; export class CalculatedFieldsTableConfig extends EntityTableConfig { readonly tenantId = getCurrentAuthUser(this.store).tenantId; additionalDebugActionConfig = { - title: this.translate.instant('calculated-fields.see-debug-events'), - action: (calculatedField: CalculatedField) => this.openDebugEventsDialog.call(this, calculatedField), + title: this.translate.instant('action.see-debug-events'), + action: (calculatedField: CalculatedField) => this.openDebugEventsDialog.call(this, null, calculatedField), }; + calculatedFieldFilterConfig: CalculatedFieldsQuery; + constructor(private calculatedFieldsService: CalculatedFieldsService, private translate: TranslateService, private dialog: MatDialog, @@ -83,11 +91,20 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { + this.editCalculatedField($event, entity); + this.rowPointer = true; + return true; + }; + } + this.tableTitle = this.pageMode ? '' : this.translate.instant('entity.type-calculated-fields'); this.detailsPanelEnabled = false; - this.pageMode = false; this.entityType = EntityType.CALCULATED_FIELD; this.entityTranslations = entityTypeTranslations.get(EntityType.CALCULATED_FIELD); @@ -115,35 +132,36 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig('expression', 'calculated-fields.expression', '250px'); - expressionColumn.sortable = false; - expressionColumn.cellContentFunction = entity => { - const expressionLabel = this.getExpressionLabel(entity); - return expressionLabel?.length < 45 ? expressionLabel : `${expressionLabel.substring(0, 44)}…`; - } - expressionColumn.cellTooltipFunction = entity => { - const expressionLabel = this.getExpressionLabel(entity); - return expressionLabel?.length < 45 ? null : expressionLabel - }; - this.columns.push(new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px')); - this.columns.push(new EntityTableColumn('name', 'common.name', '33%', + this.columns.push(new EntityTableColumn('name', 'common.name', this.pageMode ? '33%' : '60%', entity => this.utilsService.customTranslation(entity.name, entity.name))); - this.columns.push(new EntityTableColumn('type', 'common.type', '170px', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type).name), () => ({whiteSpace: 'nowrap' }))); - this.columns.push(expressionColumn); + if (this.pageMode) { + this.columns.push(new EntityTableColumn('entityType', 'entity.entity-type', '10%', + entity => this.translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type))); + this.columns.push(new EntityLinkTableColumn('entityName', 'entity.entity', '33%', + entity => this.utilsService.customTranslation(entity['entityName'], entity['entityName']), + entity => getEntityDetailsPageURL(entity.entityId?.id, entity.entityId?.entityType as EntityType), false)); + } + this.columns.push(new EntityTableColumn('type', 'common.type', this.pageMode ? '23%' : '40%', entity => this.translate.instant(CalculatedFieldTypeTranslations.get(entity.type).name), () => ({whiteSpace: 'nowrap' }))); this.cellActionDescriptors.push( + { + name: this.translate.instant('action.copy'), + icon: 'content_copy', + isEnabled: () => true, + onAction: ($event, entity) => this.copyCalculatedField($event, entity), + }, { name: this.translate.instant('action.export'), icon: 'file_download', isEnabled: () => true, - onAction: (event$, entity) => this.exportCalculatedField(event$, entity), + onAction: ($event, entity) => this.exportCalculatedField($event, entity), }, { name: this.translate.instant('entity-view.events'), icon: 'mdi:clipboard-text-clock', isEnabled: () => true, - onAction: (_, entity) => this.openDebugEventsDialog(entity), + onAction: ($event, entity) => this.openDebugEventsDialog($event, entity), }, { name: '', @@ -157,34 +175,24 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig true, - onAction: (_, entity) => this.editCalculatedField(entity), + onAction: ($event, entity) => this.editCalculatedField($event, entity), } ); } - private getExpressionLabel(entity: CalculatedField): string { - if (entity.type === CalculatedFieldType.SCRIPT || - entity.type === CalculatedFieldType.PROPAGATION && entity.configuration.applyExpressionToResolvedArguments === true) { - return 'function calculate(ctx, ' + Object.keys(entity.configuration.arguments).join(', ') + ')'; - } else if (entity.type === CalculatedFieldType.SIMPLE) { - return entity.configuration.expression ?? ''; - } - return ''; - } - fetchCalculatedFields(pageLink: PageLink): Observable> { - return this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink); + return this.pageMode ? + this.calculatedFieldsService.getCalculatedFieldsFilter(pageLink, this.calculatedFieldFilterConfig): + this.calculatedFieldsService.getCalculatedFields(this.entityId, pageLink); } onOpenDebugConfig($event: Event, calculatedField: CalculatedField): void { + $event?.stopPropagation(); const { debugSettings = {}, id } = calculatedField; const additionalActionConfig = { ...this.additionalDebugActionConfig, - action: () => this.openDebugEventsDialog(calculatedField) + action: () => this.openDebugEventsDialog($event, calculatedField) }; - if ($event) { - $event.stopPropagation(); - } const { viewContainerRef, renderer } = this.entityDebugSettingsService; if (!viewContainerRef || !renderer) { @@ -202,7 +210,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { if (res) { @@ -212,13 +221,14 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { + const entityId = this.entityId || value?.entityId; return this.dialog.open(CalculatedFieldDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { value, buttonTitle, - entityId: this.entityId, + entityId, tenantId: this.tenantId, entityName: this.entityName, ownerId: this.ownerId, @@ -232,7 +242,8 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { return (calculatedField.type === CalculatedFieldType.SCRIPT || (calculatedField.type === CalculatedFieldType.PROPAGATION && @@ -264,12 +275,25 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { + if (res) { + this.updateData(); + } + }); + } + private importCalculatedField(): void { this.importExportService.openCalculatedFieldImportDialog() .pipe( @@ -355,7 +379,7 @@ export class CalculatedFieldsTableConfig extends EntityTableConfig { if (openCalculatedFieldEdit) { - this.editCalculatedField({ + this.editCalculatedField(null, { entityId: this.entityId, ...calculatedField, configuration: {...calculatedField.configuration, expression} as any }, true) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html index df433bc70e..47a3fdeaf4 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.html @@ -16,5 +16,5 @@ --> @if (calculatedFieldsTableConfig) { - + } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.scss index 3feb1e7429..0884f69200 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.scss @@ -14,9 +14,7 @@ * limitations under the License. */ :host ::ng-deep { - tb-entities-table { - .mat-drawer-container { - background-color: white; - } + .white-background { + --mat-sidenav-content-background-color: white; } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts index ee3d013907..2a0c29e31a 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/calculated-fields-table.component.ts @@ -36,6 +36,7 @@ import { ImportExportService } from '@shared/import-export/import-export.service import { EntityDebugSettingsService } from '@home/components/entity/debug/entity-debug-settings.service'; import { DatePipe } from '@angular/common'; import { UtilsService } from "@core/services/utils.service"; +import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'tb-calculated-fields-table', @@ -55,6 +56,8 @@ export class CalculatedFieldsTableComponent { calculatedFieldsTableConfig: CalculatedFieldsTableConfig; + pageMode: boolean = false; + constructor(private calculatedFieldsService: CalculatedFieldsService, private translate: TranslateService, private dialog: MatDialog, @@ -65,10 +68,12 @@ export class CalculatedFieldsTableComponent { private importExportService: ImportExportService, private entityDebugSettingsService: EntityDebugSettingsService, private utilsService: UtilsService, - private destroyRef: DestroyRef) { - + private destroyRef: DestroyRef, + private route: ActivatedRoute, + ) { + this.pageMode = !!this.route.snapshot.data.isPage; effect(() => { - if (this.active()) { + if (this.active() || this.pageMode) { this.calculatedFieldsTableConfig = new CalculatedFieldsTableConfig( this.calculatedFieldsService, this.translate, @@ -83,6 +88,7 @@ export class CalculatedFieldsTableComponent { this.importExportService, this.entityDebugSettingsService, this.utilsService, + this.pageMode, ); this.cd.markForCheck(); } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html index c6f2a5d479..99b4225db2 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.html @@ -111,6 +111,7 @@
+ class="tb-prompt flex flex-1 items-end justify-center text-base"> {{ 'calculated-fields.no-arguments' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts index 002a515164..9a8395a557 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component.ts @@ -157,6 +157,10 @@ export class CalculatedFieldArgumentsTableComponent implements ControlValueAcces return this.errorText ? { argumentsFormArray: false } : null; } + setDisabledState(isDisabled: boolean): void { + this.disable = isDisabled; + } + onDelete($event: Event, argument: CalculatedFieldArgumentValue): void { $event.stopPropagation(); const index = this.argumentsFormArray.controls.findIndex(control => isEqual(control.value, argument)); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html index 0feeda19d4..3d669b02c6 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html @@ -21,6 +21,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index ee7bafee83..4d2df6e416 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { Component, DestroyRef, Inject, ViewEncapsulation } from '@angular/core'; +import { Component, DestroyRef, Inject, ViewChild, ViewEncapsulation } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; @@ -24,13 +24,15 @@ import { DialogComponent } from '@shared/components/dialog.component'; import { CalculatedField, CalculatedFieldConfiguration, + calculatedFieldsEntityTypeList, CalculatedFieldTestScriptFn, CalculatedFieldType, + calculatedFieldTypes, CalculatedFieldTypeTranslations, OutputStrategyType } from '@shared/models/calculated-field.models'; import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; -import { EntityType } from '@shared/models/entity-type.models'; +import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; import { pairwise, switchMap } from 'rxjs/operators'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; @@ -38,6 +40,9 @@ import { Observable } from 'rxjs'; import { EntityId } from '@shared/models/id/entity-id'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; import { deepTrim, isDefined } from '@core/utils'; +import { EntityTypeSelectComponent } from '@shared/components/entity/entity-type-select.component'; +import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component'; +import { BaseData } from '@shared/models/base-data'; export interface CalculatedFieldDialogData { value?: CalculatedField; @@ -61,6 +66,10 @@ export class CalculatedFieldDialogComponent extends DialogComponent(EntityType.DEVICE_PROFILE, Validators.required), + id: [null as null | string, Validators.required], + }), type: [CalculatedFieldType.SIMPLE], debugSettings: [], configuration: this.fb.control({} as CalculatedFieldConfiguration), @@ -71,11 +80,20 @@ export class CalculatedFieldDialogComponent extends DialogComponent this.data.additionalDebugActionConfig.action({ id: this.data.value.id, ...this.fromGroupValue }), } : null; + entityName = this.data.entityName; + + disabledConfiguration = false; + isLoading = false; + readonly EntityType = EntityType; + readonly calculatedFieldsEntityTypeList = calculatedFieldsEntityTypeList; readonly CalculatedFieldType = CalculatedFieldType; - readonly fieldTypes = Object.values(CalculatedFieldType).filter(type => type !== CalculatedFieldType.ALARM) as CalculatedFieldType[]; + readonly fieldTypes = calculatedFieldTypes; readonly CalculatedFieldTypeTranslations = CalculatedFieldTypeTranslations; + @ViewChild('entityTypeSelect') entityTypeSelect: EntityTypeSelectComponent; + @ViewChild('entityAutocompleteComponent') entityAutocompleteComponent: EntityAutocompleteComponent; + constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: CalculatedFieldDialogData, @@ -84,9 +102,25 @@ export class CalculatedFieldDialogComponent extends DialogComponent { + this.disabledConfiguration = !entityId; + if (this.disabledConfiguration) { + this.fieldFormGroup.get('configuration').disable({emitEvent: false}); + } else { + this.fieldFormGroup.get('configuration').enable({emitEvent: false}); + } + }); + } } get fromGroupValue(): CalculatedField { @@ -99,9 +133,17 @@ export class CalculatedFieldDialogComponent extends DialogComponent this.dialogRef.close(calculatedField)); + .subscribe({ + next: calculatedField => this.dialogRef.close(calculatedField), + error: () => this.isLoading = false + }); + } else { + this.fieldFormGroup.get('name').markAsTouched(); + this.entityTypeSelect?.markAsTouched(); + this.entityAutocompleteComponent?.markAsTouched(); } } @@ -120,28 +162,27 @@ export class CalculatedFieldDialogComponent extends DialogComponent): void { + this.entityName = entity.name; + } + + get entityId(): EntityId { + return this.data.entityId || this.fieldFormGroup.get('entityId').value; + } + private applyDialogData(): void { - const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {}; + const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, entityId = this.data.entityId, ...value } = this.data.value ?? {}; if (configuration.type !== CalculatedFieldType.ALARM) { if (isDefined(configuration?.output) && !configuration?.output?.strategy) { configuration.output.strategy = {type: OutputStrategyType.RULE_CHAIN}; } } - this.fieldFormGroup.patchValue({ configuration, type, debugSettings, ...value }, {emitEvent: false}); + this.fieldFormGroup.patchValue({ configuration, type, debugSettings, entityId, ...value }, {emitEvent: false}); setTimeout(() => this.fieldFormGroup.get('type').updateValueAndValidity({onlySelf: true})); - } - - private observeIsLoading(): void { - this.isLoading$.pipe(takeUntilDestroyed()).subscribe(loading => { - if (loading) { - this.fieldFormGroup.disable({emitEvent: false}); - } else { - this.fieldFormGroup.enable({emitEvent: false}); - if (this.data.isDirty) { - this.fieldFormGroup.markAsDirty(); - } - } - }); + if (!this.data.entityId) { + this.fieldFormGroup.get('configuration').disable({emitEvent: false}); + this.disabledConfiguration = true; + } } private observeType(): void { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html index 20db5c732d..fa0ba32f22 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.html @@ -101,6 +101,7 @@ #button (click)="manageZone($event, button, geofenceZone)" [matTooltip]="'action.edit' | translate" + [disabled]="disable" matTooltipPosition="above"> delete @@ -128,7 +130,7 @@ }
+ class="tb-prompt flex flex-1 items-end justify-center text-base" [class.required]="!disable"> {{ 'calculated-fields.no-zone-configured' | translate }}
@@ -137,7 +139,7 @@ color="primary" #button (click)="manageZone($event, button)" - [disabled]="maxArgumentsPerCF > 0 && zoneGroupsFormArray.length >= maxArgumentsPerCF"> + [disabled]="maxArgumentsPerCF > 0 && zoneGroupsFormArray.length >= maxArgumentsPerCF || disable"> {{ 'calculated-fields.add-zone-group' | translate }} @if (maxArgumentsPerCF && zoneGroupsFormArray.length >= maxArgumentsPerCF) { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts index b3e5edb80a..228d08fd90 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/calculated-field-geofencing-zone-groups-table.component.ts @@ -90,6 +90,7 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro entityNameMap = new Map(); sortOrder = { direction: 'asc', property: '' }; dataSource = new CalculatedFieldZoneDatasource(); + disable = false; readonly GeofencingReportStrategyTranslations = GeofencingReportStrategyTranslations; readonly entityTypeTranslations = entityTypeTranslations; @@ -135,6 +136,10 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro return this.errorText ? { zonesFormArray: false } : null; } + setDisabledState(isDisabled: boolean): void { + this.disable = isDisabled; + } + onDelete($event: Event, zone: CalculatedFieldGeofencingValue): void { $event.stopPropagation(); const index = this.zoneGroupsFormArray.controls.findIndex(control => isEqual(control.value, zone)); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html index a35dc97d8a..4693d3afb4 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.html @@ -97,6 +97,7 @@ mat-icon-button #button (click)="manageMetrics($event, button, metric)" + [disabled]="disable" [matTooltip]="'action.edit' | translate" matTooltipPosition="above"> edit @@ -104,6 +105,7 @@
+ class="tb-prompt flex flex-1 items-end justify-center text-base" [class.required]="!disable"> {{ 'calculated-fields.metrics.no-metrics-configured' | translate }}
@@ -125,7 +127,7 @@ color="primary" #button (click)="manageMetrics($event, button)" - [disabled]="maxArgumentsPerCF > 0 && metricsFormArray.length >= maxArgumentsPerCF"> + [disabled]="maxArgumentsPerCF > 0 && metricsFormArray.length >= maxArgumentsPerCF || disable"> {{ 'calculated-fields.metrics.add-metric' | translate }} @if (maxArgumentsPerCF && metricsFormArray.length >= maxArgumentsPerCF) { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts index 78021529d6..21b21d07ac 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/metrics/calculated-field-metrics-table.component.ts @@ -89,6 +89,7 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu metricsFormArray = this.fb.array([]); sortOrder = { direction: 'asc' as SortDirection, property: '' }; dataSource = new CalculatedFieldMetricsDatasource(); + disable = false; displayColumns = ['name', 'function', 'filter', 'valueSource', 'actions']; @@ -141,6 +142,10 @@ export class CalculatedFieldMetricsTableComponent implements OnInit, ControlValu return this.errorText ? { metricsFormArray: false } : null; } + setDisabledState(isDisabled: boolean): void { + this.disable = isDisabled; + } + onDelete($event: Event, metric: CalculatedFieldAggMetricValue): void { $event.stopPropagation(); const index = this.metricsFormArray.controls.findIndex(control => isEqual(control.value, metric)); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html index 3a47c00b1c..5bf7788e10 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.html @@ -78,7 +78,7 @@
-
+
{{ 'calculated-fields.output-strategy.strategy' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts index 239a4921e4..f1e8646a6c 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts @@ -78,6 +78,8 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val @Input({required: true}) entityId: EntityId; + disabled = false; + readonly outputTypes = Object.values(OutputType) as OutputType[]; readonly OutputType = OutputType; readonly AttributeScope = AttributeScope; @@ -120,6 +122,10 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val this.updatedStrategy(); }); + this.outputForm.get('strategy.saveTimeSeries').valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(value => this.updateTimeSeriesTtl(value)); + merge( this.outputForm.get('strategy.type').valueChanges, this.outputForm.get('strategy.useCustomTtl').valueChanges @@ -171,6 +177,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val registerOnTouched(_: any): void { } setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; if (isDisabled) { this.outputForm.disable({emitEvent: false}); } else { @@ -178,6 +185,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val this.updatedFormWithMode(); this.toggleScopeByOutputType(this.outputForm.get('type').value); this.updatedStrategy(); + this.updateTimeSeriesTtl(this.outputForm.get('strategy.saveTimeSeries').value); } } @@ -242,4 +250,13 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val } } } + + private updateTimeSeriesTtl(value: boolean) { + if (value) { + this.outputForm.get('strategy.useCustomTtl').enable({emitEvent: true}); + } else { + this.outputForm.get('strategy.useCustomTtl').disable({emitEvent: false}); + this.outputForm.get('strategy.ttl').disable({emitEvent: false}); + } + } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html index bd92d25f92..8abbfb2056 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.html @@ -55,6 +55,7 @@ [tenantId]="tenantId" [entityName]="entityName" [ownerId]="ownerId" + [watchKeyChange]="true" [isScript]="this.propagateConfiguration.get('applyExpressionToResolvedArguments').value"/>
@@ -77,10 +78,10 @@
@@ -89,8 +90,8 @@
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts index bce244c7ba..1c493148b3 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts @@ -88,6 +88,8 @@ export class PropagationConfigurationComponent implements ControlValueAccessor, output: this.fb.control(defaultCalculatedFieldOutput), }); + disabled = false; + readonly ScriptLanguage = ScriptLanguage; readonly CalculatedFieldType = CalculatedFieldType; readonly OutputType = OutputType; @@ -142,6 +144,7 @@ export class PropagationConfigurationComponent implements ControlValueAccessor, registerOnTouched(_: any): void { } setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; if (isDisabled) { this.propagateConfiguration.disable({emitEvent: false}); } else { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html index 178f8b47d6..e6a37043d5 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.html @@ -72,7 +72,7 @@ matTooltip="{{ 'calculated-fields.test-script-function' | translate }}" matTooltipPosition="above" class="tb-mat-32" - [disabled]="simpleConfiguration.get('arguments').invalid" + [disabled]="simpleConfiguration.get('arguments').invalid || disabled" (click)="onTestScript()"> bug_report @@ -81,7 +81,7 @@
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts index 8dc6f01726..e6ff1d1bad 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts @@ -104,6 +104,8 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid map(argumentsObj => getCalculatedFieldArgumentsHighlights(argumentsObj)) ); + disabled = false; + private propagateChange: (config: SimpeConfiguration) => void = () => { }; constructor(private fb: FormBuilder) { @@ -127,7 +129,7 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid for (const propName of Object.keys(changes)) { const change = changes[propName]; if (change.currentValue !== change.previousValue) { - if (propName === 'isScript') { + if (propName === 'isScript' && !this.disabled) { this.updatedFormWithScript(); if (!change.firstChange) { this.simpleConfiguration.updateValueAndValidity(); @@ -163,6 +165,7 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid } setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; if (isDisabled) { this.simpleConfiguration.disable({emitEvent: false}); } else { diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.html new file mode 100644 index 0000000000..5f4efea5b8 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.html @@ -0,0 +1,96 @@ + + + + + + + + + + + + +
+ + + + +
+ +
+ +
+
+
calculated-fields.calculated-field-types
+ + +
+
+
entity.entity-type
+ + + {{ 'alarm-rule.any-type' | translate }} + + {{ entityTypeTranslations.get(type)?.type | translate }} + + + +
+ @if (cfFilterForm.get('entityType').value) { +
+
entity.entities
+ + +
+ } +
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.scss new file mode 100644 index 0000000000..ad27508835 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.scss @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2025 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. + */ + +:host { + display: flex; + max-width: 100%; + .mdc-button { + max-width: 100%; + } +} + +:host ::ng-deep { + .mdc-button { + .mat-icon { + min-width: 24px; + } + .mdc-button__label { + overflow: hidden; + text-overflow: ellipsis; + } + } +} + +::ng-deep { + .tb-calculated-fields-config-component { + max-width: 100%; + width: 600px; + min-width: 100%; + flex: 1; + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.ts new file mode 100644 index 0000000000..83ea05af4c --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-filter-config.component.ts @@ -0,0 +1,293 @@ +/// +/// Copyright © 2016-2025 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 { + Component, + DestroyRef, + ElementRef, + forwardRef, + Inject, + InjectionToken, + Input, + OnInit, + Optional, + TemplateRef, + ViewChild, + ViewContainerRef +} from '@angular/core'; +import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { coerceBoolean } from '@shared/decorators/coercion'; +import { Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { TemplatePortal } from '@angular/cdk/portal'; +import { deepClone, isArraysEqualIgnoreUndefined, isDefinedAndNotNull, isEmpty, isUndefinedOrNull } from '@core/utils'; +import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { fromEvent, Subscription } from 'rxjs'; +import { POSITION_MAP } from '@shared/models/overlay.models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { + calculatedFieldsEntityTypeList, + CalculatedFieldsQuery, + calculatedFieldTypes, + CalculatedFieldTypeTranslations +} from '@shared/models/calculated-field.models'; +import { StringItemsOption } from '@shared/components/string-items-list.component'; +import { TranslateService } from '@ngx-translate/core'; + +export const CALCULATED_FIELDS_CONFIG_DATA = new InjectionToken('CalculatedFieldsFilterConfigData'); + +export interface CalculatedFieldsFilterConfigData { + panelMode: boolean; + userMode: boolean; + filterConfig: CalculatedFieldsQuery; + initialFilterConfig?: CalculatedFieldsQuery; +} + +@Component({ + selector: 'tb-calculated-fields-filter-config', + templateUrl: './calculated-fields-filter-config.component.html', + styleUrls: ['./calculated-fields-filter-config.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CalculatedFieldsFilterConfigComponent), + multi: true + } + ] +}) +export class CalculatedFieldsFilterConfigComponent implements OnInit, ControlValueAccessor { + + @ViewChild('calculatedFieldsFilterPanel') + calculatedFieldsFilterPanel: TemplateRef; + + @Input() + disabled: boolean; + + @coerceBoolean() + @Input() + buttonMode = true; + + @Input() + initialCfFilterConfig: CalculatedFieldsQuery = { + types: [], + entityType: null, + entities: [] + }; + + panelMode = false; + + buttonDisplayValue = this.translate.instant('calculated-fields.calculated-field-filter-title'); + + cfFilterForm: FormGroup; + + panelResult: CalculatedFieldsQuery = null; + + entityType = EntityType; + + listEntityTypes = calculatedFieldsEntityTypeList; + entityTypeTranslations = entityTypeTranslations; + + readonly types: StringItemsOption[] = calculatedFieldTypes.map(item => ({ + name: this.translate.instant(CalculatedFieldTypeTranslations.get(item).name), + value: item + })); + + private cfFilterOverlayRef: OverlayRef; + private cfFilterConfig: CalculatedFieldsQuery; + private resizeWindows: Subscription; + + private propagateChange = (_: any) => {}; + + constructor(@Optional() @Inject(CALCULATED_FIELDS_CONFIG_DATA) + private data: CalculatedFieldsFilterConfigData | undefined, + @Optional() private overlayRef: OverlayRef, + private fb: FormBuilder, + private overlay: Overlay, + private nativeElement: ElementRef, + private viewContainerRef: ViewContainerRef, + private destroyRef: DestroyRef, + private translate: TranslateService) { + } + + ngOnInit(): void { + if (this.data) { + this.panelMode = this.data.panelMode; + this.cfFilterConfig = this.data.filterConfig; + this.initialCfFilterConfig = this.data.initialFilterConfig; + if (this.panelMode && !this.initialCfFilterConfig) { + this.initialCfFilterConfig = deepClone(this.cfFilterConfig); + } + } + this.cfFilterForm = this.fb.group({ + types: [null, []], + entityType: [null, []], + entities: [null, []] + }); + this.cfFilterForm.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + () => { + if (!this.buttonMode) { + this.cfConfigUpdated(this.cfFilterForm.value); + } + } + ); + if (this.panelMode) { + this.updateCfConfigForm(this.cfFilterConfig); + } + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (this.disabled) { + this.cfFilterForm.disable({emitEvent: false}); + } else { + this.cfFilterForm.enable({emitEvent: false}); + } + } + + writeValue(cfFilterConfig?: CalculatedFieldsQuery): void { + this.cfFilterConfig = cfFilterConfig; + if (!this.initialCfFilterConfig && cfFilterConfig) { + this.initialCfFilterConfig = deepClone(cfFilterConfig); + } + this.updateButtonDisplayValue(); + this.updateCfConfigForm(cfFilterConfig); + } + + toggleCfFilterPanel($event: Event) { + if ($event) { + $event.stopPropagation(); + } + const config = new OverlayConfig({ + panelClass: 'tb-filter-panel', + backdropClass: 'cdk-overlay-transparent-backdrop', + hasBackdrop: true, + maxHeight: '80vh', + height: 'min-content', + minWidth: '' + }); + config.hasBackdrop = true; + config.positionStrategy = this.overlay.position() + .flexibleConnectedTo(this.nativeElement) + .withPositions([POSITION_MAP.bottomLeft]); + + this.cfFilterOverlayRef = this.overlay.create(config); + this.cfFilterOverlayRef.backdropClick().subscribe(() => { + this.cfFilterOverlayRef.dispose(); + }); + this.cfFilterOverlayRef.attach(new TemplatePortal(this.calculatedFieldsFilterPanel, + this.viewContainerRef)); + this.resizeWindows = fromEvent(window, 'resize').subscribe(() => { + this.cfFilterOverlayRef.updatePosition(); + }); + } + + cancel() { + this.updateCfConfigForm(this.cfFilterConfig); + if (this.overlayRef) { + this.overlayRef.dispose(); + } else { + this.resizeWindows.unsubscribe(); + this.cfFilterOverlayRef.dispose(); + } + } + + update() { + this.cfConfigUpdated(this.cfFilterForm.value); + this.cfFilterForm.markAsPristine(); + if (this.panelMode) { + this.panelResult = this.cfFilterConfig; + } + if (this.overlayRef) { + this.overlayRef.dispose(); + } else { + this.resizeWindows.unsubscribe(); + this.cfFilterOverlayRef.dispose(); + } + } + + reset() { + const cfFilterConfig = this.cfFilterFromFormValue(this.cfFilterForm.value); + if (!this.cfFilterConfigEquals(cfFilterConfig, this.initialCfFilterConfig)) { + this.updateCfConfigForm(this.initialCfFilterConfig); + this.cfFilterForm.markAsDirty(); + } + } + + private cfFilterConfigEquals = (filter1?: CalculatedFieldsQuery, filter2?: CalculatedFieldsQuery): boolean => { + if (filter1 === filter2) { + return true; + } + if ((isUndefinedOrNull(filter1) || isEmpty(filter1)) && (isUndefinedOrNull(filter2) || isEmpty(filter2))) { + return true; + } else if (isDefinedAndNotNull(filter1) && isDefinedAndNotNull(filter2)) { + if (!isArraysEqualIgnoreUndefined(filter1.types, filter2.types)) { + return false; + } + if (!isArraysEqualIgnoreUndefined(filter1.entities, filter2.entities)) { + return false; + } + return filter1.entityType === filter2.entityType; + } + return false; + }; + + private updateCfConfigForm(cfFilterConfig?: CalculatedFieldsQuery) { + this.cfFilterForm.patchValue({ + types: cfFilterConfig?.types ?? [], + entityType: cfFilterConfig?.entityType ?? null, + entities: cfFilterConfig?.entities ?? [], + }, {emitEvent: false}); + } + + private cfConfigUpdated(formValue: any) { + this.cfFilterConfig = this.cfFilterFromFormValue(formValue); + this.updateButtonDisplayValue(); + this.propagateChange(this.cfFilterConfig); + } + + private updateButtonDisplayValue() { + if (this.buttonMode) { + const filterTextParts: string[] = []; + if (this.cfFilterConfig?.types?.length) { + filterTextParts.push(this.cfFilterConfig.types.map((type) => this.translate.instant(CalculatedFieldTypeTranslations.get(type).name)).join(', ')); + } + if (this.cfFilterConfig?.entityType) { + filterTextParts.push(this.translate.instant( entityTypeTranslations.get(this.cfFilterConfig.entityType).type)); + } + if (!filterTextParts.length) { + this.buttonDisplayValue = this.translate.instant('calculated-fields.calculated-field-filter-title'); + } else { + this.buttonDisplayValue = this.translate.instant('calculated-fields.filter-title') + `: ${filterTextParts.join(', ')}`; + } + } + } + + private cfFilterFromFormValue(formValue: any): CalculatedFieldsQuery { + return { + types: formValue?.types ?? [], + entityType: formValue?.entityType ?? null, + entities: formValue?.entities ?? [], + }; + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.html new file mode 100644 index 0000000000..77a40a5283 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.html @@ -0,0 +1,20 @@ + + + diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.scss new file mode 100644 index 0000000000..ee0899b5ca --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.scss @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2025 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. + */ + +:host { + padding-right: 8px; + overflow: hidden; + max-width: 100%; +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.ts new file mode 100644 index 0000000000..f0824347ab --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/table-header/calculated-fields-header.component.ts @@ -0,0 +1,43 @@ +/// +/// Copyright © 2016-2025 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 { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { EntityTableHeaderComponent } from '@home/components/entity/entity-table-header.component'; +import { CalculatedField, CalculatedFieldsQuery } from "@shared/models/calculated-field.models"; +import { CalculatedFieldsTableConfig } from '@home/components/calculated-fields/calculated-fields-table-config'; + +@Component({ + selector: 'tb-calculated-fields-table-header', + templateUrl: './calculated-fields-header.component.html', + styleUrls: ['./calculated-fields-header.component.scss'] +}) +export class CalculatedFieldsHeaderComponent extends EntityTableHeaderComponent { + + get calculatedFieldsTableConfig(): CalculatedFieldsTableConfig { + return this.entitiesTableConfig as CalculatedFieldsTableConfig; + } + + constructor(protected store: Store) { + super(store); + } + + calculatedFieldsFilterChanged(calculatedFieldFilterConfig: CalculatedFieldsQuery) { + this.calculatedFieldsTableConfig.calculatedFieldFilterConfig = calculatedFieldFilterConfig; + this.calculatedFieldsTableConfig.getTable().resetSortAndFilter(true, true); + } +} diff --git a/ui-ngx/src/app/modules/home/components/device/device-info-filter.component.html b/ui-ngx/src/app/modules/home/components/device/device-info-filter.component.html index c7d97aae23..55b99d8a8d 100644 --- a/ui-ngx/src/app/modules/home/components/device/device-info-filter.component.html +++ b/ui-ngx/src/app/modules/home/components/device/device-info-filter.component.html @@ -33,6 +33,13 @@
+ +