Browse Source

Merge branch 'master' of github.com:thingsboard/thingsboard

pull/7525/head
Andrii Shvaika 4 years ago
parent
commit
c10e491de7
  1. 3
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  2. 3
      application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java
  3. 36
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  4. 18
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/EntityDataMsgConstructor.java
  5. 45
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  6. 6
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/DeviceEdgeProcessor.java
  7. 137
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java
  8. 8
      application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
  9. 2
      application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java
  10. 40
      application/src/test/java/org/thingsboard/server/edge/BaseTelemetryEdgeTest.java
  11. 21
      application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
  12. 2
      common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java
  13. 4
      common/edge-api/src/main/java/org/thingsboard/edge/rpc/EdgeGrpcClient.java
  14. 2
      pom.xml
  15. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java
  16. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbRuleNodeMathFunctionType.java
  17. 117
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathArgumentValueTest.java
  18. 170
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/math/TbMathNodeTest.java
  19. 2
      ui-ngx/src/app/modules/home/components/event/event-filter-panel.component.ts
  20. 2
      ui-ngx/src/app/modules/home/components/event/event-table-config.ts
  21. 5
      ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts
  22. 5
      ui-ngx/src/app/modules/home/components/widget/lib/entities-table-widget.component.ts
  23. 108
      ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts
  24. 13
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts
  25. 1
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts
  26. 12
      ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss
  27. 14
      ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts
  28. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html
  29. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.ts
  30. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html
  31. 2
      ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.ts
  32. 4
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html
  33. 24
      ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.html
  34. 6
      ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.ts
  35. 8
      ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts
  36. 2
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html
  37. 4
      ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts
  38. 7
      ui-ngx/src/app/shared/components/phone-input.component.ts
  39. 2
      ui-ngx/src/app/shared/components/time/timezone-select.component.html
  40. 4
      ui-ngx/src/app/shared/components/time/timezone-select.component.ts
  41. 65
      ui-ngx/src/assets/help/en_US/widget/lib/map/clustering_color_fn.md
  42. 45
      ui-ngx/src/assets/locale/locale.constant-de_DE.json
  43. 6
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  44. 24
      ui-ngx/src/typings/leaflet-extend-tb.d.ts
  45. 3
      ui-ngx/tsconfig.json

3
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;

3
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);
}

36
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<DownlinkMsg> 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);

18
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;
}
}

45
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<ListenableFuture<Void>> processActionForAllEdgesByTenantId(TenantId tenantId, EdgeEventType type, EdgeEventActionType actionType, EntityId entityId) {
protected List<ListenableFuture<Void>> processActionForAllEdgesByTenantId(TenantId tenantId,
EdgeEventType type,
EdgeEventActionType actionType,
EntityId entityId,
JsonNode body) {
PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE);
PageData<Edge> pageData;
List<ListenableFuture<Void>> 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;
}
}
}

6
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);

137
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<ListenableFuture<Void>> processTelemetryFromEdge(TenantId tenantId, CustomerId customerId, EntityDataProto entityData) {
log.trace("[{}] onTelemetryUpdate [{}]", tenantId, entityData);
public List<ListenableFuture<Void>> processTelemetryFromEdge(TenantId tenantId, EntityDataProto entityData) {
log.trace("[{}] processTelemetryFromEdge [{}]", tenantId, entityData);
List<ListenableFuture<Void>> 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<TbMsgMetaData, CustomerId> 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<TbMsgMetaData, CustomerId> 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<String, RuleChainId> 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<Void> 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<String, RuleChainId> 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<Void> processPostAttributes(TenantId tenantId, CustomerId customerId, EntityId entityId, TransportProtos.PostAttributeMsg msg, TbMsgMetaData metaData) {
SettableFuture<Void> futureToSet = SettableFuture.create();
JsonObject json = JsonUtils.getJsonObject(msg.getKvList());
@ -228,6 +243,7 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor {
}
private ListenableFuture<Void> 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<Void>() {
@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<AttributeKvEntry> attributes, Throwable e) {
notificationEntityService.logEntityAction(tenantId, entityId, ActionType.ATTRIBUTES_UPDATED, null,
BaseController.toException(e), scope, attributes);
}
private ListenableFuture<Void> processAttributeDeleteMsg(TenantId tenantId, EntityId entityId, AttributeDeleteMsg attributeDeleteMsg, String entityType) {
private ListenableFuture<Void> processAttributeDeleteMsg(TenantId tenantId, EntityId entityId, AttributeDeleteMsg attributeDeleteMsg,
String entityType) {
SettableFuture<Void> futureToSet = SettableFuture.create();
String scope = attributeDeleteMsg.getScope();
List<String> 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()) {

8
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() {

2
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();
}
}

40
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<EntityDataProto> allTelemetryMsgs = edgeImitator.findAllMessagesByType(EntityDataProto.class);
Assert.assertTrue(allTelemetryMsgs.isEmpty());
List<DeviceUpdateMsg> deviceUpdateMsgs = edgeImitator.findAllMessagesByType(DeviceUpdateMsg.class);
Assert.assertEquals(numberOfMsgsToSend, deviceUpdateMsgs.size());
edgeImitator.setRandomFailuresOnTimeseriesDownlink(false);
}
}

21
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<Void> 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());
}

