diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index b0830317a7..2ffe2002f9 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -205,8 +205,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { saveRpcRequestToEdgeQueue(request, rpcRequest.getRequestId()).get(); sent = true; } catch (InterruptedException | ExecutionException e) { - String errMsg = String.format("[%s][%s][%s] Failed to save rpc request to edge queue %s", tenantId, deviceId, edgeId.getId(), request); - log.error(errMsg, e); + log.error("[{}][{}][{}] Failed to save rpc request to edge queue {}", tenantId, deviceId, edgeId.getId(), request, e); } } else if (isSendNewRpcAvailable()) { sent = rpcSubscriptions.size() > 0; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java index e50d99f95a..1bb30d6960 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java @@ -240,8 +240,7 @@ public class DefaultEdgeNotificationService implements EdgeNotificationService { } private void callBackFailure(TransportProtos.EdgeNotificationMsgProto edgeNotificationMsg, TbCallback callback, Throwable throwable) { - String errMsg = String.format("Can't push to edge updates, edgeNotificationMsg [%s]", edgeNotificationMsg); - log.error(errMsg, throwable); + log.error("Can't push to edge updates, edgeNotificationMsg [{}]", edgeNotificationMsg, throwable); callback.onFailure(throwable); } 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 9864d89048..2342997c65 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 @@ -86,6 +86,8 @@ public final class EdgeGrpcSession implements Closeable { private static final ReentrantLock downlinkMsgLock = new ReentrantLock(); + private static final int MAX_DOWNLINK_ATTEMPTS = 10; // max number of attemps to send downlink message if edge connected + private static final String QUEUE_START_TS_ATTR_KEY = "queueStartTs"; private final UUID sessionId; @@ -156,12 +158,13 @@ public final class EdgeGrpcSession implements Closeable { @Override public void onError(Throwable t) { - log.error("Failed to deliver message from client!", t); + log.error("[{}] Stream was terminated due to error:", sessionId, t); closeSession(); } @Override public void onCompleted() { + log.info("[{}] Stream was closed and completed successfully!", sessionId); closeSession(); } @@ -184,6 +187,10 @@ public final class EdgeGrpcSession implements Closeable { public void startSyncProcess(TenantId tenantId, EdgeId edgeId, boolean fullSync) { log.trace("[{}][{}] Staring edge sync process", tenantId, edgeId); syncCompleted = false; + if (sessionState.getSendDownlinkMsgsFuture() != null && sessionState.getSendDownlinkMsgsFuture().isDone()) { + String errorMsg = String.format("[%s][%s] Sync process started. General processing interrupted!", tenantId, edgeId); + sessionState.getSendDownlinkMsgsFuture().setException(new RuntimeException(errorMsg)); + } doSync(new EdgeSyncCursor(ctx, edge, fullSync)); } @@ -252,9 +259,9 @@ public final class EdgeGrpcSession implements Closeable { try { if (msg.getSuccess()) { sessionState.getPendingMsgsMap().remove(msg.getDownlinkMsgId()); - log.debug("[{}] Msg has been processed successfully! {}", edge.getRoutingKey(), msg); + log.debug("[{}] Msg has been processed successfully!Msd Id: [{}], Msg: {}", edge.getRoutingKey(), msg.getDownlinkMsgId(), msg); } else { - log.error("[{}] Msg processing failed! Error msg: {}", edge.getRoutingKey(), msg.getErrorMsg()); + log.error("[{}] Msg processing failed! Msd Id: [{}], Error msg: {}", edge.getRoutingKey(), msg.getDownlinkMsgId(), msg.getErrorMsg()); } if (sessionState.getPendingMsgsMap().isEmpty()) { log.debug("[{}] Pending msgs map is empty. Stopping current iteration", edge.getRoutingKey()); @@ -392,17 +399,17 @@ public final class EdgeGrpcSession implements Closeable { sessionState.setSendDownlinkMsgsFuture(SettableFuture.create()); sessionState.getPendingMsgsMap().clear(); downlinkMsgsPack.forEach(msg -> sessionState.getPendingMsgsMap().put(msg.getDownlinkMsgId(), msg)); - scheduleDownlinkMsgsPackSend(true); + scheduleDownlinkMsgsPackSend(1); return sessionState.getSendDownlinkMsgsFuture(); } - private void scheduleDownlinkMsgsPackSend(boolean firstRun) { + private void scheduleDownlinkMsgsPackSend(int attempt) { Runnable sendDownlinkMsgsTask = () -> { try { if (isConnected() && sessionState.getPendingMsgsMap().values().size() > 0) { List copy = new ArrayList<>(sessionState.getPendingMsgsMap().values()); - if (!firstRun) { - log.warn("[{}] Failed to deliver the batch: {}", this.sessionId, copy); + if (attempt > 1) { + log.warn("[{}] Failed to deliver the batch: {}, attempt: {}", this.sessionId, copy, attempt); } log.trace("[{}] [{}] downlink msg(s) are going to be send.", this.sessionId, copy.size()); for (DownlinkMsg downlinkMsg : copy) { @@ -410,7 +417,13 @@ public final class EdgeGrpcSession implements Closeable { .setDownlinkMsg(downlinkMsg) .build()); } - scheduleDownlinkMsgsPackSend(false); + if (attempt < MAX_DOWNLINK_ATTEMPTS) { + scheduleDownlinkMsgsPackSend(attempt + 1); + } else { + log.warn("[{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}", + this.sessionId, MAX_DOWNLINK_ATTEMPTS, copy); + sessionState.getSendDownlinkMsgsFuture().set(null); + } } else { sessionState.getSendDownlinkMsgsFuture().set(null); } @@ -419,7 +432,7 @@ public final class EdgeGrpcSession implements Closeable { } }; - if (firstRun) { + if (attempt == 1) { sendDownlinkExecutorService.submit(sendDownlinkMsgsTask); } else { sessionState.setScheduledSendDownlinkTask( @@ -548,7 +561,7 @@ public final class EdgeGrpcSession implements Closeable { try { if (uplinkMsg.getEntityDataCount() > 0) { for (EntityDataProto entityData : uplinkMsg.getEntityDataList()) { - result.addAll(ctx.getTelemetryProcessor().processTelemetryFromEdge(edge.getTenantId(), edge.getCustomerId(), entityData)); + result.addAll(ctx.getTelemetryProcessor().processTelemetryFromEdge(edge.getTenantId(), entityData)); } } if (uplinkMsg.getDeviceUpdateMsgCount() > 0) { @@ -612,8 +625,7 @@ public final class EdgeGrpcSession implements Closeable { } } } catch (Exception e) { - String errMsg = String.format("[%s] Can't process uplink msg [%s]", this.sessionId, uplinkMsg); - log.error(errMsg, e); + log.error("[{}] Can't process uplink msg [{}]", this.sessionId, uplinkMsg, e); return Futures.immediateFailedFuture(e); } return Futures.allAsList(result); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityDataMsgConstructor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityDataMsgConstructor.java index 855890022a..e2d3735910 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityDataMsgConstructor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityDataMsgConstructor.java @@ -15,13 +15,16 @@ */ package org.thingsboard.server.service.edge.rpc.constructor; -import com.google.gson.reflect.TypeToken; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.reflect.TypeToken; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.transport.adaptor.JsonConverter; @@ -62,7 +65,7 @@ public class EntityDataMsgConstructor { JsonObject data = entityData.getAsJsonObject(); TransportProtos.PostAttributeMsg attributesUpdatedMsg = JsonConverter.convertToAttributesProto(data.getAsJsonObject("kv")); builder.setAttributesUpdatedMsg(attributesUpdatedMsg); - builder.setPostAttributeScope(data.getAsJsonPrimitive("scope").getAsString()); + builder.setPostAttributeScope(getScopeOfDefault(data)); } catch (Exception e) { log.warn("[{}] Can't convert to AttributesUpdatedMsg proto, entityData [{}]", entityId, entityData, e); } @@ -72,7 +75,7 @@ public class EntityDataMsgConstructor { JsonObject data = entityData.getAsJsonObject(); TransportProtos.PostAttributeMsg postAttributesMsg = JsonConverter.convertToAttributesProto(data.getAsJsonObject("kv")); builder.setPostAttributesMsg(postAttributesMsg); - builder.setPostAttributeScope(data.getAsJsonPrimitive("scope").getAsString()); + builder.setPostAttributeScope(getScopeOfDefault(data)); } catch (Exception e) { log.warn("[{}] Can't convert to PostAttributesMsg, entityData [{}]", entityId, entityData, e); } @@ -94,4 +97,13 @@ public class EntityDataMsgConstructor { return builder.build(); } + private String getScopeOfDefault(JsonObject data) { + JsonPrimitive scope = data.getAsJsonPrimitive("scope"); + String result = DataConstants.SERVER_SCOPE; + if (scope != null && StringUtils.isNotBlank(scope.getAsString())) { + result = scope.getAsString(); + } + return result; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index a5b315176a..3b39520606 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -24,15 +24,22 @@ import org.springframework.context.annotation.Lazy; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EdgeUtils; +import org.thingsboard.server.common.data.EntityType; 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.id.AssetId; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DashboardId; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; @@ -267,17 +274,21 @@ public abstract class BaseEdgeProcessor { do { tenantsIds = tenantService.findTenantsIds(pageLink); for (TenantId tenantId1 : tenantsIds.getData()) { - futures.addAll(processActionForAllEdgesByTenantId(tenantId1, type, actionType, entityId)); + futures.addAll(processActionForAllEdgesByTenantId(tenantId1, type, actionType, entityId, null)); } pageLink = pageLink.nextPageLink(); } while (tenantsIds.hasNext()); } else { - futures = processActionForAllEdgesByTenantId(tenantId, type, actionType, entityId); + futures = processActionForAllEdgesByTenantId(tenantId, type, actionType, entityId, null); } return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService); } - private List> processActionForAllEdgesByTenantId(TenantId tenantId, EdgeEventType type, EdgeEventActionType actionType, EntityId entityId) { + protected List> processActionForAllEdgesByTenantId(TenantId tenantId, + EdgeEventType type, + EdgeEventActionType actionType, + EntityId entityId, + JsonNode body) { PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE); PageData pageData; List> futures = new ArrayList<>(); @@ -285,7 +296,7 @@ public abstract class BaseEdgeProcessor { pageData = edgeService.findEdgesByTenantId(tenantId, pageLink); if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) { for (Edge edge : pageData.getData()) { - futures.add(saveEdgeEvent(tenantId, edge.getId(), type, actionType, entityId, null)); + futures.add(saveEdgeEvent(tenantId, edge.getId(), type, actionType, entityId, body)); } if (pageData.hasNext()) { pageLink = pageLink.nextPageLink(); @@ -425,4 +436,30 @@ public abstract class BaseEdgeProcessor { return Futures.immediateFuture(null); } } + + protected EntityId constructEntityId(String entityTypeStr, long entityIdMSB, long entityIdLSB) { + EntityType entityType = EntityType.valueOf(entityTypeStr); + switch (entityType) { + case DEVICE: + return new DeviceId(new UUID(entityIdMSB, entityIdLSB)); + case ASSET: + return new AssetId(new UUID(entityIdMSB, entityIdLSB)); + case ENTITY_VIEW: + return new EntityViewId(new UUID(entityIdMSB, entityIdLSB)); + case DASHBOARD: + return new DashboardId(new UUID(entityIdMSB, entityIdLSB)); + case TENANT: + return TenantId.fromUUID(new UUID(entityIdMSB, entityIdLSB)); + case CUSTOMER: + return new CustomerId(new UUID(entityIdMSB, entityIdLSB)); + case USER: + return new UserId(new UUID(entityIdMSB, entityIdLSB)); + case EDGE: + return new EdgeId(new UUID(entityIdMSB, entityIdLSB)); + default: + log.warn("Unsupported entity type [{}] during construct of entity id. entityIdMSB [{}], entityIdLSB [{}]", + entityTypeStr, entityIdMSB, entityIdLSB); + return null; + } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java index c6c4ef757d..e09e036953 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java @@ -100,8 +100,7 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor { try { newDevice = createDevice(tenantId, edge, deviceUpdateMsg, newDeviceName); } catch (DataValidationException e) { - String errMsg = String.format("[%s] Device update msg can't be processed due to data validation [%s]", tenantId, deviceUpdateMsg); - log.error(errMsg, e); + log.error("[{}] Device update msg can't be processed due to data validation [{}]", tenantId, deviceUpdateMsg, e); return Futures.immediateFuture(null); } ObjectNode body = JacksonUtil.OBJECT_MAPPER.createObjectNode(); @@ -116,8 +115,7 @@ public class DeviceEdgeProcessor extends BaseEdgeProcessor { try { device = createDevice(tenantId, edge, deviceUpdateMsg, deviceUpdateMsg.getName()); } catch (DataValidationException e) { - String errMsg = String.format("[%s] Device update msg can't be processed due to data validation [%s]", tenantId, deviceUpdateMsg); - log.error(errMsg, e); + log.error("[{}] Device update msg can't be processed due to data validation [{}]", tenantId, deviceUpdateMsg, e); return Futures.immediateFuture(null); } return saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, EdgeEventActionType.CREDENTIALS_REQUEST, device.getId(), null); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java index 98a0476178..722b33b426 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java @@ -17,7 +17,6 @@ package org.thingsboard.server.service.edge.rpc.processor; import com.fasterxml.jackson.core.JsonProcessingException; import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import com.google.gson.Gson; @@ -36,8 +35,8 @@ import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.asset.Asset; -import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.asset.AssetProfile; +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.AssetId; @@ -49,7 +48,6 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.kv.AttributeKey; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.msg.TbMsg; @@ -59,7 +57,7 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.session.SessionMsgType; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.common.transport.util.JsonUtils; -import org.thingsboard.server.controller.BaseController; +import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.gen.edge.v1.AttributeDeleteMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EntityDataProto; @@ -76,7 +74,6 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.UUID; @Component @Slf4j @@ -92,19 +89,21 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor { tbCoreMsgProducer = producerProvider.getTbCoreMsgProducer(); } - public List> processTelemetryFromEdge(TenantId tenantId, CustomerId customerId, EntityDataProto entityData) { - log.trace("[{}] onTelemetryUpdate [{}]", tenantId, entityData); + public List> processTelemetryFromEdge(TenantId tenantId, EntityDataProto entityData) { + log.trace("[{}] processTelemetryFromEdge [{}]", tenantId, entityData); List> result = new ArrayList<>(); - EntityId entityId = constructEntityId(entityData); + EntityId entityId = constructEntityId(entityData.getEntityType(), entityData.getEntityIdMSB(), entityData.getEntityIdLSB()); if ((entityData.hasPostAttributesMsg() || entityData.hasPostTelemetryMsg() || entityData.hasAttributesUpdatedMsg()) && entityId != null) { - TbMsgMetaData metaData = constructBaseMsgMetadata(tenantId, entityId); + Pair pair = getBaseMsgMetadataAndCustomerId(tenantId, entityId); + TbMsgMetaData metaData = pair.getKey(); + CustomerId customerId = pair.getValue(); metaData.putValue(DataConstants.MSG_SOURCE_KEY, DataConstants.EDGE_MSG_SOURCE); if (entityData.hasPostAttributesMsg()) { result.add(processPostAttributes(tenantId, customerId, entityId, entityData.getPostAttributesMsg(), metaData)); } if (entityData.hasAttributesUpdatedMsg()) { metaData.putValue("scope", entityData.getPostAttributeScope()); - result.add(processAttributesUpdate(tenantId, entityId, entityData.getAttributesUpdatedMsg(), metaData)); + result.add(processAttributesUpdate(tenantId, customerId, entityId, entityData.getAttributesUpdatedMsg(), metaData)); } if (entityData.hasPostTelemetryMsg()) { result.add(processPostTelemetry(tenantId, customerId, entityId, entityData.getPostTelemetryMsg(), metaData)); @@ -112,12 +111,16 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor { if (EntityType.DEVICE.equals(entityId.getEntityType())) { DeviceId deviceId = new DeviceId(entityId.getId()); + long currentTs = System.currentTimeMillis(); + TransportProtos.DeviceActivityProto deviceActivityMsg = TransportProtos.DeviceActivityProto.newBuilder() .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) - .setLastActivityTime(System.currentTimeMillis()).build(); + .setLastActivityTime(currentTs).build(); + + log.trace("[{}][{}] device activity time is going to be updated, ts {}", tenantId, deviceId, currentTs); TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, deviceId); tbCoreMsgProducer.send(tpi, new TbProtoQueueMsg<>(deviceId.getId(), @@ -130,12 +133,14 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor { return result; } - private TbMsgMetaData constructBaseMsgMetadata(TenantId tenantId, EntityId entityId) { + private Pair getBaseMsgMetadataAndCustomerId(TenantId tenantId, EntityId entityId) { TbMsgMetaData metaData = new TbMsgMetaData(); + CustomerId customerId = null; switch (entityId.getEntityType()) { case DEVICE: Device device = deviceService.findDeviceById(tenantId, new DeviceId(entityId.getId())); if (device != null) { + customerId = device.getCustomerId(); metaData.putValue("deviceName", device.getName()); metaData.putValue("deviceType", device.getType()); } @@ -143,6 +148,7 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor { case ASSET: Asset asset = assetService.findAssetById(tenantId, new AssetId(entityId.getId())); if (asset != null) { + customerId = asset.getCustomerId(); metaData.putValue("assetName", asset.getName()); metaData.putValue("assetType", asset.getType()); } @@ -150,38 +156,24 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor { case ENTITY_VIEW: EntityView entityView = entityViewService.findEntityViewById(tenantId, new EntityViewId(entityId.getId())); if (entityView != null) { + customerId = entityView.getCustomerId(); metaData.putValue("entityViewName", entityView.getName()); metaData.putValue("entityViewType", entityView.getType()); } break; + case EDGE: + Edge edge = edgeService.findEdgeById(tenantId, new EdgeId(entityId.getId())); + if (edge != null) { + customerId = edge.getCustomerId(); + metaData.putValue("edgeName", edge.getName()); + metaData.putValue("edgeType", edge.getType()); + } + break; default: log.debug("Using empty metadata for entityId [{}]", entityId); break; } - return metaData; - } - - private Pair getDefaultQueueNameAndRuleChainId(TenantId tenantId, EntityId entityId) { - RuleChainId ruleChainId = null; - String queueName = null; - if (EntityType.DEVICE.equals(entityId.getEntityType())) { - DeviceProfile deviceProfile = deviceProfileCache.get(tenantId, new DeviceId(entityId.getId())); - if (deviceProfile == null) { - log.warn("[{}] Device profile is null!", entityId); - } else { - ruleChainId = deviceProfile.getDefaultRuleChainId(); - queueName = deviceProfile.getDefaultQueueName(); - } - } else if (EntityType.ASSET.equals(entityId.getEntityType())) { - AssetProfile assetProfile = assetProfileCache.get(tenantId, new AssetId(entityId.getId())); - if (assetProfile == null) { - log.warn("[{}] Asset profile is null!", entityId); - } else { - ruleChainId = assetProfile.getDefaultRuleChainId(); - queueName = assetProfile.getDefaultQueueName(); - } - } - return new ImmutablePair<>(queueName, ruleChainId); + return new ImmutablePair<>(metaData, customerId != null ? customerId : new CustomerId(ModelConstants.NULL_UUID)); } private ListenableFuture processPostTelemetry(TenantId tenantId, CustomerId customerId, EntityId entityId, TransportProtos.PostTelemetryMsg msg, TbMsgMetaData metaData) { @@ -207,6 +199,29 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor { return futureToSet; } + private Pair getDefaultQueueNameAndRuleChainId(TenantId tenantId, EntityId entityId) { + RuleChainId ruleChainId = null; + String queueName = null; + if (EntityType.DEVICE.equals(entityId.getEntityType())) { + DeviceProfile deviceProfile = deviceProfileCache.get(tenantId, new DeviceId(entityId.getId())); + if (deviceProfile == null) { + log.warn("[{}] Device profile is null!", entityId); + } else { + ruleChainId = deviceProfile.getDefaultRuleChainId(); + queueName = deviceProfile.getDefaultQueueName(); + } + } else if (EntityType.ASSET.equals(entityId.getEntityType())) { + AssetProfile assetProfile = assetProfileCache.get(tenantId, new AssetId(entityId.getId())); + if (assetProfile == null) { + log.warn("[{}] Asset profile is null!", entityId); + } else { + ruleChainId = assetProfile.getDefaultRuleChainId(); + queueName = assetProfile.getDefaultQueueName(); + } + } + return new ImmutablePair<>(queueName, ruleChainId); + } + private ListenableFuture processPostAttributes(TenantId tenantId, CustomerId customerId, EntityId entityId, TransportProtos.PostAttributeMsg msg, TbMsgMetaData metaData) { SettableFuture futureToSet = SettableFuture.create(); JsonObject json = JsonUtils.getJsonObject(msg.getKvList()); @@ -228,6 +243,7 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor { } private ListenableFuture processAttributesUpdate(TenantId tenantId, + CustomerId customerId, EntityId entityId, TransportProtos.PostAttributeMsg msg, TbMsgMetaData metaData) { @@ -238,26 +254,34 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor { tsSubService.saveAndNotify(tenantId, entityId, scope, attributes, new FutureCallback() { @Override public void onSuccess(@Nullable Void tmp) { - logAttributesUpdated(tenantId, entityId, scope, attributes, null); - futureToSet.set(null); + var defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId); + TbMsg tbMsg = TbMsg.newMsg(defaultQueueAndRuleChain.getKey(), DataConstants.ATTRIBUTES_UPDATED, entityId, + customerId, metaData, gson.toJson(json), defaultQueueAndRuleChain.getValue(), null); + tbClusterService.pushMsgToRuleEngine(tenantId, tbMsg.getOriginator(), tbMsg, new TbQueueCallback() { + @Override + public void onSuccess(TbQueueMsgMetadata metadata) { + futureToSet.set(null); + } + + @Override + public void onFailure(Throwable t) { + log.error("Can't process attributes update [{}]", msg, t); + futureToSet.setException(t); + } + }); } @Override public void onFailure(Throwable t) { log.error("Can't process attributes update [{}]", msg, t); - logAttributesUpdated(tenantId, entityId, scope, attributes, t); futureToSet.setException(t); } }); return futureToSet; } - private void logAttributesUpdated(TenantId tenantId, EntityId entityId, String scope, List attributes, Throwable e) { - notificationEntityService.logEntityAction(tenantId, entityId, ActionType.ATTRIBUTES_UPDATED, null, - BaseController.toException(e), scope, attributes); - } - - private ListenableFuture processAttributeDeleteMsg(TenantId tenantId, EntityId entityId, AttributeDeleteMsg attributeDeleteMsg, String entityType) { + private ListenableFuture processAttributeDeleteMsg(TenantId tenantId, EntityId entityId, AttributeDeleteMsg attributeDeleteMsg, + String entityType) { SettableFuture futureToSet = SettableFuture.create(); String scope = attributeDeleteMsg.getScope(); List attributeNames = attributeDeleteMsg.getAttributeNamesList(); @@ -284,29 +308,6 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor { return futureToSet; } - private EntityId constructEntityId(EntityDataProto entityData) { - EntityType entityType = EntityType.valueOf(entityData.getEntityType()); - switch (entityType) { - case DEVICE: - return new DeviceId(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); - case ASSET: - return new AssetId(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); - case ENTITY_VIEW: - return new EntityViewId(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); - case DASHBOARD: - return new DashboardId(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); - case TENANT: - return TenantId.fromUUID(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); - case CUSTOMER: - return new CustomerId(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); - case USER: - return new UserId(new UUID(entityData.getEntityIdMSB(), entityData.getEntityIdLSB())); - default: - log.warn("Unsupported entity type [{}] during construct of entity id. EntityDataProto [{}]", entityData.getEntityType(), entityData); - return null; - } - } - public DownlinkMsg convertTelemetryEventToDownlink(EdgeEvent edgeEvent) throws JsonProcessingException { EntityId entityId; switch (edgeEvent.getType()) { 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 531f5b0458..02406da08a 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -178,14 +178,14 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { @After public void afterTest() throws Exception { - try { - edgeImitator.disconnect(); - } catch (Exception ignored) {} - loginSysAdmin(); doDelete("/api/tenant/" + savedTenant.getUuidId()) .andExpect(status().isOk()); + + try { + edgeImitator.disconnect(); + } catch (Exception ignored) {} } private void installation() { diff --git a/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java index 3bd9de31b6..392a6814d4 100644 --- a/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java @@ -621,5 +621,7 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest { Assert.assertEquals(JacksonUtil.OBJECT_MAPPER.createObjectNode().put(attrKey, attrValue), JacksonUtil.fromBytes(onUpdateCallback.getPayloadBytes())); + + client.disconnect(); } } diff --git a/application/src/test/java/org/thingsboard/server/edge/BaseTelemetryEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/BaseTelemetryEdgeTest.java index b33e354e89..25280de666 100644 --- a/application/src/test/java/org/thingsboard/server/edge/BaseTelemetryEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/BaseTelemetryEdgeTest.java @@ -18,13 +18,13 @@ package org.thingsboard.server.edge; import com.fasterxml.jackson.databind.JsonNode; import com.google.protobuf.AbstractMessage; import org.junit.Assert; -import org.junit.Ignore; import org.junit.Test; import org.thingsboard.server.common.data.Device; 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.gen.edge.v1.AttributeDeleteMsg; +import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.EntityDataProto; import org.thingsboard.server.gen.transport.TransportProtos; @@ -36,12 +36,12 @@ abstract public class BaseTelemetryEdgeTest extends AbstractEdgeTest { public void testTimeseriesWithFailures() throws Exception { int numberOfTimeseriesToSend = 1000; + Device device = findDeviceByName("Edge Device 1"); + edgeImitator.setRandomFailuresOnTimeseriesDownlink(true); // imitator will generate failure in 5% of cases edgeImitator.setFailureProbability(5.0); - edgeImitator.expectMessageAmount(numberOfTimeseriesToSend); - Device device = findDeviceByName("Edge Device 1"); for (int idx = 1; idx <= numberOfTimeseriesToSend; idx++) { String timeseriesData = "{\"data\":{\"idx\":" + idx + "},\"ts\":" + System.currentTimeMillis() + "}"; JsonNode timeseriesEntityData = mapper.readTree(timeseriesData); @@ -192,4 +192,38 @@ abstract public class BaseTelemetryEdgeTest extends AbstractEdgeTest { return false; } + @Test + public void testTimeseriesDeliveryFailuresForever_deliverOnlyDeviceUpdateMsgs() throws Exception { + int numberOfMsgsToSend = 100; + + Device device = findDeviceByName("Edge Device 1"); + + edgeImitator.setRandomFailuresOnTimeseriesDownlink(true); + // imitator will generate failure in 100% of timeseries cases + edgeImitator.setFailureProbability(100); + edgeImitator.expectMessageAmount(numberOfMsgsToSend); + for (int idx = 1; idx <= numberOfMsgsToSend; idx++) { + String timeseriesData = "{\"data\":{\"idx\":" + idx + "},\"ts\":" + System.currentTimeMillis() + "}"; + JsonNode timeseriesEntityData = mapper.readTree(timeseriesData); + EdgeEvent failedEdgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.TIMESERIES_UPDATED, + device.getId().getId(), EdgeEventType.DEVICE, timeseriesEntityData); + edgeEventService.saveAsync(failedEdgeEvent).get(); + + EdgeEvent successEdgeEvent = constructEdgeEvent(tenantId, edge.getId(), EdgeEventActionType.UPDATED, + device.getId().getId(), EdgeEventType.DEVICE, null); + edgeEventService.saveAsync(successEdgeEvent).get(); + + clusterService.onEdgeEventUpdate(tenantId, edge.getId()); + } + + Assert.assertTrue(edgeImitator.waitForMessages(120)); + + List allTelemetryMsgs = edgeImitator.findAllMessagesByType(EntityDataProto.class); + Assert.assertTrue(allTelemetryMsgs.isEmpty()); + + List deviceUpdateMsgs = edgeImitator.findAllMessagesByType(DeviceUpdateMsg.class); + Assert.assertEquals(numberOfMsgsToSend, deviceUpdateMsgs.size()); + + edgeImitator.setRandomFailuresOnTimeseriesDownlink(false); + } } diff --git a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java index 7908f0a5a4..668f702a87 100644 --- a/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java +++ b/application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java @@ -106,6 +106,7 @@ public class EdgeImitator { this.routingSecret = routingSecret; setEdgeCredentials("rpcHost", host); setEdgeCredentials("rpcPort", port); + setEdgeCredentials("timeoutSecs", 3); setEdgeCredentials("keepAliveTimeSec", 300); } @@ -151,22 +152,18 @@ public class EdgeImitator { Futures.addCallback(future, new FutureCallback<>() { @Override public void onSuccess(@Nullable List result) { - if (connected) { - DownlinkResponseMsg downlinkResponseMsg = DownlinkResponseMsg.newBuilder() - .setDownlinkMsgId(downlinkMsg.getDownlinkMsgId()) - .setSuccess(true).build(); - edgeRpcClient.sendDownlinkResponseMsg(downlinkResponseMsg); - } + DownlinkResponseMsg downlinkResponseMsg = DownlinkResponseMsg.newBuilder() + .setDownlinkMsgId(downlinkMsg.getDownlinkMsgId()) + .setSuccess(true).build(); + edgeRpcClient.sendDownlinkResponseMsg(downlinkResponseMsg); } @Override public void onFailure(Throwable t) { - if (connected) { - DownlinkResponseMsg downlinkResponseMsg = DownlinkResponseMsg.newBuilder() - .setDownlinkMsgId(downlinkMsg.getDownlinkMsgId()) - .setSuccess(false).setErrorMsg(t.getMessage()).build(); - edgeRpcClient.sendDownlinkResponseMsg(downlinkResponseMsg); - } + DownlinkResponseMsg downlinkResponseMsg = DownlinkResponseMsg.newBuilder() + .setDownlinkMsgId(downlinkMsg.getDownlinkMsgId()) + .setSuccess(false).setErrorMsg(t.getMessage()).build(); + edgeRpcClient.sendDownlinkResponseMsg(downlinkResponseMsg); } }, MoreExecutors.directExecutor()); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index da9a5c49eb..c6a19e0040 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -87,6 +87,8 @@ public class EntityIdFactory { public static EntityId getByEdgeEventTypeAndUuid(EdgeEventType edgeEventType, UUID uuid) { switch (edgeEventType) { + case TENANT: + return new TenantId(uuid); case CUSTOMER: return new CustomerId(uuid); case USER: diff --git a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java index b6d9464bdd..522feed5fc 100644 --- a/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java +++ b/common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java @@ -142,7 +142,7 @@ public class EdgeGrpcClient implements EdgeRpcClient { @Override public void onError(Throwable t) { - log.debug("[{}] The rpc session received an error!", edgeKey, t); + log.warn("[{}] Stream was terminated due to error:", edgeKey, t); try { EdgeGrpcClient.this.disconnect(true); } catch (InterruptedException e) { @@ -153,7 +153,7 @@ public class EdgeGrpcClient implements EdgeRpcClient { @Override public void onCompleted() { - log.debug("[{}] The rpc session was closed!", edgeKey); + log.info("[{}] Stream was closed and completed successfully!", edgeKey); } }; } diff --git a/pom.xml b/pom.xml index 4896139822..909e62e375 100755 --- a/pom.xml +++ b/pom.xml @@ -92,7 +92,7 @@ 1.18.2 1.69 2.0.1 - 42.2.20 + 42.5.0 org/thingsboard/server/gen/**/*, org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/* diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java index 6ad0ae9352..fc23a2ff6c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java @@ -220,10 +220,10 @@ public class TbMathNode implements TbNode { private TbMsg addToBodyAndMeta(TbMsg msg, Optional msgBodyOpt, double result, TbMathResult mathResultDef) { TbMsg tmpMsg = msg; if (mathResultDef.isAddToBody()) { - tmpMsg = addToBody(msg, mathResultDef, msgBodyOpt, result); + tmpMsg = addToBody(tmpMsg, mathResultDef, msgBodyOpt, result); } if (mathResultDef.isAddToMetadata()) { - tmpMsg = addToMeta(msg, mathResultDef, result); + tmpMsg = addToMeta(tmpMsg, mathResultDef, result); } return tmpMsg; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbRuleNodeMathFunctionType.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbRuleNodeMathFunctionType.java index ffcd518f24..2ac3d481f7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbRuleNodeMathFunctionType.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbRuleNodeMathFunctionType.java @@ -23,7 +23,7 @@ public enum TbRuleNodeMathFunctionType { SIN, SINH, COS, COSH, TAN, TANH, ACOS, ASIN, ATAN, ATAN2(2), EXP, EXPM1, SQRT, CBRT, GET_EXP(1, 1, true), HYPOT(2), LOG, LOG10, LOG1P, CEIL(1, 1, true), FLOOR(1, 1, true), FLOOR_DIV(2), FLOOR_MOD(2), - ABS, MIN(2), MAX(2), POW, SIGNUM, RAD, DEG, + ABS, MIN(2), MAX(2), POW(2), SIGNUM, RAD, DEG, CUSTOM(0, 16, false); //Custom function based on exp4j diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathArgumentValueTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathArgumentValueTest.java new file mode 100644 index 0000000000..d8ef3312b4 --- /dev/null +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathArgumentValueTest.java @@ -0,0 +1,117 @@ +/** + * Copyright © 2016-2022 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.rule.engine.math; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Assert; +import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.math.TbMathArgument; +import org.thingsboard.rule.engine.math.TbMathArgumentType; +import org.thingsboard.rule.engine.math.TbMathArgumentValue; +import org.thingsboard.server.common.msg.TbMsgMetaData; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class TbMathArgumentValueTest { + + @Test + public void test_fromMessageBody_then_defaultValue() { + TbMathArgument tbMathArgument = new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "TestKey"); + tbMathArgument.setDefaultValue(5.0); + TbMathArgumentValue result = TbMathArgumentValue.fromMessageBody(tbMathArgument, Optional.ofNullable(JacksonUtil.newObjectNode())); + Assert.assertEquals(5.0, result.getValue(), 0d); + } + + @Test + public void test_fromMessageBody_then_emptyBody() { + TbMathArgument tbMathArgument = new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "TestKey"); + Throwable thrown = assertThrows(RuntimeException.class, () -> { + TbMathArgumentValue result = TbMathArgumentValue.fromMessageBody(tbMathArgument, Optional.empty()); + }); + Assert.assertNotNull(thrown.getMessage()); + } + + @Test + public void test_fromMessageBody_then_noKey() { + TbMathArgument tbMathArgument = new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "TestKey"); + Throwable thrown = assertThrows(RuntimeException.class, () -> TbMathArgumentValue.fromMessageBody(tbMathArgument, Optional.ofNullable(JacksonUtil.newObjectNode()))); + Assert.assertNotNull(thrown.getMessage()); + } + + @Test + public void test_fromMessageBody_then_valueEmpty() { + TbMathArgument tbMathArgument = new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "TestKey"); + ObjectNode msgData = JacksonUtil.newObjectNode(); + msgData.putNull("TestKey"); + + //null value + Throwable thrown = assertThrows(RuntimeException.class, () -> TbMathArgumentValue.fromMessageBody(tbMathArgument, Optional.of(msgData))); + Assert.assertNotNull(thrown.getMessage()); + + //empty value + msgData.put("TestKey", ""); + thrown = assertThrows(RuntimeException.class, () -> TbMathArgumentValue.fromMessageBody(tbMathArgument, Optional.of(msgData))); + Assert.assertNotNull(thrown.getMessage()); + } + + @Test + public void test_fromMessageBody_then_valueCantConvert_to_double() { + TbMathArgument tbMathArgument = new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "TestKey"); + ObjectNode msgData = JacksonUtil.newObjectNode(); + msgData.put("TestKey", "Test"); + + //string value + Throwable thrown = assertThrows(RuntimeException.class, () -> TbMathArgumentValue.fromMessageBody(tbMathArgument, Optional.of(msgData))); + Assert.assertNotNull(thrown.getMessage()); + + //object value + msgData.set("TestKey", JacksonUtil.newObjectNode()); + thrown = assertThrows(RuntimeException.class, () -> TbMathArgumentValue.fromMessageBody(tbMathArgument, Optional.of(msgData))); + Assert.assertNotNull(thrown.getMessage()); + } + + @Test + public void test_fromMessageMetadata_then_noKey() { + TbMathArgument tbMathArgument = new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "TestKey"); + Throwable thrown = assertThrows(RuntimeException.class, () -> TbMathArgumentValue.fromMessageMetadata(tbMathArgument, new TbMsgMetaData())); + Assert.assertNotNull(thrown.getMessage()); + } + + @Test + public void test_fromMessageMetadata_then_valueEmpty() { + TbMathArgument tbMathArgument = new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "TestKey"); + Throwable thrown = assertThrows(RuntimeException.class, () -> TbMathArgumentValue.fromMessageMetadata(tbMathArgument, null)); + Assert.assertNotNull(thrown.getMessage()); + } + + @Test + public void test_fromString_thenOK() { + var value = "5.0"; + TbMathArgumentValue result = TbMathArgumentValue.fromString(value); + Assert.assertNotNull(result); + Assert.assertEquals(5.0, result.getValue(), 0d); + } + + @Test + public void test_fromString_then_failure() { + var value = "Test"; + Throwable thrown = assertThrows(RuntimeException.class, () -> TbMathArgumentValue.fromString(value)); + Assert.assertNotNull(thrown.getMessage()); + } +} diff --git a/rule-engine/rule-engine-components/src/test/java/math/TbMathNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java similarity index 63% rename from rule-engine/rule-engine-components/src/test/java/math/TbMathNodeTest.java rename to rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java index 5a0cf6eb91..d682e398e8 100644 --- a/rule-engine/rule-engine-components/src/test/java/math/TbMathNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package math; +package org.thingsboard.rule.engine.math; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.common.util.concurrent.Futures; @@ -28,6 +28,7 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.thingsboard.common.util.AbstractListeningExecutor; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.api.RuleEngineTelemetryService; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; @@ -45,6 +46,7 @@ import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.attributes.AttributesService; @@ -53,7 +55,13 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService; import java.util.Arrays; import java.util.Optional; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) public class TbMathNodeTest { @@ -67,7 +75,8 @@ public class TbMathNodeTest { private AttributesService attributesService; @Mock private TimeseriesService tsService; - + @Mock + private RuleEngineTelemetryService telemetryService; private AbstractListeningExecutor dbExecutor; @Before @@ -91,7 +100,9 @@ public class TbMathNodeTest { Mockito.reset(ctx); Mockito.reset(attributesService); Mockito.reset(tsService); + Mockito.reset(telemetryService); lenient().when(ctx.getAttributesService()).thenReturn(attributesService); + lenient().when(ctx.getTelemetryService()).thenReturn(telemetryService); lenient().when(ctx.getTimeseriesService()).thenReturn(tsService); lenient().when(ctx.getTenantId()).thenReturn(tenantId); lenient().when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor); @@ -163,10 +174,36 @@ public class TbMathNodeTest { testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.TAN, Math.toRadians(45), 1); testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.TAN, Math.toRadians(0), 0); + testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.TANH, 90, 1); + + testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.ACOS, 0.5, 1.05); + testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.ASIN, 0.5, 0.52); + testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.ATAN, 0.5, 0.46); + testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.ATAN2, 0.5, 0.3, 1.03); + testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.EXP, 1, 2.72); + testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.EXPM1, 1, 1.72); testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.ABS, -1, 1); testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.SQRT, 4, 2); testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.CBRT, 8, 2); + + testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.GET_EXP, 4, 2); + testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.HYPOT, 4, 5, 6.4); + testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.LOG, 4, 1.39); + testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.LOG10, 4, 0.6); + testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.LOG1P, 4, 1.61); + + testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.CEIL, 1.55, 2); + testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.FLOOR, 23.97, 23); + testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.FLOOR_DIV, 5, 3, 1); + testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.FLOOR_MOD, 6, 3, 0); + + testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.MIN, 5, 3, 3); + testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.MAX, 5, 3, 5); + testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType.POW, 5, 3, 125); + testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.SIGNUM, 0.55, 1); + testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.RAD, 5, 0.09); + testSimpleOneArgumentFunction(TbRuleNodeMathFunctionType.DEG, 5, 286.48); } private void testSimpleTwoArgumentFunction(TbRuleNodeMathFunctionType function, double arg1, double arg2, double result) { @@ -336,4 +373,133 @@ public class TbMathNodeTest { Assert.assertEquals("2.236", result); } + @Test + public void test_sqrt_5_to_attribute_and_metadata() { + var node = initNode(TbRuleNodeMathFunctionType.SQRT, + new TbMathResult(TbMathArgumentType.ATTRIBUTE, "result", 3, false, true, DataConstants.SERVER_SCOPE), + new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a") + ); + + TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 5).toString()); + + Mockito.when(telemetryService.saveAttrAndNotify(any(), any(), anyString(), anyString(), anyDouble())) + .thenReturn(Futures.immediateFuture(null)); + + node.onMsg(ctx, msg); + + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture()); + Mockito.verify(telemetryService, times(1)).saveAttrAndNotify(any(), any(), anyString(), anyString(), anyDouble()); + + TbMsg resultMsg = msgCaptor.getValue(); + Assert.assertNotNull(resultMsg); + Assert.assertNotNull(resultMsg.getData()); + var result = resultMsg.getMetaData().getValue("result"); + Assert.assertNotNull(result); + Assert.assertEquals("2.236", result); + } + + @Test + public void test_sqrt_5_to_timeseries_and_data() { + var node = initNode(TbRuleNodeMathFunctionType.SQRT, + new TbMathResult(TbMathArgumentType.TIME_SERIES, "result", 3, true, false, DataConstants.SERVER_SCOPE), + new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a") + ); + + TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 5).toString()); + Mockito.when(telemetryService.saveAndNotify(any(), any(), any(TsKvEntry.class))) + .thenReturn(Futures.immediateFuture(null)); + + node.onMsg(ctx, msg); + + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture()); + Mockito.verify(telemetryService, times(1)).saveAndNotify(any(), any(), any(TsKvEntry.class)); + + TbMsg resultMsg = msgCaptor.getValue(); + Assert.assertNotNull(resultMsg); + Assert.assertNotNull(resultMsg.getData()); + var resultJson = JacksonUtil.toJsonNode(resultMsg.getData()); + Assert.assertTrue(resultJson.has("result")); + Assert.assertEquals(2.236, resultJson.get("result").asDouble(), 0.0); + } + + @Test + public void test_sqrt_5_to_timeseries_and_metadata_and_data() { + var node = initNode(TbRuleNodeMathFunctionType.SQRT, + new TbMathResult(TbMathArgumentType.TIME_SERIES, "result", 3, true, true, DataConstants.SERVER_SCOPE), + new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a") + ); + + TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 5).toString()); + Mockito.when(telemetryService.saveAndNotify(any(), any(), any(TsKvEntry.class))) + .thenReturn(Futures.immediateFuture(null)); + + node.onMsg(ctx, msg); + + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture()); + Mockito.verify(telemetryService, times(1)).saveAndNotify(any(), any(), any(TsKvEntry.class)); + + TbMsg resultMsg = msgCaptor.getValue(); + Assert.assertNotNull(resultMsg); + Assert.assertNotNull(resultMsg.getData()); + var resultMetadata = resultMsg.getMetaData().getValue("result"); + var resultData = JacksonUtil.toJsonNode(resultMsg.getData()); + + Assert.assertTrue(resultData.has("result")); + Assert.assertEquals(2.236, resultData.get("result").asDouble(), 0.0); + + Assert.assertNotNull(resultMetadata); + Assert.assertEquals("2.236", resultMetadata); + } + + @Test + public void test_sqrt_5_default_value() { + TbMathArgument tbMathArgument = new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "TestKey"); + tbMathArgument.setDefaultValue(5.0); + var node = initNode(TbRuleNodeMathFunctionType.SQRT, + new TbMathResult(TbMathArgumentType.MESSAGE_METADATA, "result", 3, false, false, null), + tbMathArgument + ); + TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 10).toString()); + + node.onMsg(ctx, msg); + ArgumentCaptor msgCaptor = ArgumentCaptor.forClass(TbMsg.class); + Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture()); + + TbMsg resultMsg = msgCaptor.getValue(); + Assert.assertNotNull(resultMsg); + Assert.assertNotNull(resultMsg.getData()); + var result = resultMsg.getMetaData().getValue("result"); + Assert.assertNotNull(result); + Assert.assertEquals("2.236", result); + } + + @Test + public void test_sqrt_5_default_value_failure() { + var node = initNode(TbRuleNodeMathFunctionType.SQRT, + new TbMathResult(TbMathArgumentType.TIME_SERIES, "result", 3, true, false, DataConstants.SERVER_SCOPE), + new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "TestKey") + ); + TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), JacksonUtil.newObjectNode().put("a", 10).toString()); + Throwable thrown = assertThrows(RuntimeException.class, () -> { + node.onMsg(ctx, msg); + }); + Assert.assertNotNull(thrown.getMessage()); + } + + @Test + public void testConvertMsgBodyIfRequiredFailure() { + var node = initNode(TbRuleNodeMathFunctionType.SQRT, + new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", 3, true, false, DataConstants.SERVER_SCOPE), + new TbMathArgument(TbMathArgumentType.MESSAGE_BODY, "a") + ); + + TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), "[]"); + Throwable thrown = assertThrows(RuntimeException.class, () -> { + node.onMsg(ctx, msg); + }); + Assert.assertNotNull(thrown.getMessage()); + } } diff --git a/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.ts b/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.ts index 68b803e006..ee4a7f7378 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.ts @@ -108,7 +108,7 @@ export class EventFilterPanelComponent { changeIsError(value: boolean | string) { if (this.conditionError && value === '') { - this.eventFilterFormGroup.get('error').reset('', {emitEvent: false}); + this.eventFilterFormGroup.get('errorStr').reset('', {emitEvent: false}); } } } diff --git a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts index 19c49638ff..0e5cb4e91e 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table-config.ts @@ -440,6 +440,8 @@ export class EventTableConfig extends EntityTableConfig { }; config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) .withPositions([connectedPosition]); + config.maxHeight = '70vh'; + config.height = 'min-content'; const overlayRef = this.overlay.create(config); overlayRef.backdropClick().subscribe(() => { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts index 18078d3255..46fe0ed9cb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts @@ -75,6 +75,7 @@ import { getColumnWidth, getRowStyleInfo, getTableCellButtonActions, + getHeaderTitle, noDataMessage, prepareTableCellButtonActions, RowStyleInfo, @@ -407,11 +408,11 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, if (this.subscription.alarmSource) { this.subscription.alarmSource.dataKeys.forEach((alarmDataKey) => { const dataKey: EntityColumn = deepClone(alarmDataKey) as EntityColumn; + const keySettings: TableWidgetDataKeySettings = dataKey.settings; dataKey.entityKey = dataKeyToEntityKey(alarmDataKey); dataKey.label = this.utils.customTranslation(dataKey.label, dataKey.label); - dataKey.title = dataKey.label; + dataKey.title = getHeaderTitle(dataKey, keySettings, this.utils); dataKey.def = 'def' + this.columns.length; - const keySettings: TableWidgetDataKeySettings = dataKey.settings; if (dataKey.type === DataKeyType.alarm && !isDefined(keySettings.columnWidth)) { const alarmField = alarmFields[dataKey.name]; if (alarmField && alarmField.time) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts index 5617ddcc9b..dbd20189ad 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts @@ -78,6 +78,7 @@ import { getColumnDefaultVisibility, getColumnSelectionAvailability, getColumnWidth, + getHeaderTitle, getEntityValue, getRowStyleInfo, getTableCellButtonActions, @@ -426,11 +427,11 @@ export class EntitiesTableWidgetComponent extends PageComponent implements OnIni } dataKeys.push(dataKey); + const keySettings: TableWidgetDataKeySettings = dataKey.settings; dataKey.label = this.utils.customTranslation(dataKey.label, dataKey.label); - dataKey.title = dataKey.label; + dataKey.title = getHeaderTitle(dataKey, keySettings, this.utils); dataKey.def = 'def' + this.columns.length; dataKey.sortable = !dataKey.usePostProcessing && (!dataKey.aggregationType || dataKey.aggregationType === AggregationType.NONE); - const keySettings: TableWidgetDataKeySettings = dataKey.settings; if (dataKey.type === DataKeyType.entityField && !isDefined(keySettings.columnWidth) || keySettings.columnWidth === '0px') { const entityField = entityFields[dataKey.name]; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts index 121a6aabce..2301a8c47a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts @@ -21,11 +21,15 @@ import { MarkerClusterGroup, MarkerClusterGroupOptions } from 'leaflet.markerclu import '@geoman-io/leaflet-geoman-free'; import { - CircleData, defaultMapSettings, - MarkerClusteringSettings, + CircleData, + defaultMapSettings, MarkerIconInfo, MarkerImageInfo, - WidgetPolygonSettings, WidgetPolylineSettings, WidgetMarkersSettings, WidgetUnitedMapSettings + WidgetMarkerClusteringSettings, + WidgetMarkersSettings, + WidgetPolygonSettings, + WidgetPolylineSettings, + WidgetUnitedMapSettings } from './map-models'; import { Marker } from './markers'; import { Observable, of } from 'rxjs'; @@ -33,10 +37,7 @@ import { Polyline } from './polyline'; import { Polygon } from './polygon'; import { Circle } from './circle'; import { createTooltip, isCutPolygon, isJSON } from '@home/components/widget/lib/maps/maps-utils'; -import { - checkLngLat, - createLoadingDiv -} from '@home/components/widget/lib/maps/common-maps-utils'; +import { checkLngLat, createLoadingDiv } from '@home/components/widget/lib/maps/common-maps-utils'; import { WidgetContext } from '@home/models/widget-component.models'; import { deepClone, @@ -44,7 +45,9 @@ import { formattedDataFormDatasourceData, isDefinedAndNotNull, isNotEmptyStr, - isString, mergeFormattedData, safeExecute + isString, + mergeFormattedData, + safeExecute } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; import { @@ -52,8 +55,8 @@ import { SelectEntityDialogData } from '@home/components/widget/lib/maps/dialogs/select-entity-dialog.component'; import { MatDialog } from '@angular/material/dialog'; -import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; import { FormattedData, ReplaceInfo } from '@shared/models/widget.models'; +import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance; export default abstract class LeafletMap { @@ -98,6 +101,8 @@ export default abstract class LeafletMap { translateService: TranslateService; tooltipInstances: ITooltipsterInstance[] = []; + clusteringSettings: MarkerClusterGroupOptions; + protected constructor(public ctx: WidgetContext, public $container: HTMLElement, options: WidgetUnitedMapSettings) { @@ -111,35 +116,68 @@ export default abstract class LeafletMap { } private initMarkerClusterSettings() { - const markerClusteringSettings: MarkerClusteringSettings = this.options; - if (markerClusteringSettings.useClusterMarkers) { - // disabled marker cluster icon - (L as any).MarkerCluster = (L as any).MarkerCluster.extend({ - options: { pmIgnore: true, ...L.Icon.prototype.options } - }); - const clusteringSettings: MarkerClusterGroupOptions = { - spiderfyOnMaxZoom: markerClusteringSettings.spiderfyOnMaxZoom, - zoomToBoundsOnClick: markerClusteringSettings.zoomOnClick, - showCoverageOnHover: markerClusteringSettings.showCoverageOnHover, - removeOutsideVisibleBounds: markerClusteringSettings.removeOutsideVisibleBounds, - animate: markerClusteringSettings.animate, - chunkedLoading: markerClusteringSettings.chunkedLoading, - pmIgnore: true, - spiderLegPolylineOptions: { - pmIgnore: true - }, - polygonOptions: { - pmIgnore: true - } - }; - if (markerClusteringSettings.maxClusterRadius && markerClusteringSettings.maxClusterRadius > 0) { - clusteringSettings.maxClusterRadius = Math.floor(markerClusteringSettings.maxClusterRadius); + const markerClusteringSettings: WidgetMarkerClusteringSettings = this.options; + if (markerClusteringSettings.useClusterMarkers) { + // disabled marker cluster icon + (L as any).MarkerCluster = (L as any).MarkerCluster.extend({ + options: { pmIgnore: true, ...L.Icon.prototype.options } + }); + this.clusteringSettings = { + spiderfyOnMaxZoom: markerClusteringSettings.spiderfyOnMaxZoom, + zoomToBoundsOnClick: markerClusteringSettings.zoomOnClick, + showCoverageOnHover: markerClusteringSettings.showCoverageOnHover, + removeOutsideVisibleBounds: markerClusteringSettings.removeOutsideVisibleBounds, + animate: markerClusteringSettings.animate, + chunkedLoading: markerClusteringSettings.chunkedLoading, + pmIgnore: true, + spiderLegPolylineOptions: { + pmIgnore: true + }, + polygonOptions: { + pmIgnore: true } - if (markerClusteringSettings.maxZoom && markerClusteringSettings.maxZoom >= 0 && markerClusteringSettings.maxZoom < 19) { - clusteringSettings.disableClusteringAtZoom = Math.floor(markerClusteringSettings.maxZoom); + }; + if (markerClusteringSettings.useIconCreateFunction && markerClusteringSettings.clusterMarkerFunction) { + this.clusteringSettings.iconCreateFunction = (cluster) => { + const childCount = cluster.getChildCount(); + const formattedData = cluster.getAllChildMarkers().map(clusterMarker => clusterMarker.options.tbMarkerData); + const markerColor = markerClusteringSettings.clusterMarkerFunction + ? safeExecute(markerClusteringSettings.parsedClusterMarkerFunction, + [formattedData, childCount]) + : null; + if (isDefinedAndNotNull(markerColor) && tinycolor(markerColor).isValid()) { + const parsedColor = tinycolor(markerColor); + return L.divIcon({ + html: `
` + + `
` + childCount + '
', + iconSize: new L.Point(40, 40), + className: 'tb-cluster-marker-container' + }); + } else { + let c = ' marker-cluster-'; + if (childCount < 10) { + c += 'small'; + } else if (childCount < 100) { + c += 'medium'; + } else { + c += 'large'; + } + return new L.DivIcon({ + html: '
' + childCount + '
', + className: 'marker-cluster' + c, + iconSize: new L.Point(40, 40) + }); } - this.markersCluster = new MarkerClusterGroup(clusteringSettings); + }; } + if (markerClusteringSettings.maxClusterRadius && markerClusteringSettings.maxClusterRadius > 0) { + this.clusteringSettings.maxClusterRadius = Math.floor(markerClusteringSettings.maxClusterRadius); + } + if (markerClusteringSettings.maxZoom && markerClusteringSettings.maxZoom >= 0 && markerClusteringSettings.maxZoom < 19) { + this.clusteringSettings.disableClusteringAtZoom = Math.floor(markerClusteringSettings.maxZoom); + } + this.markersCluster = new MarkerClusterGroup(this.clusteringSettings); + } } private selectEntityWithoutLocationDialog(shapes: L.PM.SUPPORTED_SHAPES): Observable { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts index 21aee908da..fdc24ab2a7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts @@ -17,7 +17,6 @@ import { Datasource, FormattedData } from '@app/shared/models/widget.models'; import tinycolor from 'tinycolor2'; import { BaseIconOptions, Icon } from 'leaflet'; -import { DeviceProfileType } from '@shared/models/device.models'; export const DEFAULT_MAP_PAGE_SIZE = 16384; export const DEFAULT_ZOOM_LEVEL = 8; @@ -604,6 +603,12 @@ export interface MarkerClusteringSettings { showCoverageOnHover: boolean; chunkedLoading: boolean; removeOutsideVisibleBounds: boolean; + useIconCreateFunction: boolean; + clusterMarkerFunction?: string; +} + +export interface WidgetMarkerClusteringSettings extends MarkerClusteringSettings { + parsedClusterMarkerFunction?: GenericFunction; } export const defaultMarkerClusteringSettings: MarkerClusteringSettings = { @@ -615,7 +620,9 @@ export const defaultMarkerClusteringSettings: MarkerClusteringSettings = { spiderfyOnMaxZoom: false, showCoverageOnHover: true, chunkedLoading: false, - removeOutsideVisibleBounds: true + removeOutsideVisibleBounds: true, + useIconCreateFunction: false, + clusterMarkerFunction: null }; export interface MapEditorSettings { @@ -637,7 +644,7 @@ export const defaultMapEditorSettings: MapEditorSettings = { }; export type UnitedMapSettings = MapProviderSettings & CommonMapSettings & MarkersSettings & - PolygonSettings & CircleSettings & PolylineSettings & PointsSettings & MarkerClusteringSettings & MapEditorSettings; + PolygonSettings & CircleSettings & PolylineSettings & PointsSettings & WidgetMarkerClusteringSettings & MapEditorSettings; export const defaultMapSettings: UnitedMapSettings = { ...defaultMapProviderSettings, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts index dca583c130..dbbccd824e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts @@ -250,6 +250,7 @@ export class MapWidgetController implements MapWidgetInterface { parsedCircleFillColorFunction: parseFunction(settings.circleFillColorFunction, functionParams), parsedCircleTooltipFunction: parseFunction(settings.circleTooltipFunction, functionParams), parsedMarkerImageFunction: parseFunction(settings.markerImageFunction, ['data', 'images', 'dsData', 'dsIndex']), + parsedClusterMarkerFunction: parseFunction(settings.clusterMarkerFunction, ['data', 'childCount']), // labelColor: this.ctx.widgetConfig.color, // polygonLabelColor: this.ctx.widgetConfig.color, polygonKeyName: (settings as any).polKeyName ? (settings as any).polKeyName : settings.polygonKeyName, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss index 3d93f87ee0..7424334d81 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss @@ -41,3 +41,15 @@ .leaflet-container { background-color: white; } + +.tb-cluster-marker-container { + border: none; + background-color: transparent; +} +.tb-cluster-marker-element { + position: absolute; + top: 0; + left: 0; + width: 40px; + height: 40px; +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts index 09c9f7dad1..007f82b869 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts @@ -15,11 +15,7 @@ /// import L, { LeafletMouseEvent } from 'leaflet'; -import { - MarkerIconInfo, - MarkerIconReadyFunction, - MarkerImageInfo, WidgetMarkersSettings, -} from './map-models'; +import { MarkerIconInfo, MarkerIconReadyFunction, MarkerImageInfo, WidgetMarkersSettings, } from './map-models'; import { bindPopupActions, createTooltip } from './maps-utils'; import { aspectCache, parseWithTranslation } from './common-maps-utils'; import tinycolor from 'tinycolor2'; @@ -46,7 +42,8 @@ export class Marker { snappable = false) { this.leafletMarker = L.marker(location, { pmIgnore: !settings.draggableMarker, - snapIgnore: !snappable + snapIgnore: !snappable, + tbMarkerData: this.data }); this.markerOffset = [ @@ -93,8 +90,9 @@ export class Marker { } setDataSources(data: FormattedData, dataSources: FormattedData[]) { - this.data = data; - this.dataSources = dataSources; + this.data = data; + this.dataSources = dataSources; + this.leafletMarker.options.tbMarkerData = data; } updateMarkerTooltip(data: FormattedData) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html index 6931d256e8..d592bb3749 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html @@ -16,6 +16,10 @@ -->
+ + widgets.table.custom-title + + widgets.table.column-width diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.ts index 9f9cbbf5e1..3f5c257b38 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.ts @@ -40,6 +40,7 @@ export class AlarmsTableKeySettingsComponent extends WidgetSettingsComponent { protected defaultSettings(): WidgetSettings { return { + customTitle: '', columnWidth: '0px', useCellStyleFunction: false, cellStyleFunction: '', @@ -52,6 +53,7 @@ export class AlarmsTableKeySettingsComponent extends WidgetSettingsComponent { protected onSettingsSet(settings: WidgetSettings) { this.alarmsTableKeySettingsForm = this.fb.group({ + customTitle: [settings.customTitle, []], columnWidth: [settings.columnWidth, []], useCellStyleFunction: [settings.useCellStyleFunction, []], cellStyleFunction: [settings.cellStyleFunction, [Validators.required]], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html index 81086f046c..acc5dcbb79 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html @@ -16,6 +16,10 @@ -->
+ + widgets.table.custom-title + + widgets.table.column-width diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.ts index 738e99223c..8301e81bfb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.ts @@ -40,6 +40,7 @@ export class EntitiesTableKeySettingsComponent extends WidgetSettingsComponent { protected defaultSettings(): WidgetSettings { return { + customTitle: '', columnWidth: '0px', useCellStyleFunction: false, cellStyleFunction: '', @@ -52,6 +53,7 @@ export class EntitiesTableKeySettingsComponent extends WidgetSettingsComponent { protected onSettingsSet(settings: WidgetSettings) { this.entitiesTableKeySettingsForm = this.fb.group({ + customTitle: [settings.customTitle, []], columnWidth: [settings.columnWidth, []], useCellStyleFunction: [settings.useCellStyleFunction, []], cellStyleFunction: [settings.cellStyleFunction, [Validators.required]], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html index bc590c3ce2..c454ef1af0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html @@ -31,8 +31,8 @@ widgets.value-source.value -
+
{{ 'widgets.value-source.source-entity-alias' | translate }} +
+ widgets.maps.clustering-markers + + + + + {{ 'widgets.maps.use-icon-create-function' | translate }} + + + + widget-config.advanced-settings + + + + + + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.ts index f1ca781bed..087b8aaa9d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.ts @@ -56,6 +56,8 @@ export class MarkerClusteringSettingsComponent extends PageComponent implements private modelValue: MarkerClusteringSettings; + functionScopeVariables = this.widgetService.getWidgetScopeVariables(); + private propagateChange = null; public markerClusteringSettingsFormGroup: FormGroup; @@ -77,7 +79,9 @@ export class MarkerClusteringSettingsComponent extends PageComponent implements spiderfyOnMaxZoom: [null, []], showCoverageOnHover: [null, []], chunkedLoading: [null, []], - removeOutsideVisibleBounds: [null, []] + removeOutsideVisibleBounds: [null, []], + useIconCreateFunction: [null, []], + clusterMarkerFunction: [null, []] }); this.markerClusteringSettingsFormGroup.valueChanges.subscribe(() => { this.updateModel(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts index bcd0cd4c80..b18806bce3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts @@ -43,6 +43,7 @@ export interface TableWidgetSettings { } export interface TableWidgetDataKeySettings { + customTitle?: string; columnWidth?: string; useCellStyleFunction: boolean; cellStyleFunction?: string; @@ -475,3 +476,10 @@ export function constructTableCssString(widgetConfig: WidgetConfig): string { '}'; return cssString; } + +export function getHeaderTitle(dataKey: DataKey, keySettings: TableWidgetDataKeySettings, utils: UtilsService) { + if (isDefined(keySettings.customTitle) && isNotEmptyStr(keySettings.customTitle)) { + return utils.customTranslation(keySettings.customTitle, keySettings.customTitle); + } + return dataKey.label; +} diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html index 24a2370e72..efa10a3ad3 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + { this.updateModel(); this.defineCountryFromNumber(value); diff --git a/ui-ngx/src/app/shared/components/time/timezone-select.component.html b/ui-ngx/src/app/shared/components/time/timezone-select.component.html index 2d3a027144..be3c88f131 100644 --- a/ui-ngx/src/app/shared/components/time/timezone-select.component.html +++ b/ui-ngx/src/app/shared/components/time/timezone-select.component.html @@ -15,7 +15,7 @@ limitations under the License. --> - + timezone.timezone +
+ +*function (data, childCount): string* + +A JavaScript function used to compute clustering marker color. + +**Parameters:** + +
    +
  • data: FormattedData[] + - the array of total markers contained within each cluster.
    + Represents basic entity properties (ex. entityId, entityName)
    and provides access to other entity attributes/timeseries declared in widget datasource configuration. +
  • +
  • childCount: number - the total number of markers contained within that cluster +
  • +
+ +**Returns:** + +Should return string value presenting color of the marker. + +In case no data is returned, color value from **Color** settings field will be used. + +
+ +##### Examples + +
    +
  • +Calculate color depending on temperature telemetry value: +
  • + + +```javascript +let customColor; +for (let markerData of data) { + if (markerData.temperature > 40) { + customColor = 'red' + } +} +return customColor ? customColor : 'green'; +{:copy-code} +``` + +
  • +Calculate color depending on childCount: +
  • + +```javascript +if (childCount < 10) { + return 'green'; +} else if (childCount < 100) { + return 'yellow'; +} else { + return 'red'; +} +{:copy-code} +``` + +
+
+
diff --git a/ui-ngx/src/assets/locale/locale.constant-de_DE.json b/ui-ngx/src/assets/locale/locale.constant-de_DE.json index 59adb6bf31..d60ed055ad 100644 --- a/ui-ngx/src/assets/locale/locale.constant-de_DE.json +++ b/ui-ngx/src/assets/locale/locale.constant-de_DE.json @@ -23,7 +23,7 @@ "no": "Nein", "update": "Aktualisieren", "remove": "Löschen", - "select": "Auswählen", + "select": "Auswählen", "search": "Suche", "clear-search": "Suchanfrage löschen", "assign": "Zuordnen", @@ -57,7 +57,11 @@ "download": "Download", "next-with-label": "Nächste: {{label}}", "read-more": "Mehr dazu", - "hide": "Verstecken" + "hide": "Verstecken", + "done": "Erledigt", + "print": "Drucken", + "restore": "Wiederherstellen", + "confirm": "Bestätigen" }, "aggregation": { "aggregation": "Aggregation", @@ -74,15 +78,15 @@ "admin": { "general": "Allgemein", "general-settings": "Allgemeine Einstellungen", - "home-settings": "Home Einstellungen", + "home-settings": "Home Einstellungen", "outgoing-mail": "E-Mail Versand", "outgoing-mail-settings": "Konfiguration des Postausgangsservers", "system-settings": "Systemeinstellungen", "test-mail-sent": "Test E-Mail wurde erfolgreich versendet!", "base-url": "Basis-URL", "base-url-required": "Basis-URL ist erforderlich.", - "prohibit-different-url": "Prohibit to use hostname from the client request headers", - "prohibit-different-url-hint": "This setting should be enabled for production environments. May cause security issues when disabled", + "prohibit-different-url": "Prohibit to use hostname from the client request headers", + "prohibit-different-url-hint": "This setting should be enabled for production environments. May cause security issues when disabled", "mail-from": "E-Mail von", "mail-from-required": "E-Mail von ist erforderlich.", "smtp-protocol": "SMTP Protokoll", @@ -96,7 +100,23 @@ "timeout-invalid": "Das ist keine gültige Wartezeit.", "enable-tls": "TLS aktivieren", "tls-version" : "TLS-Version", + "enable-proxy": "Proxy aktivieren", + "proxy-host": "Proxy Host", + "proxy-host-required": "Proxy Host ist erforderlich.", + "proxy-port": "Proxy Port", + "proxy-port-required": "Proxy Port ist erforderlich.", + "proxy-port-range": "Proxy Port sollte im Bereich 1 bis 65535 sein.", + "proxy-user": "Proxy Benutzername", + "proxy-password": "Proxy Passwort", + "change-password": "Passwort ändern", "send-test-mail": "Test E-Mail senden", + "sms-provider": "SMS Anbieter", + "sms-provider-settings": "SMS Anbieter Einstellungen", + "sms-provider-type": "SMS Anbieter Typ", + "sms-provider-type-required": "SMS Anbieter Typ ist erforderlich.", + "sms-provider-type-aws-sns": "Amazon SNS", + "sms-provider-type-twilio": "Twilio", + "sms-provider-type-smpp": "SMPP", "security-settings": "Sicherheitseinstellungen", "password-policy": "Kennwortrichtlinie", "minimum-password-length": "Minimale Passwortlänge", @@ -126,6 +146,8 @@ "no-alarms-matching": "Keine passenden Alarme zu '{{entity}}' wurden gefunden.", "alarm-required": "Alarm ist erforderlich", "alarm-status": "Alarm Status", + "alarm-status-list": "Alarm Status Liste", + "any-status": "Jeder Status", "search-status": { "ANY": "Jeder", "ACTIVE": "Aktiv", @@ -152,6 +174,8 @@ "end-time": "Endzeit", "ack-time": "Bestätigungszeit", "clear-time": "Zeit gelöscht", + "alarm-severity-list": "Alarm Schwere Liste", + "any-severity": "Jede Schwere", "severity-critical": "Kritisch", "severity-major": "Groß", "severity-minor": "Klein", @@ -159,7 +183,7 @@ "severity-indeterminate": "Unbestimmt", "acknowledge": "Bestätigen", "clear": "Löschen", - "search": "Alarme Suchen", + "search": "Alarme suchen", "selected-alarms": "{ count, plural, 1 {1 Alarm} other {# Alarme} } ausgewählt", "no-data": "Keine Daten zum Anzeigen", "polling-interval": "Alarmabfrageintervall (sec)", @@ -184,6 +208,7 @@ "filter-type-single-entity": "Einzelne Entität", "filter-type-entity-list": "Entitätsliste", "filter-type-entity-name": "Entitätsname", + "filter-type-entity-type": "Entitätstyp", "filter-type-state-entity": "Entität aus dem Dashboard Status", "filter-type-state-entity-description": "Entität aus den Dashboard Status Parametern", "filter-type-asset-type": "Objekttyp", @@ -1890,6 +1915,14 @@ "row-double-click": "Doppelklicken auf Zeile" } }, + "paginator" : { + "items-per-page": "Einträge pro Seite:", + "first-page-label": "Erste Seite", + "last-page-label": "Letzte Seite", + "next-page-label": "Nächste Seite", + "previous-page-label": "Vorherige Seite", + "items-per-page-separator": "von" + }, "language": { "language": "Sprache" } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index e8c322c650..2b1eb11de7 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -4630,7 +4630,10 @@ "point-color-function": "Point color function", "use-point-as-anchor": "Use point as anchor", "point-as-anchor-function": "Point as anchor function", - "independent-point-tooltip": "Independent point tooltip" + "independent-point-tooltip": "Independent point tooltip", + "clustering-markers": "Clustering markers", + "use-icon-create-function": "Use markers colour function", + "marker-color-function": "Marker color function" }, "markdown": { "use-markdown-text-function": "Use markdown/HTML value function", @@ -4676,6 +4679,7 @@ "entity-label-column-title": "Entity label column title", "display-entity-type": "Display entity type column", "default-sort-order": "Default sort order", + "custom-title": "Custom header title", "column-width": "Column width (px or %)", "default-column-visibility": "Default column visibility", "column-visibility-visible": "Visible", diff --git a/ui-ngx/src/typings/leaflet-extend-tb.d.ts b/ui-ngx/src/typings/leaflet-extend-tb.d.ts new file mode 100644 index 0000000000..887356c8de --- /dev/null +++ b/ui-ngx/src/typings/leaflet-extend-tb.d.ts @@ -0,0 +1,24 @@ +/// +/// Copyright © 2016-2022 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { FormattedData } from '@shared/models/widget.models'; + +// redeclare module, maintains compatibility with @types/leaflet +declare module 'leaflet' { + interface MarkerOptions { + tbMarkerData?: FormattedData; + } +} diff --git a/ui-ngx/tsconfig.json b/ui-ngx/tsconfig.json index 9963cadb75..07c6df4d2d 100644 --- a/ui-ngx/tsconfig.json +++ b/ui-ngx/tsconfig.json @@ -23,7 +23,8 @@ "src/typings/jquery.flot.typings.d.ts", "src/typings/jquery.jstree.typings.d.ts", "src/typings/split.js.typings.d.ts", - "src/typings/leaflet-geoman-extend.d.ts" + "src/typings/leaflet-geoman-extend.d.ts", + "src/typings/leaflet-extend-tb.d.ts", ], "paths": { "@app/*": ["src/app/*"],