2
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:

4
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);
}
};
}

2
pom.xml

@ -92,7 +92,7 @@
<jts.version>1.18.2</jts.version>
<bouncycastle.version>1.69</bouncycastle.version>
<winsw.version>2.0.1</winsw.version>
<postgresql.driver.version>42.2.20</postgresql.driver.version>
<postgresql.driver.version>42.5.0</postgresql.driver.version>
<sonar.exclusions>org/thingsboard/server/gen/**/*,
org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/*
</sonar.exclusions>

4
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<ObjectNode> 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;
}

2
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

117
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());
}
}

170
rule-engine/rule-engine-components/src/test/java/math/TbMathNodeTest.java → 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<TbMsg> 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<TbMsg> 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<TbMsg> 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<TbMsg> 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());
}
}

2
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});
}
}
}

2
ui-ngx/src/app/modules/home/components/event/event-table-config.ts

@ -440,6 +440,8 @@ export class EventTableConfig extends EntityTableConfig<Event, TimePageLink> {
};
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(() => {

5
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) {

5
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];

108
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: `<div style="background-color: ${parsedColor.setAlpha(0.4).toRgbString()};" class="marker-cluster tb-cluster-marker-element">` +
`<div style="background-color: ${parsedColor.setAlpha(0.9).toRgbString()};"><span>` + childCount + '</span></div></div>',
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: '<div><span>' + childCount + '</span></div>',
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<FormattedData> {

13
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,

1
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,

12
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;
}

14
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) {

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-key-settings.component.html

@ -16,6 +16,10 @@
-->
<section class="tb-widget-settings" [formGroup]="alarmsTableKeySettingsForm" fxLayout="column">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.custom-title</mat-label>
<input matInput formControlName="customTitle">
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.column-width</mat-label>
<input matInput formControlName="columnWidth">

2
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]],

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/entities-table-key-settings.component.html

@ -16,6 +16,10 @@
-->
<section class="tb-widget-settings" [formGroup]="entitiesTableKeySettingsForm" fxLayout="column">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.custom-title</mat-label>
<input matInput formControlName="customTitle">
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>widgets.table.column-width</mat-label>
<input matInput formControlName="columnWidth">

2
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]],

4
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/value-source.component.html

@ -31,8 +31,8 @@
<mat-label translate>widgets.value-source.value</mat-label>
<input matInput type="number" formControlName="value">
</mat-form-field>
<section [fxShow]="valueSourceFormGroup.get('valueSource').value === 'entityAttribute'" fxLayout="column" fxLayout.gt-xs="column"
fxLayoutGap.gt-xs="8px" fxLayoutAlign.gt-xs="start center">
<section [fxShow]="valueSourceFormGroup.get('valueSource').value === 'entityAttribute'"
fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px" fxLayoutAlign="start center">
<mat-form-field fxFlex class="mat-block">
<mat-label>{{ 'widgets.value-source.source-entity-alias' | translate }}</mat-label>
<input matInput type="text" placeholder="{{ 'entity.entity-alias' | translate }}"

24
ui-ngx/src/app/modules/home/components/widget/lib/settings/map/marker-clustering-settings.component.html

@ -64,6 +64,30 @@
{{ 'widgets.maps.cluster-markers-lazy-load' | translate }}
</mat-slide-toggle>
</fieldset>
<fieldset class="fields-group fields-group-slider">
<legend class="group-title" translate>widgets.maps.clustering-markers</legend>
<mat-expansion-panel class="tb-settings" [expanded]="markerClusteringSettingsFormGroup.get('useIconCreateFunction').value">
<mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title>
<mat-slide-toggle formControlName="useIconCreateFunction" (click)="$event.stopPropagation()"
fxLayoutAlign="center">
{{ 'widgets.maps.use-icon-create-function' | translate }}
</mat-slide-toggle>
</mat-panel-title>
<mat-panel-description fxLayoutAlign="end center" fxHide.xs translate>
widget-config.advanced-settings
</mat-panel-description>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<tb-js-func formControlName="clusterMarkerFunction"
[globalVariables]="functionScopeVariables"
[functionArgs]="['data', 'childCount']"
functionTitle="{{ 'widgets.maps.marker-color-function' | translate }}"
helpId="widget/lib/map/clustering_color_fn">
</tb-js-func>
</ng-template>
</mat-expansion-panel>
</fieldset>
</ng-template>
</mat-expansion-panel>
</fieldset>

6
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();

8
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;
}

2
ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<mat-form-field [formGroup]="selectEntityFormGroup" class="mat-block">
<mat-form-field [formGroup]="selectEntityFormGroup" class="mat-block" [appearance]="appearance">
<input matInput type="text" placeholder="{{ entityText | translate }}"
#entityInput
formControlName="entity"

4
ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts

@ -25,6 +25,7 @@ import {
Output,
ViewChild
} from '@angular/core';
import { MatFormFieldAppearance } from '@angular/material/form-field/form-field';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { merge, Observable, of, Subject } from 'rxjs';
import { catchError, debounceTime, map, share, switchMap, tap } from 'rxjs/operators';
@ -96,6 +97,9 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit
@Input()
requiredText: string;
@Input()
appearance: MatFormFieldAppearance = 'legacy';
private requiredValue: boolean;
get required(): boolean {
return this.requiredValue;

7
ui-ngx/src/app/shared/components/phone-input.component.ts

@ -94,6 +94,9 @@ export class PhoneInputComponent implements OnInit, ControlValueAccessor, Valida
set isLoad(value) {
if (this.isLoading) {
this.isLoading = value;
if (this.defaultCountry) {
this.getFlagAndPhoneNumberData(this.defaultCountry);
}
if (this.phoneFormGroup) {
this.defineCountryFromNumber(this.phoneFormGroup.get('phoneNumber').value);
}
@ -125,12 +128,10 @@ export class PhoneInputComponent implements OnInit, ControlValueAccessor, Valida
this.validators.push(Validators.required);
}
this.phoneFormGroup = this.fb.group({
country: [this.defaultCountry, []],
country: [null, []],
phoneNumber: [null, this.validators]
});
this.flagIcon = this.getFlagIcon(this.phoneFormGroup.get('country').value);
this.changeSubscriptions.push(this.phoneFormGroup.get('phoneNumber').valueChanges.subscribe(value => {
this.updateModel();
this.defineCountryFromNumber(value);

2
ui-ngx/src/app/shared/components/time/timezone-select.component.html

@ -15,7 +15,7 @@
limitations under the License.
-->
<mat-form-field [formGroup]="selectTimezoneFormGroup" fxFlex class="mat-block">
<mat-form-field [formGroup]="selectTimezoneFormGroup" fxFlex class="mat-block" [appearance]="appearance">
<mat-label translate>timezone.timezone</mat-label>
<input matInput type="text" placeholder="{{ 'timezone.select-timezone' | translate }}"
#timezoneInput

4
ui-ngx/src/app/shared/components/time/timezone-select.component.ts

@ -16,6 +16,7 @@
import { AfterViewInit, Component, forwardRef, Input, NgZone, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms';
import { MatFormFieldAppearance } from '@angular/material/form-field/form-field';
import { Observable, of } from 'rxjs';
import { map, mergeMap, share, tap } from 'rxjs/operators';
import { Store } from '@ngrx/store';
@ -44,6 +45,9 @@ export class TimezoneSelectComponent implements ControlValueAccessor, OnInit, Af
defaultTimezoneId: string = null;
@Input()
appearance: MatFormFieldAppearance = 'legacy';
@Input()
set defaultTimezone(timezone: string) {
if (this.defaultTimezoneId !== timezone) {

65
ui-ngx/src/assets/help/en_US/widget/lib/map/clustering_color_fn.md

@ -0,0 +1,65 @@
#### Clustering marker function
<div class="divider"></div>
<br/>
*function (data, childCount): string*
A JavaScript function used to compute clustering marker color.
**Parameters:**
<ul>
<li><b>data:</b> <code><a href="https://github.com/thingsboard/thingsboard/blob/5bb6403407aa4898084832d6698aa9ea6d484889/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts#L108" target="_blank">FormattedData[]</a></code>
- the array of total markers contained within each cluster.<br/>
Represents basic entity properties (ex. <code>entityId</code>, <code>entityName</code>)<br/>and provides access to other entity attributes/timeseries declared in widget datasource configuration.
</li>
<li><b>childCount:</b> <code>number</code> - the total number of markers contained within that cluster
</li>
</ul>
**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.
<div class="divider"></div>
##### Examples
<ul>
<li>
Calculate color depending on temperature telemetry value:
</li>
```javascript
let customColor;
for (let markerData of data) {
if (markerData.temperature > 40) {
customColor = 'red'
}
}
return customColor ? customColor : 'green';
{:copy-code}
```
<li>
Calculate color depending on childCount:
</li>
```javascript
if (childCount < 10) {
return 'green';
} else if (childCount < 100) {
return 'yellow';
} else {
return 'red';
}
{:copy-code}
```
</ul>
<br>
<br>

45
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"
}

6
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",

24
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;
}
}

3
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/*"],

Loading…
Cancel
Save