From e0fd611c768d98ef6475f9872aaa08bb533bad82 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Fri, 13 Dec 2024 14:35:02 +0100 Subject: [PATCH 01/63] used TbMsgProto instead of ByteString --- .../actors/ruleChain/DefaultTbContext.java | 5 +- .../RuleChainActorMessageProcessor.java | 2 +- .../RuleNodeActorMessageProcessor.java | 2 +- .../device/DeviceProvisionServiceImpl.java | 3 +- ...riginatorIdTbRuleEngineSubmitStrategy.java | 7 +- ...TbRuleEngineProcessingStrategyFactory.java | 6 +- .../TbRuleEngineQueueConsumerManager.java | 14 +++- .../rpc/DefaultTbRuleEngineRpcService.java | 2 +- .../DefaultRuleEngineCallService.java | 2 +- .../actors/rule/DefaultTbContextTest.java | 12 +-- .../controller/TenantControllerTest.java | 3 +- .../TbRuleEngineQueueConsumerManagerTest.java | 2 +- .../ruleengine/TbRuleEngineStrategyTest.java | 2 +- .../DefaultTbRuleEngineRpcServiceTest.java | 2 +- .../DefaultRuleEngineCallServiceTest.java | 2 +- .../thingsboard/server/common/msg/TbMsg.java | 82 ++++++++++--------- common/proto/src/main/proto/queue.proto | 8 +- .../common/TbRuleEngineProducerService.java | 2 +- 18 files changed, 90 insertions(+), 68 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 0070eba4a1..6ebcc1843e 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -230,7 +230,8 @@ public class DefaultTbContext implements TbContext { TransportProtos.ToRuleEngineMsg msg = TransportProtos.ToRuleEngineMsg.newBuilder() .setTenantIdMSB(getTenantId().getId().getMostSignificantBits()) .setTenantIdLSB(getTenantId().getId().getLeastSignificantBits()) - .setTbMsg(TbMsg.toByteString(tbMsg)).build(); + .setTbMsgProto(TbMsg.toProto(tbMsg)) + .build(); mainCtx.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), msg, callback); } @@ -309,7 +310,7 @@ public class DefaultTbContext implements TbContext { TransportProtos.ToRuleEngineMsg.Builder msg = TransportProtos.ToRuleEngineMsg.newBuilder() .setTenantIdMSB(getTenantId().getId().getMostSignificantBits()) .setTenantIdLSB(getTenantId().getId().getLeastSignificantBits()) - .setTbMsg(TbMsg.toByteString(tbMsg)) + .setTbMsgProto(TbMsg.toProto(tbMsg)) .addAllRelationTypes(relationTypes); if (failureMessage != null) { msg.setFailureMessage(failureMessage); diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java index 460da228c3..917066cd2d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleChainActorMessageProcessor.java @@ -370,7 +370,7 @@ public class RuleChainActorMessageProcessor extends ComponentMsgProcessor(tbMsg.getId(), msg), callback); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByOriginatorIdTbRuleEngineSubmitStrategy.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByOriginatorIdTbRuleEngineSubmitStrategy.java index 1ee6df8361..3c11095894 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByOriginatorIdTbRuleEngineSubmitStrategy.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByOriginatorIdTbRuleEngineSubmitStrategy.java @@ -34,7 +34,12 @@ public class SequentialByOriginatorIdTbRuleEngineSubmitStrategy extends Sequenti @Override protected EntityId getEntityId(TransportProtos.ToRuleEngineMsg msg) { try { - MsgProtos.TbMsgProto proto = MsgProtos.TbMsgProto.parseFrom(msg.getTbMsg()); + MsgProtos.TbMsgProto proto; + if (msg.getTbMsg().isEmpty()) { + proto = msg.getTbMsgProto(); + } else { + proto = MsgProtos.TbMsgProto.parseFrom(msg.getTbMsg()); + } return EntityIdFactory.getByTypeAndUuid(proto.getEntityType(), new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB())); } catch (InvalidProtocolBufferException e) { log.warn("[{}] Failed to parse TbMsg: {}", queueName, msg); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java index 4712497524..597bcef098 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java @@ -125,7 +125,7 @@ public class TbRuleEngineProcessingStrategyFactory { } log.debug("[{}] Going to reprocess {} messages", queueName, toReprocess.size()); if (log.isTraceEnabled()) { - toReprocess.forEach((id, msg) -> log.trace("Going to reprocess [{}]: {}", id, TbMsg.fromBytes(result.getQueueName(), msg.getValue().getTbMsg().toByteArray(), TbMsgCallback.EMPTY))); + toReprocess.forEach((id, msg) -> log.trace("Going to reprocess [{}]: {}", id, TbMsg.fromProto(result.getQueueName(), msg.getValue().getTbMsgProto(), msg.getValue().getTbMsg(), TbMsgCallback.EMPTY))); } if (pauseBetweenRetries > 0) { try { @@ -164,10 +164,10 @@ public class TbRuleEngineProcessingStrategyFactory { log.debug("[{}] Reprocessing skipped for {} failed and {} timeout messages", queueName, result.getFailedMap().size(), result.getPendingMap().size()); } if (log.isTraceEnabled()) { - result.getFailedMap().forEach((id, msg) -> log.trace("Failed messages [{}]: {}", id, TbMsg.fromBytes(result.getQueueName(), msg.getValue().getTbMsg().toByteArray(), TbMsgCallback.EMPTY))); + result.getFailedMap().forEach((id, msg) -> log.trace("Failed messages [{}]: {}", id, TbMsg.fromProto(result.getQueueName(), msg.getValue().getTbMsgProto(), msg.getValue().getTbMsg(), TbMsgCallback.EMPTY))); } if (log.isTraceEnabled()) { - result.getPendingMap().forEach((id, msg) -> log.trace("Timeout messages [{}]: {}", id, TbMsg.fromBytes(result.getQueueName(), msg.getValue().getTbMsg().toByteArray(), TbMsgCallback.EMPTY))); + result.getPendingMap().forEach((id, msg) -> log.trace("Timeout messages [{}]: {}", id, TbMsg.fromProto(result.getQueueName(), msg.getValue().getTbMsgProto(), msg.getValue().getTbMsg(), TbMsgCallback.EMPTY))); } return new TbRuleEngineProcessingDecision(true, null); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java index c2823d3c00..231d7b205d 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java @@ -170,7 +170,7 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager relationTypes; @@ -199,7 +199,7 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager> pending : map.entrySet()) { ToRuleEngineMsg tmp = pending.getValue().getValue(); - TbMsg tmpMsg = TbMsg.fromBytes(config.getName(), tmp.getTbMsg().toByteArray(), TbMsgCallback.EMPTY); + TbMsg tmpMsg = TbMsg.fromProto(config.getName(), tmp.getTbMsgProto(), tmp.getTbMsg(), TbMsgCallback.EMPTY); RuleNodeInfo ruleNodeInfo = ctx.getLastVisitedRuleNode(pending.getKey()); if (printAll) { log.trace("[{}][{}] {} to process message: {}, Last Rule Node: {}", queueKey, TenantId.fromUUID(new UUID(tmp.getTenantIdMSB(), tmp.getTenantIdLSB())), prefix, tmpMsg, ruleNodeInfo); @@ -228,7 +228,13 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager msg : msgs) { try { - MsgProtos.TbMsgProto tbMsgProto = MsgProtos.TbMsgProto.parseFrom(msg.getValue().getTbMsg().toByteArray()); + MsgProtos.TbMsgProto tbMsgProto; + if (msg.getValue().getTbMsg().isEmpty()) { + tbMsgProto = msg.getValue().getTbMsgProto(); + } else { + tbMsgProto = MsgProtos.TbMsgProto.parseFrom(msg.getValue().getTbMsg()); + } + EntityId originator = EntityIdFactory.getByTypeAndUuid(tbMsgProto.getEntityType(), new UUID(tbMsgProto.getEntityIdMSB(), tbMsgProto.getEntityIdLSB())); TopicPartitionInfo tpi = ctx.getPartitionService().resolve(ServiceType.TB_RULE_ENGINE, config.getName(), TenantId.SYS_TENANT_ID, originator); diff --git a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java index debc1cb987..80b3bd7a37 100644 --- a/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcService.java @@ -132,7 +132,7 @@ public class DefaultTbRuleEngineRpcService implements TbRuleEngineDeviceRpcServi TransportProtos.RestApiCallResponseMsgProto msg = TransportProtos.RestApiCallResponseMsgProto.newBuilder() .setRequestIdMSB(requestId.getMostSignificantBits()) .setRequestIdLSB(requestId.getLeastSignificantBits()) - .setResponse(TbMsg.toByteString(tbMsg)) + .setResponseProto(TbMsg.toProto(tbMsg)) .build(); clusterService.pushNotificationToCore(serviceId, msg, null); } diff --git a/application/src/main/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallService.java b/application/src/main/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallService.java index 6f4d18b94e..e435c8ad36 100644 --- a/application/src/main/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallService.java +++ b/application/src/main/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallService.java @@ -73,7 +73,7 @@ public class DefaultRuleEngineCallService implements RuleEngineCallService { UUID requestId = new UUID(restApiCallResponseMsg.getRequestIdMSB(), restApiCallResponseMsg.getRequestIdLSB()); Consumer consumer = requests.remove(requestId); if (consumer != null) { - consumer.accept(TbMsg.fromBytes(null, restApiCallResponseMsg.getResponse().toByteArray(), TbMsgCallback.EMPTY)); + consumer.accept(TbMsg.fromProto(null, restApiCallResponseMsg.getResponseProto(), restApiCallResponseMsg.getResponse(), TbMsgCallback.EMPTY)); } else { log.trace("[{}] Unknown or stale rest api call response received", requestId); } diff --git a/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java b/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java index 21a4a45c6f..4370b98b2d 100644 --- a/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java +++ b/application/src/test/java/org/thingsboard/server/actors/rule/DefaultTbContextTest.java @@ -644,11 +644,11 @@ class DefaultTbContextTest { ToRuleEngineMsg actualToRuleEngineMsg = toRuleEngineMsgCaptor.getValue(); assertThat(actualToRuleEngineMsg).usingRecursiveComparison() - .ignoringFields("tbMsg_") + .ignoringFields("tbMsgProto_") .isEqualTo(ToRuleEngineMsg.newBuilder() .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) - .setTbMsg(TbMsg.toByteString(expectedTbMsg)) + .setTbMsgProto(TbMsg.toProto(expectedTbMsg)) .addAllRelationTypes(List.of(connectionType)).build()); var simpleTbQueueCallback = simpleTbQueueCallbackCaptor.getValue(); @@ -701,11 +701,11 @@ class DefaultTbContextTest { ToRuleEngineMsg actualToRuleEngineMsg = toRuleEngineMsgCaptor.getValue(); assertThat(actualToRuleEngineMsg).usingRecursiveComparison() - .ignoringFields("tbMsg_") + .ignoringFields("tbMsgProto_") .isEqualTo(ToRuleEngineMsg.newBuilder() .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) - .setTbMsg(TbMsg.toByteString(expectedTbMsg)) + .setTbMsgProto(TbMsg.toProto(expectedTbMsg)) .build()); var simpleTbQueueCallback = simpleTbQueueCallbackCaptor.getValue(); @@ -883,11 +883,11 @@ class DefaultTbContextTest { ToRuleEngineMsg actualToRuleEngineMsg = toRuleEngineMsgCaptor.getValue(); assertThat(actualToRuleEngineMsg).usingRecursiveComparison() - .ignoringFields("tbMsg_") + .ignoringFields("tbMsgProto_.id_") .isEqualTo(ToRuleEngineMsg.newBuilder() .setTenantIdMSB(TENANT_ID.getId().getMostSignificantBits()) .setTenantIdLSB(TENANT_ID.getId().getLeastSignificantBits()) - .setTbMsg(TbMsg.toByteString(expectedTbMsg)) + .setTbMsgProto(TbMsg.toProto(expectedTbMsg)) .setFailureMessage(EXCEPTION_MSG) .addAllRelationTypes(List.of(TbNodeConnectionType.FAILURE)).build()); diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java index c8cc72c5b5..dbf40c0e30 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java @@ -747,7 +747,8 @@ public class TenantControllerTest extends AbstractControllerTest { TransportProtos.ToRuleEngineMsg msg = TransportProtos.ToRuleEngineMsg.newBuilder() .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()) - .setTbMsg(TbMsg.toByteString(tbMsg)).build(); + .setTbMsgProto(TbMsg.toProto(tbMsg)) + .build(); tbClusterService.pushMsgToRuleEngine(tpi, tbMsg.getId(), msg, null); return tbMsg; } diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java index 66e3de13d1..bff5d165f4 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManagerTest.java @@ -738,7 +738,7 @@ public class TbRuleEngineQueueConsumerManagerTest { .setTenantIdMSB(tenantId.getMostSignificantBits()) .setTenantIdLSB(tenantId.getLeastSignificantBits()) .addRelationTypes("Success") - .setTbMsg(TbMsg.toByteString(tbMsg)) + .setTbMsgProto(TbMsg.toProto(tbMsg)) .build()); } diff --git a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java index 098c622e31..435304fdef 100644 --- a/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java +++ b/application/src/test/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineStrategyTest.java @@ -263,7 +263,7 @@ public class TbRuleEngineStrategyTest { .setTenantIdMSB(tenantId.getMostSignificantBits()) .setTenantIdLSB(tenantId.getLeastSignificantBits()) .addRelationTypes("Success") - .setTbMsg(TbMsg.toByteString(tbMsg)) + .setTbMsgProto(TbMsg.toProto(tbMsg)) .build()); } diff --git a/application/src/test/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcServiceTest.java b/application/src/test/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcServiceTest.java index 2cab5f991a..ea0b3b91f5 100644 --- a/application/src/test/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/rpc/DefaultTbRuleEngineRpcServiceTest.java @@ -50,7 +50,7 @@ class DefaultTbRuleEngineRpcServiceTest { var restApiCallResponseMsgProto = TransportProtos.RestApiCallResponseMsgProto.newBuilder() .setRequestIdMSB(requestId.getMostSignificantBits()) .setRequestIdLSB(requestId.getLeastSignificantBits()) - .setResponse(TbMsg.toByteString(msg)) + .setResponseProto(TbMsg.toProto(msg)) .build(); // WHEN diff --git a/application/src/test/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallServiceTest.java b/application/src/test/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallServiceTest.java index c49149bd07..a4ee9cda71 100644 --- a/application/src/test/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/ruleengine/DefaultRuleEngineCallServiceTest.java @@ -132,7 +132,7 @@ public class DefaultRuleEngineCallServiceTest { private TransportProtos.RestApiCallResponseMsgProto getResponse(UUID requestId, TbMsg msg) { return TransportProtos.RestApiCallResponseMsgProto.newBuilder() - .setResponse(TbMsg.toByteString(msg)) + .setResponseProto(TbMsg.toProto(msg)) .setRequestIdMSB(requestId.getMostSignificantBits()) .setRequestIdLSB(requestId.getLeastSignificantBits()) .build(); diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java index 47fb80c90e..88e7c6b691 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java @@ -371,11 +371,7 @@ public final class TbMsg implements Serializable { this.callback = Objects.requireNonNullElse(callback, TbMsgCallback.EMPTY); } - public static ByteString toByteString(TbMsg msg) { - return ByteString.copyFrom(toByteArray(msg)); - } - - public static byte[] toByteArray(TbMsg msg) { + public static MsgProtos.TbMsgProto toProto(TbMsg msg) { MsgProtos.TbMsgProto.Builder builder = MsgProtos.TbMsgProto.newBuilder(); builder.setId(msg.getId().toString()); builder.setTs(msg.getTs()); @@ -415,47 +411,55 @@ public final class TbMsg implements Serializable { } builder.setCtx(msg.ctx.toProto()); - return builder.build().toByteArray(); + return builder.build(); } - public static TbMsg fromBytes(String queueName, byte[] data, TbMsgCallback callback) { + //TODO: added for processing old messages from queue, should be removed after release + @Deprecated(forRemoval = true) + public static TbMsg fromProto(String queueName, MsgProtos.TbMsgProto proto, ByteString data, TbMsgCallback callback) { try { - MsgProtos.TbMsgProto proto = MsgProtos.TbMsgProto.parseFrom(data); - TbMsgMetaData metaData = new TbMsgMetaData(proto.getMetaData().getDataMap()); - EntityId entityId = EntityIdFactory.getByTypeAndUuid(proto.getEntityType(), new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB())); - CustomerId customerId = null; - RuleChainId ruleChainId = null; - RuleNodeId ruleNodeId = null; - UUID correlationId = null; - Integer partition = null; - if (proto.getCustomerIdMSB() != 0L && proto.getCustomerIdLSB() != 0L) { - customerId = new CustomerId(new UUID(proto.getCustomerIdMSB(), proto.getCustomerIdLSB())); - } - if (proto.getRuleChainIdMSB() != 0L && proto.getRuleChainIdLSB() != 0L) { - ruleChainId = new RuleChainId(new UUID(proto.getRuleChainIdMSB(), proto.getRuleChainIdLSB())); - } - if (proto.getRuleNodeIdMSB() != 0L && proto.getRuleNodeIdLSB() != 0L) { - ruleNodeId = new RuleNodeId(new UUID(proto.getRuleNodeIdMSB(), proto.getRuleNodeIdLSB())); - } - if (proto.getCorrelationIdMSB() != 0L && proto.getCorrelationIdLSB() != 0L) { - correlationId = new UUID(proto.getCorrelationIdMSB(), proto.getCorrelationIdLSB()); - partition = proto.getPartition(); + if (!data.isEmpty()) { + proto = MsgProtos.TbMsgProto.parseFrom(data); } - - TbMsgProcessingCtx ctx; - if (proto.hasCtx()) { - ctx = TbMsgProcessingCtx.fromProto(proto.getCtx()); - } else { - // Backward compatibility with unprocessed messages fetched from queue after update. - ctx = new TbMsgProcessingCtx(proto.getRuleNodeExecCounter()); - } - - TbMsgDataType dataType = TbMsgDataType.values()[proto.getDataType()]; - return new TbMsg(queueName, UUID.fromString(proto.getId()), proto.getTs(), null, proto.getType(), entityId, customerId, - metaData, dataType, proto.getData(), ruleChainId, ruleNodeId, correlationId, partition, ctx, callback); } catch (InvalidProtocolBufferException e) { throw new IllegalStateException("Could not parse protobuf for TbMsg", e); } + return fromProto(queueName, proto, callback); + } + + public static TbMsg fromProto(String queueName, MsgProtos.TbMsgProto proto, TbMsgCallback callback) { + TbMsgMetaData metaData = new TbMsgMetaData(proto.getMetaData().getDataMap()); + EntityId entityId = EntityIdFactory.getByTypeAndUuid(proto.getEntityType(), new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB())); + CustomerId customerId = null; + RuleChainId ruleChainId = null; + RuleNodeId ruleNodeId = null; + UUID correlationId = null; + Integer partition = null; + if (proto.getCustomerIdMSB() != 0L && proto.getCustomerIdLSB() != 0L) { + customerId = new CustomerId(new UUID(proto.getCustomerIdMSB(), proto.getCustomerIdLSB())); + } + if (proto.getRuleChainIdMSB() != 0L && proto.getRuleChainIdLSB() != 0L) { + ruleChainId = new RuleChainId(new UUID(proto.getRuleChainIdMSB(), proto.getRuleChainIdLSB())); + } + if (proto.getRuleNodeIdMSB() != 0L && proto.getRuleNodeIdLSB() != 0L) { + ruleNodeId = new RuleNodeId(new UUID(proto.getRuleNodeIdMSB(), proto.getRuleNodeIdLSB())); + } + if (proto.getCorrelationIdMSB() != 0L && proto.getCorrelationIdLSB() != 0L) { + correlationId = new UUID(proto.getCorrelationIdMSB(), proto.getCorrelationIdLSB()); + partition = proto.getPartition(); + } + + TbMsgProcessingCtx ctx; + if (proto.hasCtx()) { + ctx = TbMsgProcessingCtx.fromProto(proto.getCtx()); + } else { + // Backward compatibility with unprocessed messages fetched from queue after update. + ctx = new TbMsgProcessingCtx(proto.getRuleNodeExecCounter()); + } + + TbMsgDataType dataType = TbMsgDataType.values()[proto.getDataType()]; + return new TbMsg(queueName, UUID.fromString(proto.getId()), proto.getTs(), null, proto.getType(), entityId, customerId, + metaData, dataType, proto.getData(), ruleChainId, ruleNodeId, correlationId, partition, ctx, callback); } public TbMsg copyWithRuleChainId(RuleChainId ruleChainId) { diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 228a4039d2..ceef0914a7 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -20,6 +20,8 @@ package transport; option java_package = "org.thingsboard.server.gen.transport"; option java_outer_classname = "TransportProtos"; +import "tbmsg.proto"; + /** * Common data structures */ @@ -108,7 +110,8 @@ message SessionInfoProto { message RestApiCallResponseMsgProto { int64 requestIdMSB = 1; int64 requestIdLSB = 2; - bytes response = 5; + bytes response = 5 [deprecated = true]; + msgqueue.TbMsgProto responseProto = 6; } enum SessionEvent { @@ -1556,9 +1559,10 @@ message ToEdgeEventNotificationMsg { message ToRuleEngineMsg { int64 tenantIdMSB = 1; int64 tenantIdLSB = 2; - bytes tbMsg = 3; + bytes tbMsg = 3 [deprecated = true]; repeated string relationTypes = 4; string failureMessage = 5; + msgqueue.TbMsgProto tbMsgProto = 6; } message ToRuleEngineNotificationMsg { diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/TbRuleEngineProducerService.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/TbRuleEngineProducerService.java index 49b40e3a6d..aefce4e176 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/TbRuleEngineProducerService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/TbRuleEngineProducerService.java @@ -65,7 +65,7 @@ public class TbRuleEngineProducerService { log.trace("[{}][{}] Pushing to topic {} message {}", tenantId, tbMsg.getOriginator(), tpi.getFullTopicName(), tbMsg); } ToRuleEngineMsg msg = ToRuleEngineMsg.newBuilder() - .setTbMsg(TbMsg.toByteString(tbMsg)) + .setTbMsgProto(TbMsg.toProto(tbMsg)) .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) .setTenantIdLSB(tenantId.getId().getLeastSignificantBits()).build(); producer.send(tpi, new TbProtoQueueMsg<>(tbMsg.getId(), msg), callback); From f87aa0b1425f03ce9ec8ce64b34a7ad6b0ff5466 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 28 Jan 2025 14:35:21 +0100 Subject: [PATCH 02/63] Added new relation between rule chains if RuleChainInput node is used --- .../common/data/relation/EntityRelation.java | 1 + .../server/dao/rule/BaseRuleChainService.java | 35 +++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java index 62b5caafd3..ff45c01500 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelation.java @@ -41,6 +41,7 @@ public class EntityRelation implements HasVersion, Serializable { public static final String EDGE_TYPE = "ManagedByEdge"; public static final String CONTAINS_TYPE = "Contains"; public static final String MANAGES_TYPE = "Manages"; + public static final String USES_TYPE = "Uses"; @Setter private EntityId from; diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 0e51cf256a..1ccc5fa95f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -71,11 +71,13 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.UUID; import java.util.function.Function; import java.util.stream.Collectors; @@ -196,11 +198,18 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } } } - + RuleChainId ruleChainId = ruleChain.getId(); List updatedRuleNodes = new ArrayList<>(); List existingRuleNodes = getRuleChainNodes(tenantId, ruleChainMetaData.getRuleChainId()); for (RuleNode existingNode : existingRuleNodes) { relationService.deleteEntityRelations(tenantId, existingNode.getId()); + if (existingNode.getType().equals("org.thingsboard.rule.engine.flow.TbRuleChainInputNode")) { + if (existingNode.getConfiguration().has("ruleChainId")) { + RuleChainId targetRuleChainId = extractRuleChainIdFromInputNode(existingNode); + var relation = createRuleChainInputRelation(ruleChainId, targetRuleChainId); + relationService.deleteRelation(tenantId, relation); + } + } Integer index = ruleNodeIndexMap.get(existingNode.getId()); RuleNode newRuleNode = null; if (index != null) { @@ -212,7 +221,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC } updatedRuleNodes.add(new RuleNodeUpdateResult(existingNode, newRuleNode)); } - RuleChainId ruleChainId = ruleChain.getId(); + if (nodes != null) { long now = System.currentTimeMillis(); for (RuleNode node : toAddOrUpdate) { @@ -225,6 +234,13 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC RuleNode savedNode = ruleNodeDao.save(tenantId, node); relations.add(new EntityRelation(ruleChainMetaData.getRuleChainId(), savedNode.getId(), EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); + if (node.getType().equals("org.thingsboard.rule.engine.flow.TbRuleChainInputNode")) { + if (node.getConfiguration().has("ruleChainId")) { + RuleChainId targetRuleChainId = extractRuleChainIdFromInputNode(node); + var relation = createRuleChainInputRelation(ruleChainId, targetRuleChainId); + relations.add(relation); + } + } int index = nodes.indexOf(node); nodes.set(index, savedNode); ruleNodeIndexMap.put(savedNode.getId(), index); @@ -295,6 +311,21 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC return RuleChainUpdateResult.successful(updatedRuleNodes); } + private EntityRelation createRuleChainInputRelation(RuleChainId ruleChainId, RuleChainId targetRuleChainId) { + EntityRelation relation = new EntityRelation(); + relation.setFrom(ruleChainId); + relation.setTo(targetRuleChainId); + relation.setType(EntityRelation.USES_TYPE); + relation.setTypeGroup(RelationTypeGroup.COMMON); + return relation; + } + + private RuleChainId extractRuleChainIdFromInputNode(RuleNode node) { + JsonNode configuration = node.getConfiguration(); + UUID targetUuid = UUID.fromString(configuration.get("ruleChainId").asText()); + return new RuleChainId(targetUuid); + } + @Override public RuleChainMetaData loadRuleChainMetaData(TenantId tenantId, RuleChainId ruleChainId) { Validator.validateId(ruleChainId, "Incorrect rule chain id."); From f06fc5b27597500919545e09ea2b7b6d1a26b2d5 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 28 Jan 2025 14:38:35 +0100 Subject: [PATCH 03/63] imports optimization --- .../org/thingsboard/server/dao/rule/BaseRuleChainService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 1ccc5fa95f..eb78cf316e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -71,7 +71,6 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; From bbdeda95580c5903c1cff4981ed90671168c96c6 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Sun, 9 Mar 2025 19:48:38 +0100 Subject: [PATCH 04/63] added tests --- .../dao/service/RuleChainServiceTest.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java index 86b76bd5a5..8b5ebd4306 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java @@ -16,6 +16,7 @@ package org.thingsboard.server.dao.service; import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Assert; import org.junit.Test; import org.junit.jupiter.api.Assertions; @@ -28,12 +29,14 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; import java.io.IOException; @@ -44,6 +47,7 @@ import java.util.UUID; import java.util.function.Function; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.thingsboard.server.common.data.relation.EntityRelation.USES_TYPE; /** * Created by igor on 3/13/18. @@ -55,6 +59,8 @@ public class RuleChainServiceTest extends AbstractServiceTest { EdgeService edgeService; @Autowired RuleChainService ruleChainService; + @Autowired + RelationService relationService; private IdComparator idComparator = new IdComparator<>(); private IdComparator ruleNodeIdComparator = new IdComparator<>(); @@ -354,6 +360,66 @@ public class RuleChainServiceTest extends AbstractServiceTest { Assert.assertTrue(ruleChainById.isRoot()); } + @Test + public void testSaveRuleChainWithInputNode() { + RuleChain toRuleChain = new RuleChain(); + toRuleChain.setName("To Rule Chain"); + toRuleChain.setTenantId(tenantId); + RuleChain savedToRuleChain = ruleChainService.saveRuleChain(toRuleChain); + + RuleChain fromRuleChain = new RuleChain(); + fromRuleChain.setName("From RuleChain"); + fromRuleChain.setTenantId(tenantId); + RuleChain savedFromRuleChain = ruleChainService.saveRuleChain(fromRuleChain); + + RuleChainMetaData ruleChainMetaData = new RuleChainMetaData(); + ruleChainMetaData.setRuleChainId(savedFromRuleChain.getId()); + + RuleNode ruleNode = new RuleNode(); + ruleNode.setName("Input node"); + ruleNode.setType("org.thingsboard.rule.engine.flow.TbRuleChainInputNode"); + ObjectNode configuration = JacksonUtil.newObjectNode(); + configuration.put("ruleChainId", savedToRuleChain.getId().toString()); + ruleNode.setConfiguration(configuration); + + List ruleNodes = new ArrayList<>(); + ruleNodes.add(ruleNode); + ruleChainMetaData.setFirstNodeIndex(0); + ruleChainMetaData.setNodes(ruleNodes); + + ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData, Function.identity()); + + List relations = relationService.findByFromAndType(tenantId, savedFromRuleChain.getId(), USES_TYPE, RelationTypeGroup.COMMON); + Assert.assertEquals(1, relations.size()); + EntityRelation usesRelation = relations.get(0); + Assert.assertEquals(savedFromRuleChain.getId(), usesRelation.getFrom()); + Assert.assertEquals(savedToRuleChain.getId(), usesRelation.getTo()); + + RuleChain newToRuleChain = new RuleChain(); + newToRuleChain.setName("New To Rule Chain"); + newToRuleChain.setTenantId(tenantId); + RuleChain savedNewToRuleChain = ruleChainService.saveRuleChain(newToRuleChain); + + RuleNode newRuleNode = new RuleNode(); + newRuleNode.setName("Input node"); + newRuleNode.setType("org.thingsboard.rule.engine.flow.TbRuleChainInputNode"); + ObjectNode newConfiguration = JacksonUtil.newObjectNode(); + configuration.put("ruleChainId", savedNewToRuleChain.getId().toString()); + newRuleNode.setConfiguration(newConfiguration); + + List newRuleNodes = new ArrayList<>(); + newRuleNodes.add(newRuleNode); + RuleChainMetaData foundRuleChainMetaData = ruleChainService.loadRuleChainMetaData(tenantId, ruleChainMetaData.getRuleChainId()); + foundRuleChainMetaData.setNodes(newRuleNodes); + ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData, Function.identity()); + + List newRelations = relationService.findByFromAndType(tenantId, savedFromRuleChain.getId(), USES_TYPE, RelationTypeGroup.COMMON); + Assert.assertEquals(1, relations.size()); + EntityRelation newUsesRelation = newRelations.get(0); + Assert.assertEquals(savedFromRuleChain.getId(), newUsesRelation.getFrom()); + Assert.assertEquals(savedNewToRuleChain.getId(), newUsesRelation.getTo()); + } + private RuleChainId saveRuleChainAndSetAutoAssignToEdge(String name) { RuleChain edgeRuleChain = new RuleChain(); edgeRuleChain.setTenantId(tenantId); From 37482b51f33f7e4003ffddc534c98aa10c1f31e5 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Tue, 11 Mar 2025 12:33:05 +0100 Subject: [PATCH 05/63] added upgrade script --- .../update/DefaultDataUpdateService.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index e8622fae37..27e0f6adc2 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -24,23 +24,36 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageDataIterable; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.FilterPredicateValue; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.sql.JpaExecutorService; +import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.RuleNodeClassInfo; +import org.thingsboard.server.service.install.DbUpgradeExecutorService; import org.thingsboard.server.service.install.InstallScripts; import org.thingsboard.server.utils.TbNodeUpgradeUtils; import java.util.ArrayList; import java.util.List; +import java.util.UUID; import java.util.concurrent.ExecutionException; +import static org.thingsboard.server.common.data.relation.EntityRelation.USES_TYPE; + @Service @Profile("install") @Slf4j @@ -52,6 +65,12 @@ public class DefaultDataUpdateService implements DataUpdateService { @Autowired private RuleChainService ruleChainService; + @Autowired + private RelationService relationService; + + @Autowired + private TenantService tenantService; + @Autowired private ComponentDiscoveryService componentDiscoveryService; @@ -61,13 +80,66 @@ public class DefaultDataUpdateService implements DataUpdateService { @Autowired private InstallScripts installScripts; + @Autowired + private DbUpgradeExecutorService executorService; + @Override public void updateData() throws Exception { log.info("Updating data ..."); //TODO: should be cleaned after each release + inputNodesUpdater.updateEntities(); log.info("Data updated."); } + //TODO: should be removed after release + private final PaginatedUpdater inputNodesUpdater = new PaginatedUpdater<>() { + @Override + protected String getName() { + return "Input nodes updater"; + } + + @Override + protected PageData findEntities(String type, PageLink pageLink) { + return tenantService.findTenants(pageLink); + } + + @Override + protected void updateEntity(Tenant tenant) { + TenantId tenantId = tenant.getId(); + try { + var inputNodes = ruleChainService.findRuleNodesByTenantIdAndType(tenantId, "org.thingsboard.rule.engine.flow.TbRuleChainInputNode"); + var resultFutures = inputNodes.stream().map(ruleNode -> { + try { + JsonNode id = ruleNode.getConfiguration().get("ruleChainId"); + if (id != null) { + RuleChainId toRuleChainId = new RuleChainId(UUID.fromString(id.asText())); + RuleChainId fromRuleChainId = ruleNode.getRuleChainId(); + var isExistFuture = relationService.checkRelationAsync(null, fromRuleChainId, toRuleChainId, USES_TYPE, RelationTypeGroup.COMMON); + Futures.transformAsync(isExistFuture, isExist -> { + if (!isExist) { + EntityRelation relation = new EntityRelation(); + relation.setFrom(fromRuleChainId); + relation.setTo(toRuleChainId); + relation.setType(EntityRelation.USES_TYPE); + relation.setTypeGroup(RelationTypeGroup.COMMON); + return relationService.saveRelationAsync(tenantId, relation); + } + return Futures.immediateFuture(null); + }, executorService); + } + } catch (Exception e) { + log.error("[{}] Create relation for input node: [{}]", tenantId, ruleNode, e); + } + return Futures.immediateFuture(null); + }).toList(); + + Futures.allAsList(resultFutures).get(); + } catch (Exception e) { + log.error("[{}] Unable to update Tenant input nodes", tenantId, e); + } + } + }; + @Override public void upgradeRuleNodes() { int totalRuleNodesUpgraded = 0; From ccd970b0da44092d8adbc817080968ad503dc817 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 12 Mar 2025 17:53:00 +0200 Subject: [PATCH 06/63] fixed resource vc restoring --- .../ie/importing/impl/AssetImportService.java | 2 +- .../impl/AssetProfileImportService.java | 2 +- .../impl/BaseEntityImportService.java | 33 ++++++++++---- .../importing/impl/CustomerImportService.java | 2 +- .../impl/DashboardImportService.java | 8 ++-- .../importing/impl/DeviceImportService.java | 2 +- .../impl/DeviceProfileImportService.java | 2 +- .../impl/EntityViewImportService.java | 2 +- .../impl/NotificationRuleImportService.java | 2 +- .../impl/NotificationTargetImportService.java | 2 +- .../NotificationTemplateImportService.java | 2 +- .../importing/impl/ResourceImportService.java | 11 ++--- .../impl/RuleChainImportService.java | 12 ++--- .../impl/WidgetTypeImportService.java | 6 +-- .../impl/WidgetsBundleImportService.java | 6 +-- .../DefaultEntitiesVersionControlService.java | 2 +- .../server/utils/LwM2mObjectModelUtils.java | 1 - .../server/common/data/TbResourceInfo.java | 45 +++++++++++++++++++ .../common/data/sync/vc/EntityLoadError.java | 6 ++- 19 files changed, 108 insertions(+), 40 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java index 7cd4c3aca1..9bf44ffbec 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java @@ -46,7 +46,7 @@ public class AssetImportService extends BaseEntityImportService exportData, IdProvider idProvider) { + protected Asset saveOrUpdate(EntitiesImportCtx ctx, Asset asset, EntityExportData exportData, IdProvider idProvider, CompareResult compareResult) { Asset savedAsset = assetService.saveAsset(asset); if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) { importCalculatedFields(ctx, savedAsset, exportData, idProvider); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java index 32a0090a4a..d5663c34a4 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java @@ -49,7 +49,7 @@ public class AssetProfileImportService extends BaseEntityImportService exportData, IdProvider idProvider) { + protected AssetProfile saveOrUpdate(EntitiesImportCtx ctx, AssetProfile assetProfile, EntityExportData exportData, IdProvider idProvider, CompareResult compareResult) { AssetProfile saved = assetProfileService.saveAssetProfile(assetProfile); if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) { importCalculatedFields(ctx, saved, exportData, idProvider); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java index bfa95af83c..83052360b6 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java @@ -18,6 +18,8 @@ package org.thingsboard.server.service.sync.ie.importing.impl; import com.fasterxml.jackson.databind.JsonNode; import com.google.api.client.util.Objects; import com.google.common.util.concurrent.FutureCallback; +import lombok.AllArgsConstructor; +import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; @@ -117,10 +119,10 @@ public abstract class BaseEntityImportService importResult, D exportData, IdProvider idProvider) throws ThingsboardException { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CustomerImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CustomerImportService.java index 8774c77870..d4179b639d 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CustomerImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/CustomerImportService.java @@ -52,7 +52,7 @@ public class CustomerImportService extends BaseEntityImportService exportData, IdProvider idProvider) { + protected Customer saveOrUpdate(EntitiesImportCtx ctx, Customer customer, EntityExportData exportData, IdProvider idProvider, CompareResult compareResult) { if (!customer.isPublic()) { return customerService.saveCustomer(customer); } else { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java index e9407f2d58..6744b04f55 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java @@ -75,7 +75,7 @@ public class DashboardImportService extends BaseEntityImportService exportData, IdProvider idProvider) { + protected Dashboard saveOrUpdate(EntitiesImportCtx ctx, Dashboard dashboard, EntityExportData exportData, IdProvider idProvider, CompareResult compareResult) { var tenantId = ctx.getTenantId(); Set assignedCustomers = Optional.ofNullable(dashboard.getAssignedCustomers()).orElse(Collections.emptySet()).stream() @@ -116,8 +116,10 @@ public class DashboardImportService extends BaseEntityImportService exportData, Dashboard prepared, Dashboard existing) { - return super.compare(ctx, exportData, prepared, existing) || !prepared.getConfiguration().equals(existing.getConfiguration()); + protected CompareResult compare(EntitiesImportCtx ctx, EntityExportData exportData, Dashboard prepared, Dashboard existing) { + CompareResult result = super.compare(ctx, exportData, prepared, existing); + result.setNeedUpdate(result.isNeedUpdate() || !prepared.getConfiguration().equals(existing.getConfiguration())); + return result; } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java index 84e264efdd..4ace9ff938 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DeviceImportService.java @@ -63,7 +63,7 @@ public class DeviceImportService extends BaseEntityImportService exportData, IdProvider idProvider) { + protected DeviceProfile saveOrUpdate(EntitiesImportCtx ctx, DeviceProfile deviceProfile, EntityExportData exportData, IdProvider idProvider, CompareResult compareResult) { DeviceProfile saved = deviceProfileService.saveDeviceProfile(deviceProfile); if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) { importCalculatedFields(ctx, saved, exportData, idProvider); diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/EntityViewImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/EntityViewImportService.java index 8e8f2e90a6..1479943b08 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/EntityViewImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/EntityViewImportService.java @@ -55,7 +55,7 @@ public class EntityViewImportService extends BaseEntityImportService exportData, IdProvider idProvider) { + protected EntityView saveOrUpdate(EntitiesImportCtx ctx, EntityView entityView, EntityExportData exportData, IdProvider idProvider, CompareResult compareResult) { return entityViewService.saveEntityView(entityView); } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationRuleImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationRuleImportService.java index 52f91912e6..a2ae7c8a13 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationRuleImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationRuleImportService.java @@ -135,7 +135,7 @@ public class NotificationRuleImportService extends BaseEntityImportService exportData, IdProvider idProvider) { + protected NotificationRule saveOrUpdate(EntitiesImportCtx ctx, NotificationRule notificationRule, EntityExportData exportData, IdProvider idProvider, CompareResult compareResult) { ConstraintValidator.validateFields(notificationRule); return notificationRuleService.saveNotificationRule(ctx.getTenantId(), notificationRule); } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTargetImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTargetImportService.java index 9bca0f8054..4323aba9cc 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTargetImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTargetImportService.java @@ -80,7 +80,7 @@ public class NotificationTargetImportService extends BaseEntityImportService exportData, IdProvider idProvider) { + protected NotificationTarget saveOrUpdate(EntitiesImportCtx ctx, NotificationTarget notificationTarget, EntityExportData exportData, IdProvider idProvider, CompareResult compareResult) { ConstraintValidator.validateFields(notificationTarget); return notificationTargetService.saveNotificationTarget(ctx.getTenantId(), notificationTarget); } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTemplateImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTemplateImportService.java index 1452321744..09a93e0937 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTemplateImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationTemplateImportService.java @@ -48,7 +48,7 @@ public class NotificationTemplateImportService extends BaseEntityImportService exportData, IdProvider idProvider) { + protected NotificationTemplate saveOrUpdate(EntitiesImportCtx ctx, NotificationTemplate notificationTemplate, EntityExportData exportData, IdProvider idProvider, CompareResult compareResult) { ConstraintValidator.validateFields(notificationTemplate); return notificationTemplateService.saveNotificationTemplate(ctx.getTenantId(), notificationTemplate); } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ResourceImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ResourceImportService.java index bedda621e0..e97a5ad06e 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ResourceImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ResourceImportService.java @@ -58,17 +58,18 @@ public class ResourceImportService extends BaseEntityImportService exportData, TbResource prepared, TbResource existing) { - return true; + protected TbResource deepCopy(TbResource resource) { + return new TbResource(resource); } @Override - protected TbResource deepCopy(TbResource resource) { - return new TbResource(resource); + protected void cleanupForComparison(TbResource resource) { + super.cleanupForComparison(resource); + resource.setSearchText(null); } @Override - protected TbResource saveOrUpdate(EntitiesImportCtx ctx, TbResource resource, EntityExportData exportData, IdProvider idProvider) { + protected TbResource saveOrUpdate(EntitiesImportCtx ctx, TbResource resource, EntityExportData exportData, IdProvider idProvider, CompareResult compareResult) { if (resource.getResourceType() == ResourceType.IMAGE) { return new TbResource(imageService.saveImage(resource)); } else { diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java index a7c68af297..6ad2b98953 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java @@ -103,7 +103,7 @@ public class RuleChainImportService extends BaseEntityImportService { String bundleAlias = widgetTypeNode.remove("bundleAlias").asText(); @@ -75,8 +75,8 @@ public class WidgetsBundleImportService extends BaseEntityImportService analyze(Throwable e, EntityId externalId) { diff --git a/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java b/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java index fb6125bc0b..b1c71c9ded 100644 --- a/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/LwM2mObjectModelUtils.java @@ -54,7 +54,6 @@ public class LwM2mObjectModelUtils { if (resource.getId() == null) { resource.setTitle(name + " id=" + objectModel.id + " v" + objectModel.version); } - resource.setSearchText(resourceKey + LWM2M_SEPARATOR_SEARCH_TEXT + name); } else { throw new DataValidationException(String.format("Could not parse the XML of objectModel with name %s", resource.getSearchText())); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java index a3bf383503..89f5fa38b0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceInfo.java @@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; +import java.util.Objects; import java.util.function.UnaryOperator; @Schema @@ -151,4 +152,48 @@ public class TbResourceInfo extends BaseData implements HasName, H this.descriptor = value != null ? mapper.valueToTree(value) : null; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + TbResourceInfo that = (TbResourceInfo) o; + + if (isPublic != that.isPublic) return false; + if (!Objects.equals(tenantId, that.tenantId)) return false; + if (!Objects.equals(title, that.title)) return false; + if (resourceType != that.resourceType) return false; + if (resourceSubType != that.resourceSubType) return false; + if (!Objects.equals(resourceKey, that.resourceKey)) return false; + if (!Objects.equals(publicResourceKey, that.publicResourceKey)) + return false; + if (!Objects.equals(searchText, that.searchText)) return false; + if (!Objects.equals(etag, that.etag)) return false; + if (!Objects.equals(fileName, that.fileName)) return false; + if (!Objects.equals(descriptor, that.descriptor)) { + if (!((descriptor == null || descriptor.isNull()) && (that.descriptor == null || that.descriptor.isNull()))){ + return false; + } + } + return Objects.equals(externalId, that.externalId); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (tenantId != null ? tenantId.hashCode() : 0); + result = 31 * result + (title != null ? title.hashCode() : 0); + result = 31 * result + (resourceType != null ? resourceType.hashCode() : 0); + result = 31 * result + (resourceSubType != null ? resourceSubType.hashCode() : 0); + result = 31 * result + (resourceKey != null ? resourceKey.hashCode() : 0); + result = 31 * result + (isPublic ? 1 : 0); + result = 31 * result + (publicResourceKey != null ? publicResourceKey.hashCode() : 0); + result = 31 * result + (searchText != null ? searchText.hashCode() : 0); + result = 31 * result + (etag != null ? etag.hashCode() : 0); + result = 31 * result + (fileName != null ? fileName.hashCode() : 0); + result = 31 * result + (descriptor != null ? descriptor.hashCode() : 0); + result = 31 * result + (externalId != null ? externalId.hashCode() : 0); + return result; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityLoadError.java b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityLoadError.java index 049a7e5d2a..157f3bf553 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityLoadError.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/sync/vc/EntityLoadError.java @@ -45,11 +45,15 @@ public class EntityLoadError implements Serializable { } public static EntityLoadError runtimeError(Throwable e) { + return runtimeError(e, null); + } + + public static EntityLoadError runtimeError(Throwable e, EntityId externalId) { String message = e.getMessage(); if (StringUtils.isEmpty(message)) { message = "unexpected error (" + ClassUtils.getShortClassName(e.getClass()) + ")"; } - return EntityLoadError.builder().type("RUNTIME").message(message).build(); + return EntityLoadError.builder().type("RUNTIME").message(message).source(externalId).build(); } } From 29e9a3d122e07de10916980c0fa79cf3a6699850 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 12 Mar 2025 18:31:30 +0200 Subject: [PATCH 07/63] added test --- .../importing/impl/ResourceImportService.java | 6 ++- .../service/sync/vc/VersionControlTest.java | 44 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ResourceImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ResourceImportService.java index e97a5ad06e..f5ca15cfca 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ResourceImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ResourceImportService.java @@ -73,7 +73,11 @@ public class ResourceImportService extends BaseEntityImportService & HasTenantId> void checkImportedEntity(TenantId tenantId1, E initialEntity, TenantId tenantId2, E importedEntity) { assertThat(initialEntity.getTenantId()).isEqualTo(tenantId1); assertThat(importedEntity.getTenantId()).isEqualTo(tenantId2); @@ -1126,4 +1152,22 @@ public class VersionControlTest extends AbstractControllerTest { return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields?", new TypeReference>() {}, new PageLink(100, 0)).getData(); } + private TbResourceInfo createResource(String name) { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.JKS); + resource.setTitle(name); + resource.setFileName(DEFAULT_FILE_NAME); + resource.setEncodedData(TEST_DATA); + + return saveTbResource(resource); + } + + private TbResourceInfo saveTbResource(TbResource tbResource) { + return doPost("/api/resource", tbResource, TbResourceInfo.class); + } + + private TbResource findResource(String name) throws Exception { + return doGetTypedWithPageLink("/api/resource?", new TypeReference>() {}, new PageLink(100, 0, name)).getData().get(0); + } + } From e1dfa3f80329dd3a29d70ddc223c5d9036c512f3 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 12 Mar 2025 19:24:28 +0200 Subject: [PATCH 08/63] refactoring --- .../impl/BaseEntityImportService.java | 28 +++++++++++-------- .../impl/DashboardImportService.java | 2 +- .../impl/RuleChainImportService.java | 4 +-- .../impl/WidgetTypeImportService.java | 2 +- .../impl/WidgetsBundleImportService.java | 2 +- .../controller/TbResourceControllerTest.java | 4 +-- .../service/sync/vc/VersionControlTest.java | 5 ++-- 7 files changed, 25 insertions(+), 22 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java index 83052360b6..27e4209e7c 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java @@ -121,7 +121,7 @@ public abstract class BaseEntityImportService exportData, Dashboard prepared, Dashboard existing) { CompareResult result = super.compare(ctx, exportData, prepared, existing); - result.setNeedUpdate(result.isNeedUpdate() || !prepared.getConfiguration().equals(existing.getConfiguration())); + result.setUpdateNeeded(result.isUpdateNeeded() || !prepared.getConfiguration().equals(existing.getConfiguration())); return result; } diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java index 6ad2b98953..f5b4521629 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java @@ -117,11 +117,11 @@ public class RuleChainImportService extends BaseEntityImportService Date: Thu, 13 Mar 2025 18:33:56 +0200 Subject: [PATCH 09/63] compare method refactoring --- .../importing/impl/BaseEntityImportService.java | 16 ++++++++++++---- .../importing/impl/DashboardImportService.java | 6 ++---- .../importing/impl/RuleChainImportService.java | 12 +++++++----- .../server/utils/LwM2mObjectModelUtils.java | 1 + .../service/sync/vc/VersionControlTest.java | 4 ++-- .../server/common/data/TbResourceInfo.java | 2 +- 6 files changed, 25 insertions(+), 16 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java index 27e4209e7c..57b4737be6 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java @@ -141,7 +141,7 @@ public abstract class BaseEntityImportService exportData, Dashboard prepared, Dashboard existing) { - CompareResult result = super.compare(ctx, exportData, prepared, existing); - result.setUpdateNeeded(result.isUpdateNeeded() || !prepared.getConfiguration().equals(existing.getConfiguration())); - return result; + protected boolean isUpdateNeeded(EntitiesImportCtx ctx, EntityExportData exportData, Dashboard prepared, Dashboard existing) { + return super.isUpdateNeeded(ctx, exportData, prepared, existing) || !prepared.getConfiguration().equals(existing.getConfiguration()); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java index f5b4521629..baf1b84f82 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.sync.ie.importing.impl; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.audit.ActionType; @@ -27,6 +28,7 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.rule.RuleNode; +import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.RuleChainExportData; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleNodeDao; @@ -115,15 +117,15 @@ public class RuleChainImportService extends BaseEntityImportService implements HasName, H if (!Objects.equals(resourceKey, that.resourceKey)) return false; if (!Objects.equals(publicResourceKey, that.publicResourceKey)) return false; - if (!Objects.equals(searchText, that.searchText)) return false; + if (!Objects.equals(getSearchText(), that.getSearchText())) return false; if (!Objects.equals(etag, that.etag)) return false; if (!Objects.equals(fileName, that.fileName)) return false; if (!Objects.equals(descriptor, that.descriptor)) { From 753071ea17fbb4eb74af7d88839f525f459683b8 Mon Sep 17 00:00:00 2001 From: YevhenBondarenko Date: Fri, 14 Mar 2025 14:48:32 +0100 Subject: [PATCH 10/63] minor refactoring --- .../update/DefaultDataUpdateService.java | 24 +++++++------------ .../server/dao/rule/BaseRuleChainService.java | 6 ++--- .../dao/service/RuleChainServiceTest.java | 5 ++-- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index b1f80cb793..4c90258330 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -49,7 +49,7 @@ import java.util.List; import java.util.UUID; import java.util.concurrent.ExecutionException; -import static org.thingsboard.server.common.data.relation.EntityRelation.USES_TYPE; +import static org.thingsboard.server.dao.rule.BaseRuleChainService.TB_RULE_CHAIN_INPUT_NODE; @Service @Profile("install") @@ -98,28 +98,22 @@ public class DefaultDataUpdateService implements DataUpdateService { protected void updateEntity(Tenant tenant) { TenantId tenantId = tenant.getId(); try { - var inputNodes = ruleChainService.findRuleNodesByTenantIdAndType(tenantId, "org.thingsboard.rule.engine.flow.TbRuleChainInputNode"); + var inputNodes = ruleChainService.findRuleNodesByTenantIdAndType(tenantId, TB_RULE_CHAIN_INPUT_NODE); var resultFutures = inputNodes.stream().map(ruleNode -> { try { JsonNode id = ruleNode.getConfiguration().get("ruleChainId"); if (id != null) { RuleChainId toRuleChainId = new RuleChainId(UUID.fromString(id.asText())); RuleChainId fromRuleChainId = ruleNode.getRuleChainId(); - var isExistFuture = relationService.checkRelationAsync(null, fromRuleChainId, toRuleChainId, USES_TYPE, RelationTypeGroup.COMMON); - Futures.transformAsync(isExistFuture, isExist -> { - if (!isExist) { - EntityRelation relation = new EntityRelation(); - relation.setFrom(fromRuleChainId); - relation.setTo(toRuleChainId); - relation.setType(EntityRelation.USES_TYPE); - relation.setTypeGroup(RelationTypeGroup.COMMON); - return relationService.saveRelationAsync(tenantId, relation); - } - return Futures.immediateFuture(null); - }, executorService); + EntityRelation relation = new EntityRelation(); + relation.setFrom(fromRuleChainId); + relation.setTo(toRuleChainId); + relation.setType(EntityRelation.USES_TYPE); + relation.setTypeGroup(RelationTypeGroup.COMMON); + return relationService.saveRelationAsync(tenantId, relation); } } catch (Exception e) { - log.error("[{}] Create relation for input node: [{}]", tenantId, ruleNode, e); + log.error("[{}] Failed to save relation for input node: [{}]", tenantId, ruleNode, e); } return Futures.immediateFuture(null); }).toList(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 9d61408199..1ce9bd555b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -210,7 +210,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC List existingRuleNodes = getRuleChainNodes(tenantId, ruleChainMetaData.getRuleChainId()); for (RuleNode existingNode : existingRuleNodes) { relationService.deleteEntityRelations(tenantId, existingNode.getId()); - if (existingNode.getType().equals("org.thingsboard.rule.engine.flow.TbRuleChainInputNode")) { + if (existingNode.getType().equals(TB_RULE_CHAIN_INPUT_NODE)) { if (existingNode.getConfiguration().has("ruleChainId")) { RuleChainId targetRuleChainId = extractRuleChainIdFromInputNode(existingNode); var relation = createRuleChainInputRelation(ruleChainId, targetRuleChainId); @@ -241,7 +241,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC RuleNode savedNode = ruleNodeDao.save(tenantId, node); relations.add(new EntityRelation(ruleChainMetaData.getRuleChainId(), savedNode.getId(), EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); - if (node.getType().equals("org.thingsboard.rule.engine.flow.TbRuleChainInputNode")) { + if (node.getType().equals(TB_RULE_CHAIN_INPUT_NODE)) { if (node.getConfiguration().has("ruleChainId")) { RuleChainId targetRuleChainId = extractRuleChainIdFromInputNode(node); var relation = createRuleChainInputRelation(ruleChainId, targetRuleChainId); @@ -280,7 +280,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC RuleNode targetNode = new RuleNode(); targetNode.setName(targetRuleChain != null ? targetRuleChain.getName() : "Rule Chain Input"); targetNode.setRuleChainId(ruleChainId); - targetNode.setType("org.thingsboard.rule.engine.flow.TbRuleChainInputNode"); + targetNode.setType(TB_RULE_CHAIN_INPUT_NODE); var configuration = JacksonUtil.newObjectNode(); configuration.put("ruleChainId", targetRuleChainId.getId().toString()); targetNode.setConfiguration(configuration); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java index 54144d5192..a2ca663619 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java @@ -48,6 +48,7 @@ import java.util.function.Function; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.thingsboard.server.common.data.relation.EntityRelation.USES_TYPE; +import static org.thingsboard.server.dao.rule.BaseRuleChainService.TB_RULE_CHAIN_INPUT_NODE; /** * Created by igor on 3/13/18. @@ -377,7 +378,7 @@ public class RuleChainServiceTest extends AbstractServiceTest { RuleNode ruleNode = new RuleNode(); ruleNode.setName("Input node"); - ruleNode.setType("org.thingsboard.rule.engine.flow.TbRuleChainInputNode"); + ruleNode.setType(TB_RULE_CHAIN_INPUT_NODE); ObjectNode configuration = JacksonUtil.newObjectNode(); configuration.put("ruleChainId", savedToRuleChain.getId().toString()); ruleNode.setConfiguration(configuration); @@ -402,7 +403,7 @@ public class RuleChainServiceTest extends AbstractServiceTest { RuleNode newRuleNode = new RuleNode(); newRuleNode.setName("Input node"); - newRuleNode.setType("org.thingsboard.rule.engine.flow.TbRuleChainInputNode"); + newRuleNode.setType(TB_RULE_CHAIN_INPUT_NODE); ObjectNode newConfiguration = JacksonUtil.newObjectNode(); configuration.put("ruleChainId", savedNewToRuleChain.getId().toString()); newRuleNode.setConfiguration(newConfiguration); From 264775ddb43da8aa6816f2bf9aee65325dc981b3 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Wed, 19 Mar 2025 20:51:27 +0100 Subject: [PATCH 11/63] MQTT client reconnect strategy exponential --- .../org/thingsboard/mqtt/MqttClientImpl.java | 5 +- .../thingsboard/mqtt/ReconnectStrategy.java | 21 +++++++ .../mqtt/ReconnectStrategyExponential.java | 58 +++++++++++++++++++ .../ReconnectStrategyExponentialTest.java | 43 ++++++++++++++ 4 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 netty-mqtt/src/main/java/org/thingsboard/mqtt/ReconnectStrategy.java create mode 100644 netty-mqtt/src/main/java/org/thingsboard/mqtt/ReconnectStrategyExponential.java create mode 100644 netty-mqtt/src/test/java/org/thingsboard/mqtt/ReconnectStrategyExponentialTest.java diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java index 47eae565dc..344886165e 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java @@ -80,6 +80,8 @@ final class MqttClientImpl implements MqttClient { private final MqttHandler defaultHandler; + private final ReconnectStrategy reconnectStrategy; + private EventLoopGroup eventLoop; private volatile Channel channel; @@ -110,6 +112,7 @@ final class MqttClientImpl implements MqttClient { this.clientConfig = clientConfig; this.defaultHandler = defaultHandler; this.handlerExecutor = handlerExecutor; + this.reconnectStrategy = new ReconnectStrategyExponential(getClientConfig().getReconnectDelay()); } /** @@ -191,7 +194,7 @@ final class MqttClientImpl implements MqttClient { if (reconnect) { this.reconnect = true; } - eventLoop.schedule((Runnable) () -> connect(host, port, reconnect), clientConfig.getReconnectDelay(), TimeUnit.SECONDS); + eventLoop.schedule((Runnable) () -> connect(host, port, reconnect), reconnectStrategy.getNextReconnectDelay(), TimeUnit.SECONDS); } } diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/ReconnectStrategy.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/ReconnectStrategy.java new file mode 100644 index 0000000000..f924b79ca5 --- /dev/null +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/ReconnectStrategy.java @@ -0,0 +1,21 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.mqtt; + +@FunctionalInterface +public interface ReconnectStrategy { + long getNextReconnectDelay(); +} diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/ReconnectStrategyExponential.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/ReconnectStrategyExponential.java new file mode 100644 index 0000000000..dbd11f91e1 --- /dev/null +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/ReconnectStrategyExponential.java @@ -0,0 +1,58 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.mqtt; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.TimeUnit; + +@Getter +@Slf4j +public class ReconnectStrategyExponential implements ReconnectStrategy { + + public static final int DEFAULT_RECONNECT_INTERVAL = 10; + final long reconnectIntervalMinSeconds; + final long reconnectIntervalMaxSeconds = 30; + long lastDisconnectNanoTime = 0; //isotonic time + int retryCount = 0; + + public ReconnectStrategyExponential(long reconnectIntervalMin) { + this.reconnectIntervalMinSeconds = calculateIntervalMin(reconnectIntervalMin); + } + + private long calculateIntervalMin(long reconnectIntervalMin) { + return Math.min((reconnectIntervalMin > 0 ? reconnectIntervalMin : DEFAULT_RECONNECT_INTERVAL), this.reconnectIntervalMaxSeconds); + } + + @Override + synchronized public long getNextReconnectDelay() { + final long currentNanoTime = getNanoTime(); + final long lastDisconnectIntervalNanos = currentNanoTime - lastDisconnectNanoTime; + lastDisconnectNanoTime = currentNanoTime; + if (TimeUnit.NANOSECONDS.toSeconds(lastDisconnectIntervalNanos) > reconnectIntervalMaxSeconds + reconnectIntervalMinSeconds) { + log.debug("Reset retry counter"); + retryCount = 0; + return reconnectIntervalMinSeconds; + } + return Math.min(reconnectIntervalMaxSeconds, reconnectIntervalMinSeconds + (1L << retryCount++)); + } + + long getNanoTime() { + return System.nanoTime(); + } + +} diff --git a/netty-mqtt/src/test/java/org/thingsboard/mqtt/ReconnectStrategyExponentialTest.java b/netty-mqtt/src/test/java/org/thingsboard/mqtt/ReconnectStrategyExponentialTest.java new file mode 100644 index 0000000000..828cfae730 --- /dev/null +++ b/netty-mqtt/src/test/java/org/thingsboard/mqtt/ReconnectStrategyExponentialTest.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.mqtt; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.mockito.BDDMockito; +import org.mockito.Mockito; + +import java.util.concurrent.TimeUnit; + +@Slf4j +class ReconnectStrategyExponentialTest { + + @Test + public void exponentialReconnect() { + ReconnectStrategyExponential strategy = Mockito.spy(new ReconnectStrategyExponential(1)); + for (int i = 0; i < 10; i++) { + log.info("Disconnect [{}] Delay [{}]", i, strategy.getNextReconnectDelay()); + } + + final long coolDownPeriod = strategy.getReconnectIntervalMinSeconds() + strategy.getReconnectIntervalMaxSeconds() + 1; + + BDDMockito.willAnswer((x) -> System.nanoTime() + TimeUnit.SECONDS.toNanos(coolDownPeriod)).given(strategy).getNanoTime(); + log.info("After cooldown period [{}] seconds later...", coolDownPeriod); + for (int i = 0; i < 10; i++) { + log.info("Disconnect [{}] Delay [{}]", i, strategy.getNextReconnectDelay()); + } + } +} \ No newline at end of file From 83790fa0fb6a208ca872a7c9cb704d2e8dab80e6 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Thu, 20 Mar 2025 11:02:52 +0100 Subject: [PATCH 12/63] ReconnectStrategyExponential jitter, max added, refactored, tested --- .../mqtt/ReconnectStrategyExponential.java | 50 +++++++++--- .../ReconnectStrategyExponentialTest.java | 80 +++++++++++++++---- 2 files changed, 103 insertions(+), 27 deletions(-) diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/ReconnectStrategyExponential.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/ReconnectStrategyExponential.java index dbd11f91e1..e0b83c6ab7 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/ReconnectStrategyExponential.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/ReconnectStrategyExponential.java @@ -18,37 +18,61 @@ package org.thingsboard.mqtt; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; @Getter @Slf4j public class ReconnectStrategyExponential implements ReconnectStrategy { - public static final int DEFAULT_RECONNECT_INTERVAL = 10; - final long reconnectIntervalMinSeconds; - final long reconnectIntervalMaxSeconds = 30; - long lastDisconnectNanoTime = 0; //isotonic time - int retryCount = 0; + public static final int DEFAULT_RECONNECT_INTERVAL_SEC = 10; + public static final int MAX_RECONNECT_INTERVAL_SEC = 60; + public static final int EXP_MAX = 8; + public static final long JITTER_MAX = 1; + private final long reconnectIntervalMinSeconds; + private final long reconnectIntervalMaxSeconds; + private long lastDisconnectNanoTime = 0; //isotonic time + private long retryCount = 0; - public ReconnectStrategyExponential(long reconnectIntervalMin) { - this.reconnectIntervalMinSeconds = calculateIntervalMin(reconnectIntervalMin); + public ReconnectStrategyExponential(long reconnectIntervalMinSeconds) { + this.reconnectIntervalMaxSeconds = calculateIntervalMax(reconnectIntervalMinSeconds); + this.reconnectIntervalMinSeconds = calculateIntervalMin(reconnectIntervalMinSeconds); } - private long calculateIntervalMin(long reconnectIntervalMin) { - return Math.min((reconnectIntervalMin > 0 ? reconnectIntervalMin : DEFAULT_RECONNECT_INTERVAL), this.reconnectIntervalMaxSeconds); + long calculateIntervalMax(long reconnectIntervalMinSeconds) { + return reconnectIntervalMinSeconds > MAX_RECONNECT_INTERVAL_SEC ? reconnectIntervalMinSeconds : MAX_RECONNECT_INTERVAL_SEC; + } + + long calculateIntervalMin(long reconnectIntervalMinSeconds) { + return Math.min((reconnectIntervalMinSeconds > 0 ? reconnectIntervalMinSeconds : DEFAULT_RECONNECT_INTERVAL_SEC), this.reconnectIntervalMaxSeconds); } @Override synchronized public long getNextReconnectDelay() { final long currentNanoTime = getNanoTime(); - final long lastDisconnectIntervalNanos = currentNanoTime - lastDisconnectNanoTime; + final long coolDownSpentNanos = currentNanoTime - lastDisconnectNanoTime; lastDisconnectNanoTime = currentNanoTime; - if (TimeUnit.NANOSECONDS.toSeconds(lastDisconnectIntervalNanos) > reconnectIntervalMaxSeconds + reconnectIntervalMinSeconds) { - log.debug("Reset retry counter"); + if (isCooledDown(coolDownSpentNanos)) { retryCount = 0; return reconnectIntervalMinSeconds; } - return Math.min(reconnectIntervalMaxSeconds, reconnectIntervalMinSeconds + (1L << retryCount++)); + return calculateNextReconnectDelay() + calculateJitter(); + } + + long calculateJitter() { + return ThreadLocalRandom.current().nextInt() >= 0 ? JITTER_MAX : 0; + } + + long calculateNextReconnectDelay() { + return Math.min(reconnectIntervalMaxSeconds, reconnectIntervalMinSeconds + calculateExp(retryCount++)); + } + + long calculateExp(long e) { + return 1L << Math.min(e, EXP_MAX); + } + + boolean isCooledDown(long coolDownSpentNanos) { + return TimeUnit.NANOSECONDS.toSeconds(coolDownSpentNanos) > reconnectIntervalMaxSeconds + reconnectIntervalMinSeconds; } long getNanoTime() { diff --git a/netty-mqtt/src/test/java/org/thingsboard/mqtt/ReconnectStrategyExponentialTest.java b/netty-mqtt/src/test/java/org/thingsboard/mqtt/ReconnectStrategyExponentialTest.java index 828cfae730..085dcef3a6 100644 --- a/netty-mqtt/src/test/java/org/thingsboard/mqtt/ReconnectStrategyExponentialTest.java +++ b/netty-mqtt/src/test/java/org/thingsboard/mqtt/ReconnectStrategyExponentialTest.java @@ -16,28 +16,80 @@ package org.thingsboard.mqtt; import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.Test; -import org.mockito.BDDMockito; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; +import org.mockito.stubbing.Answer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.Offset.offset; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.BDDMockito.willAnswer; +import static org.thingsboard.mqtt.ReconnectStrategyExponential.EXP_MAX; +import static org.thingsboard.mqtt.ReconnectStrategyExponential.JITTER_MAX; @Slf4j class ReconnectStrategyExponentialTest { - @Test - public void exponentialReconnect() { - ReconnectStrategyExponential strategy = Mockito.spy(new ReconnectStrategyExponential(1)); - for (int i = 0; i < 10; i++) { - log.info("Disconnect [{}] Delay [{}]", i, strategy.getNextReconnectDelay()); - } + @Execution(ExecutionMode.SAME_THREAD) // just for convenient log reading + @ParameterizedTest + @ValueSource(ints = {1, 0, 60}) + public void exponentialReconnectDelayTest(final int reconnectIntervalMinSeconds) { + final ReconnectStrategyExponential strategy = Mockito.spy(new ReconnectStrategyExponential(reconnectIntervalMinSeconds)); + log.info("=== Reconnect delay test for ReconnectStrategyExponential({}) : calculated min [{}] max [{}] ===", reconnectIntervalMinSeconds, strategy.getReconnectIntervalMinSeconds(), strategy.getReconnectIntervalMaxSeconds()); + final AtomicLong nanoTime = new AtomicLong(System.nanoTime()); + willAnswer((x) -> nanoTime.get()).given(strategy).getNanoTime(); + final LinkedBlockingDeque jittersCaptured = new LinkedBlockingDeque<>(); + final LinkedBlockingDeque expCaptured = new LinkedBlockingDeque<>(); - final long coolDownPeriod = strategy.getReconnectIntervalMinSeconds() + strategy.getReconnectIntervalMaxSeconds() + 1; + willAnswer(captureResult(jittersCaptured)).given(strategy).calculateJitter(); + willAnswer(captureResult(expCaptured)).given(strategy).calculateExp(anyLong()); - BDDMockito.willAnswer((x) -> System.nanoTime() + TimeUnit.SECONDS.toNanos(coolDownPeriod)).given(strategy).getNanoTime(); - log.info("After cooldown period [{}] seconds later...", coolDownPeriod); - for (int i = 0; i < 10; i++) { - log.info("Disconnect [{}] Delay [{}]", i, strategy.getNextReconnectDelay()); + for (int phase = 0; phase < 3; phase++) { + log.info("== Phase {} ==", phase); + long previousDelay = 0; + for (int i = 0; i < EXP_MAX + 4; i++) { + final long nextReconnectDelay = strategy.getNextReconnectDelay(); + nanoTime.addAndGet(TimeUnit.SECONDS.toNanos(nextReconnectDelay)); + log.info("Retry [{}] Delay [{}] : min [{}] exp [{}] jitter [{}]", strategy.getRetryCount(), nextReconnectDelay, strategy.getReconnectIntervalMinSeconds(), expCaptured.peekLast(), jittersCaptured.peekLast()); + assertThat(previousDelay).satisfiesAnyOf( + v -> assertThat(v).isLessThanOrEqualTo(nextReconnectDelay), + v -> assertThat(v).isCloseTo(nextReconnectDelay, offset(JITTER_MAX)) // Adjust tolerance as needed + ); + previousDelay = nextReconnectDelay; + } + log.info("Jitters captured: {}", drainAll(jittersCaptured)); + log.info("Exponents captured: {}", drainAll(expCaptured)); + assertThat(previousDelay).isCloseTo(strategy.getReconnectIntervalMaxSeconds(), offset(JITTER_MAX)); + + final long coolDownPeriodSec = strategy.getReconnectIntervalMinSeconds() + strategy.getReconnectIntervalMaxSeconds() + 1; + log.info("Cooling down for [{}] seconds ...", coolDownPeriodSec); + nanoTime.addAndGet(TimeUnit.SECONDS.toNanos(coolDownPeriodSec)); + assertThat(strategy.isCooledDown(TimeUnit.SECONDS.toNanos(coolDownPeriodSec))).as("cooled down").isTrue(); } } -} \ No newline at end of file + + private Answer captureResult(Collection collection) { + return invocation -> { + long result = (long) invocation.callRealMethod(); + collection.add(result); + return result; + }; + } + + private Collection drainAll(BlockingQueue jittersCaptured) { + Collection elements = new ArrayList<>(); + jittersCaptured.drainTo(elements); + return elements; + } + +} From 8813e32910790330ae7c4765f3876dc8866d958c Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 27 Mar 2025 11:59:49 +0200 Subject: [PATCH 13/63] moved resource vc comparison fix to cleanupForComparison method --- .../importing/impl/ResourceImportService.java | 3 ++ .../server/common/data/TbResourceInfo.java | 45 ------------------- 2 files changed, 3 insertions(+), 45 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ResourceImportService.java b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ResourceImportService.java index f5ca15cfca..96fcdd5425 100644 --- a/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ResourceImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/ResourceImportService.java @@ -66,6 +66,9 @@ public class ResourceImportService extends BaseEntityImportService implements HasName, H this.descriptor = value != null ? mapper.valueToTree(value) : null; } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - - TbResourceInfo that = (TbResourceInfo) o; - - if (isPublic != that.isPublic) return false; - if (!Objects.equals(tenantId, that.tenantId)) return false; - if (!Objects.equals(title, that.title)) return false; - if (resourceType != that.resourceType) return false; - if (resourceSubType != that.resourceSubType) return false; - if (!Objects.equals(resourceKey, that.resourceKey)) return false; - if (!Objects.equals(publicResourceKey, that.publicResourceKey)) - return false; - if (!Objects.equals(getSearchText(), that.getSearchText())) return false; - if (!Objects.equals(etag, that.etag)) return false; - if (!Objects.equals(fileName, that.fileName)) return false; - if (!Objects.equals(descriptor, that.descriptor)) { - if (!((descriptor == null || descriptor.isNull()) && (that.descriptor == null || that.descriptor.isNull()))){ - return false; - } - } - return Objects.equals(externalId, that.externalId); - } - - @Override - public int hashCode() { - int result = super.hashCode(); - result = 31 * result + (tenantId != null ? tenantId.hashCode() : 0); - result = 31 * result + (title != null ? title.hashCode() : 0); - result = 31 * result + (resourceType != null ? resourceType.hashCode() : 0); - result = 31 * result + (resourceSubType != null ? resourceSubType.hashCode() : 0); - result = 31 * result + (resourceKey != null ? resourceKey.hashCode() : 0); - result = 31 * result + (isPublic ? 1 : 0); - result = 31 * result + (publicResourceKey != null ? publicResourceKey.hashCode() : 0); - result = 31 * result + (searchText != null ? searchText.hashCode() : 0); - result = 31 * result + (etag != null ? etag.hashCode() : 0); - result = 31 * result + (fileName != null ? fileName.hashCode() : 0); - result = 31 * result + (descriptor != null ? descriptor.hashCode() : 0); - result = 31 * result + (externalId != null ? externalId.hashCode() : 0); - return result; - } } From 82d4cb538112a98ed6425cf56c63e27cceb6731f Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 15 Apr 2025 15:12:18 +0300 Subject: [PATCH 14/63] Add monitoring for calculated fields --- .../monitoring/client/WsClient.java | 69 +++++++++++-------- .../config/transport/TransportInfo.java | 11 ++- .../transport/TransportMonitoringTarget.java | 1 + .../monitoring/data/MonitoredServiceKey.java | 1 + .../data/ServiceFailureException.java | 11 ++- .../monitoring/data/cmd/EntityDataUpdate.java | 18 +++-- .../monitoring/service/BaseHealthChecker.java | 44 ++++++++---- .../service/BaseMonitoringService.java | 37 +++++----- .../service/MonitoringReporter.java | 2 +- .../transport/TransportHealthChecker.java | 44 +++++++++++- .../src/main/resources/tb-monitoring.yml | 11 +++ .../thingsboard/rest/client/RestClient.java | 20 +++++- 12 files changed, 189 insertions(+), 80 deletions(-) diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/client/WsClient.java b/monitoring/src/main/java/org/thingsboard/monitoring/client/WsClient.java index 9246feac27..9bacbdfd45 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/client/WsClient.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/client/WsClient.java @@ -36,8 +36,11 @@ import org.thingsboard.server.common.data.query.EntityListFilter; import javax.net.ssl.SSLParameters; import java.net.URI; import java.nio.channels.NotYetConnectedException; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -48,13 +51,13 @@ import java.util.stream.Collectors; @Slf4j public class WsClient extends WebSocketClient implements AutoCloseable { - public volatile JsonNode lastMsg; + public final List lastMsgs = new ArrayList<>(); private CountDownLatch reply; private CountDownLatch update; private final Lock updateLock = new ReentrantLock(); - private long requestTimeoutMs; + private final long requestTimeoutMs; public WsClient(URI serverUri, long requestTimeoutMs) { super(serverUri); @@ -63,7 +66,6 @@ public class WsClient extends WebSocketClient implements AutoCloseable { @Override public void onOpen(ServerHandshake serverHandshake) { - } @Override @@ -73,8 +75,9 @@ public class WsClient extends WebSocketClient implements AutoCloseable { } updateLock.lock(); try { - lastMsg = JacksonUtil.toJsonNode(s); - log.trace("Received new msg: {}", lastMsg.toPrettyString()); + JsonNode msg = JacksonUtil.toJsonNode(s); + lastMsgs.add(msg); + log.trace("Received new msg: {}", msg.toPrettyString()); if (update != null) { update.countDown(); } @@ -96,11 +99,11 @@ public class WsClient extends WebSocketClient implements AutoCloseable { log.error("WebSocket client error:", e); } - public void registerWaitForUpdate() { + public void registerWaitForUpdates(int count) { updateLock.lock(); try { - lastMsg = null; - update = new CountDownLatch(1); + lastMsgs.clear(); + update = new CountDownLatch(count); } finally { updateLock.unlock(); } @@ -111,6 +114,7 @@ public class WsClient extends WebSocketClient implements AutoCloseable { public void send(String text) throws NotYetConnectedException { updateLock.lock(); try { + lastMsgs.clear(); reply = new CountDownLatch(1); } finally { updateLock.unlock(); @@ -118,19 +122,19 @@ public class WsClient extends WebSocketClient implements AutoCloseable { super.send(text); } - public WsClient subscribeForTelemetry(List devices, String key) { + public WsClient subscribeForTelemetry(List devices, List keys) { EntityDataCmd cmd = new EntityDataCmd(); cmd.setCmdId(RandomUtils.nextInt(0, 1000)); EntityListFilter devicesFilter = new EntityListFilter(); devicesFilter.setEntityType(EntityType.DEVICE); devicesFilter.setEntityList(devices.stream().map(UUID::toString).collect(Collectors.toList())); - EntityDataPageLink pageLink = new EntityDataPageLink(100,0, null, null); + EntityDataPageLink pageLink = new EntityDataPageLink(100, 0, null, null); EntityDataQuery devicesQuery = new EntityDataQuery(devicesFilter, pageLink, Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); cmd.setQuery(devicesQuery); LatestValueCmd latestCmd = new LatestValueCmd(); - latestCmd.setKeys(List.of(new EntityKey(EntityKeyType.TIME_SERIES, key))); + latestCmd.setKeys(keys.stream().map(key -> new EntityKey(EntityKeyType.TIME_SERIES, key)).toList()); cmd.setLatestCmd(latestCmd); CmdsWrapper wrapper = new CmdsWrapper(); @@ -139,12 +143,12 @@ public class WsClient extends WebSocketClient implements AutoCloseable { return this; } - public JsonNode waitForUpdate(long ms) { + public List waitForUpdates(long ms) { log.trace("update latch count: {}", update.getCount()); try { if (update.await(ms, TimeUnit.MILLISECONDS)) { log.trace("Waited for update"); - return getLastMsg(); + return getLastMsgs(); } } catch (InterruptedException e) { log.debug("Failed to await reply", e); @@ -157,7 +161,8 @@ public class WsClient extends WebSocketClient implements AutoCloseable { try { if (reply.await(requestTimeoutMs, TimeUnit.MILLISECONDS)) { log.trace("Waited for reply"); - return getLastMsg(); + List lastMsgs = getLastMsgs(); + return lastMsgs.isEmpty() ? null : lastMsgs.get(0); } } catch (InterruptedException e) { log.debug("Failed to await reply", e); @@ -166,24 +171,30 @@ public class WsClient extends WebSocketClient implements AutoCloseable { throw new IllegalStateException("No WS reply arrived within " + requestTimeoutMs + " ms"); } - private JsonNode getLastMsg() { - if (lastMsg != null) { - JsonNode errorMsg = lastMsg.get("errorMsg"); - if (errorMsg != null && !errorMsg.isNull() && StringUtils.isNotEmpty(errorMsg.asText())) { - throw new RuntimeException("WS error from server: " + errorMsg.asText()); - } else { - return lastMsg; - } - } else { - return null; + private List getLastMsgs() { + if (lastMsgs.isEmpty()) { + return lastMsgs; + } + List errors = lastMsgs.stream() + .map(msg -> msg.get("errorMsg")) + .filter(errorMsg -> errorMsg != null && !errorMsg.isNull() && StringUtils.isNotEmpty(errorMsg.asText())) + .toList(); + if (!errors.isEmpty()) { + throw new RuntimeException("WS error from server: " + errors.stream() + .map(JsonNode::asText) + .collect(Collectors.joining(", "))); } + return lastMsgs; } - public Object getTelemetryUpdate(UUID deviceId, String key) { - JsonNode lastMsg = getLastMsg(); - if (lastMsg == null || lastMsg.isNull()) return null; - EntityDataUpdate update = JacksonUtil.treeToValue(lastMsg, EntityDataUpdate.class); - return update.getLatest(deviceId, key); + public Map getLatest(UUID deviceId) { + Map updates = new HashMap<>(); + getLastMsgs().forEach(msg -> { + EntityDataUpdate update = JacksonUtil.treeToValue(msg, EntityDataUpdate.class); + Map latest = update.getLatest(deviceId); + updates.putAll(latest); + }); + return updates; } @Override diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportInfo.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportInfo.java index 208d28ffee..6b77e1e268 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportInfo.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportInfo.java @@ -20,16 +20,15 @@ import lombok.Data; @Data public class TransportInfo { - private final TransportType transportType; - private final String baseUrl; - private final String queue; + private final TransportType type; + private final TransportMonitoringTarget target; @Override public String toString() { - if (queue.equals("Main")) { - return String.format("*%s* (%s)", transportType.getName(), baseUrl); + if (target.getQueue().equals("Main")) { + return String.format("*%s* (%s)", type.getName(), target.getBaseUrl()); } else { - return String.format("*%s* (%s) _%s_", transportType.getName(), baseUrl, queue); + return String.format("*%s* (%s) _%s_", type.getName(), target.getBaseUrl(), target.getQueue()); } } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java index 1e6a3c8509..e8a9ab03fa 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java @@ -28,6 +28,7 @@ public class TransportMonitoringTarget implements MonitoringTarget { private DeviceConfig device; // set manually during initialization private String queue; private boolean checkDomainIps; + private String namePrefix; @Override public UUID getDeviceId() { diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java index 9c3ee5b786..7579d75231 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java @@ -19,5 +19,6 @@ public class MonitoredServiceKey { public static final String GENERAL = "Monitoring"; public static final String EDQS = "*EDQS*"; + public static final String CF = "*CF*"; } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/ServiceFailureException.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/ServiceFailureException.java index 5f46514bd1..b2592b8719 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/data/ServiceFailureException.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/ServiceFailureException.java @@ -15,14 +15,21 @@ */ package org.thingsboard.monitoring.data; +import lombok.Getter; + +@Getter public class ServiceFailureException extends RuntimeException { - public ServiceFailureException(Throwable cause) { + private final Object serviceKey; + + public ServiceFailureException(Object serviceKey, Throwable cause) { super(cause.getMessage(), cause); + this.serviceKey = serviceKey; } - public ServiceFailureException(String message) { + public ServiceFailureException(Object serviceKey, String message) { super(message); + this.serviceKey = serviceKey; } } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/EntityDataUpdate.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/EntityDataUpdate.java index b82dcbf54d..0706dedcf3 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/EntityDataUpdate.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/cmd/EntityDataUpdate.java @@ -19,9 +19,11 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityKeyType; -import org.thingsboard.server.common.data.query.TsValue; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; @Data @@ -31,14 +33,16 @@ public class EntityDataUpdate { @JsonIgnoreProperties(ignoreUnknown = true) private List update; - public String getLatest(UUID entityId, String key) { - if (update == null) return null; - - return update.stream() + public Map getLatest(UUID entityId) { + if (update == null || update.isEmpty()) { + return Collections.emptyMap(); + } + Map result = new HashMap<>(); + update.stream() .filter(entityData -> entityData.getEntityId().getId().equals(entityId)).findFirst() .map(EntityData::getLatest).map(latest -> latest.get(EntityKeyType.TIME_SERIES)) - .map(latest -> latest.get(key)).map(TsValue::getValue) - .orElse(null); + .ifPresent(latest -> latest.forEach((key, tsValue) -> result.put(key, tsValue.getValue()))); + return result; } } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java index 482b3a30fb..34e0ebc4c5 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java @@ -52,11 +52,14 @@ public abstract class BaseHealthChecker> associates = new HashMap<>(); public static final String TEST_TELEMETRY_KEY = "testData"; + public static final String TEST_CF_TELEMETRY_KEY = "testDataCf"; @PostConstruct private void init() { @@ -68,7 +71,8 @@ public abstract class BaseHealthChecker latest = wsClient.getLatest(target.getDeviceId()); + if (latest.isEmpty()) { + throw new ServiceFailureException(info, "No WS update arrived within " + resultCheckTimeoutMs + " ms"); + } + String actualValue = latest.get(TEST_TELEMETRY_KEY); + if (!testValue.equals(actualValue)) { + throw new ServiceFailureException(info, "Was expecting value " + testValue + " but got " + actualValue); + } + if (checkCalculatedFields) { + String cfTestValue = testValue + "-cf"; + String actualCfValue = latest.get(TEST_CF_TELEMETRY_KEY); + if (actualCfValue == null) { + throw new ServiceFailureException(MonitoredServiceKey.CF, "No CF value arrived"); + } else if (!cfTestValue.equals(actualCfValue)) { + throw new ServiceFailureException(MonitoredServiceKey.CF, "Was expecting CF value " + cfTestValue + " but got " + actualCfValue); + } else { + reporter.serviceIsOk(MonitoredServiceKey.CF); + } } reporter.reportLatency(Latencies.wsUpdate(getKey()), stopWatch.getTime()); } @@ -121,6 +138,7 @@ public abstract class BaseHealthChecker, T extends MonitoringTarget> { @@ -79,7 +81,9 @@ public abstract class BaseMonitoringService, T ext protected ApplicationContext applicationContext; @Value("${monitoring.edqs.enabled:false}") - private boolean edqsMonitoringEnabled; + private boolean checkEdqs; + @Value("${monitoring.calculated_fields.enabled:true}") + protected boolean checkCalculatedFields; @PostConstruct private void init() { @@ -121,7 +125,7 @@ public abstract class BaseMonitoringService, T ext try (WsClient wsClient = wsClientFactory.createClient(accessToken)) { stopWatch.start(); - wsClient.subscribeForTelemetry(devices, TransportHealthChecker.TEST_TELEMETRY_KEY).waitForReply(); + wsClient.subscribeForTelemetry(devices, getTestTelemetryKeys()).waitForReply(); reporter.reportLatency(Latencies.WS_SUBSCRIBE, stopWatch.getTime()); for (BaseHealthChecker healthChecker : healthCheckers) { @@ -129,22 +133,17 @@ public abstract class BaseMonitoringService, T ext } } - if (edqsMonitoringEnabled) { - try { - stopWatch.start(); - checkEdqs(); - reporter.reportLatency(Latencies.EDQS_QUERY, stopWatch.getTime()); - - reporter.serviceIsOk(MonitoredServiceKey.EDQS); - } catch (ServiceFailureException e) { - reporter.serviceFailure(MonitoredServiceKey.EDQS, e); - } catch (Exception e) { - reporter.serviceFailure(MonitoredServiceKey.GENERAL, e); - } + if (checkEdqs) { + stopWatch.start(); + checkEdqs(); + reporter.reportLatency(Latencies.EDQS_QUERY, stopWatch.getTime()); + reporter.serviceIsOk(MonitoredServiceKey.EDQS); } reporter.reportLatencies(tbClient); log.debug("Finished {}", getName()); + } catch (ServiceFailureException e) { + reporter.serviceFailure(e.getServiceKey(), e); } catch (Throwable error) { try { reporter.serviceFailure(MonitoredServiceKey.GENERAL, error); @@ -199,7 +198,7 @@ public abstract class BaseMonitoringService, T ext .collect(Collectors.toSet()); Set missing = Sets.difference(new HashSet<>(this.devices), devices); if (!missing.isEmpty()) { - throw new ServiceFailureException("Missing devices in the response: " + missing); + throw new ServiceFailureException(MonitoredServiceKey.EDQS, "Missing devices in the response: " + missing); } result.getData().stream() @@ -211,7 +210,7 @@ public abstract class BaseMonitoringService, T ext Stream.of("name", "type", "testData").forEach(key -> { TsValue value = values.get(key); if (value == null || StringUtils.isBlank(value.getValue())) { - throw new ServiceFailureException("Missing " + key + " for device " + entityData.getEntityId()); + throw new ServiceFailureException(MonitoredServiceKey.EDQS, "Missing " + key + " for device " + entityData.getEntityId()); } }); }); @@ -232,6 +231,10 @@ public abstract class BaseMonitoringService, T ext .collect(Collectors.toSet()); } + private List getTestTelemetryKeys() { + return checkCalculatedFields ? List.of(TEST_TELEMETRY_KEY, TEST_CF_TELEMETRY_KEY) : List.of(TEST_TELEMETRY_KEY); + } + private void stopHealthChecker(BaseHealthChecker healthChecker) throws Exception { healthChecker.destroyClient(); devices.remove(healthChecker.getTarget().getDeviceId()); diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java index 3554731e58..62ed0d74aa 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java @@ -113,7 +113,7 @@ public class MonitoringReporter { public void serviceFailure(Object serviceKey, Throwable error) { if (log.isDebugEnabled()) { - log.error("Error occurred", error); + log.error("[{}] Error occurred", serviceKey, error); } int failuresCount = failuresCounters.computeIfAbsent(serviceKey, k -> new AtomicInteger()).incrementAndGet(); ServiceFailureNotification notification = new ServiceFailureNotification(serviceKey, error, failuresCount); diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java index f5e6cb5469..b892f5d609 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java @@ -32,6 +32,14 @@ import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapClientCredentials; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecBootstrapClientCredential; @@ -47,6 +55,8 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; +import java.util.Map; + @Slf4j public abstract class TransportHealthChecker extends BaseHealthChecker { @@ -74,7 +84,7 @@ public abstract class TransportHealthChecker getImages(PageLink pageLink, boolean includeSystemImages) { - return this.getImages(pageLink, null, includeSystemImages); + return this.getImages(pageLink, null, includeSystemImages); } public PageData getImages(PageLink pageLink, ResourceSubType imageSubType, boolean includeSystemImages) { @@ -4056,6 +4057,21 @@ public class RestClient implements Closeable { timeout).getBody(); } + public CalculatedField saveCalculatedField(CalculatedField calculatedField) { + return restTemplate.postForEntity(baseURL + "/api/calculatedField", calculatedField, CalculatedField.class).getBody(); + } + + public PageData getCalculatedFieldsByEntityId(EntityId entityId, PageLink pageLink) { + Map params = new HashMap<>(); + addPageLinkToParam(params, pageLink); + return restTemplate.exchange( + baseURL + "/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields?" + getUrlParams(pageLink), + HttpMethod.GET, HttpEntity.EMPTY, + new ParameterizedTypeReference>() { + }, params).getBody(); + + } + private String getTimeUrlParams(TimePageLink pageLink) { String urlParams = getUrlParams(pageLink); if (pageLink.getStartTime() != null) { From a8a0083bb2d430288794badf90501436b9a8cdaf Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Tue, 15 Apr 2025 15:16:55 +0300 Subject: [PATCH 15/63] Monitoring: trim device name --- .../service/transport/TransportHealthChecker.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java index b892f5d609..d002546335 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java @@ -97,7 +97,7 @@ public abstract class TransportHealthChecker Date: Tue, 15 Apr 2025 15:37:20 +0300 Subject: [PATCH 16/63] Monitoring: minor refactoring --- .../monitoring/service/BaseHealthChecker.java | 8 ++++---- .../service/transport/TransportHealthChecker.java | 11 ++++++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java index 34e0ebc4c5..1e9cdbe191 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java @@ -52,8 +52,6 @@ public abstract class BaseHealthChecker> associates = new HashMap<>(); @@ -71,7 +69,7 @@ public abstract class BaseHealthChecker extends BaseHealthChecker { + @Value("${monitoring.calculated_fields.enabled:true}") + private boolean calculatedFieldsMonitoringEnabled; + public TransportHealthChecker(C config, TransportMonitoringTarget target) { super(config, target); } @@ -100,7 +104,7 @@ public abstract class TransportHealthChecker Date: Wed, 16 Apr 2025 16:18:21 +0200 Subject: [PATCH 17/63] invalidate async js scripts --- .../api/AbstractScriptInvokeService.java | 4 +- .../api/js/AbstractJsInvokeService.java | 17 +++ .../script/api/js/JsValidator.java | 120 ++++++++++++++++++ .../api/js/AbstractJsInvokeServiceTest.java | 91 +++++++++++++ .../script/api/js/JsValidatorTest.java | 88 +++++++++++++ 5 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 common/script/script-api/src/main/java/org/thingsboard/script/api/js/JsValidator.java create mode 100644 common/script/script-api/src/test/java/org/thingsboard/script/api/js/AbstractJsInvokeServiceTest.java create mode 100644 common/script/script-api/src/test/java/org/thingsboard/script/api/js/JsValidatorTest.java diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java index 3d9b855064..f7b640d3af 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java @@ -269,7 +269,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService } } - private boolean scriptBodySizeExceeded(String scriptBody) { + public boolean scriptBodySizeExceeded(String scriptBody) { if (getMaxScriptBodySize() <= 0) return false; return scriptBody.length() > getMaxScriptBodySize(); } @@ -297,7 +297,7 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService return result != null && result.length() > getMaxResultSize(); } - private ListenableFuture error(String message) { + public ListenableFuture error(String message) { return Futures.immediateFailedFuture(new RuntimeException(message)); } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java index b6ddf16e66..d8fae31290 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java @@ -35,6 +35,8 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import static java.lang.String.format; + /** * Created by ashvayka on 26.09.18. */ @@ -93,6 +95,21 @@ public abstract class AbstractJsInvokeService extends AbstractScriptInvokeServic doRelease(scriptId, scriptInfoMap.remove(scriptId)); } + @Override + public ListenableFuture eval(TenantId tenantId, ScriptType scriptType, String scriptBody, String... argNames) { + if (!isExecEnabled(tenantId)) { + return error("Script Execution is disabled due to API limits!"); + } + if (scriptBodySizeExceeded(scriptBody)) { + return error(format("Script body exceeds maximum allowed size of %s symbols", getMaxScriptBodySize())); + } + final String validationIssue = JsValidator.validate(scriptBody); + if (validationIssue != null ) { + return error(validationIssue); + } + return super.eval(tenantId, scriptType, scriptBody, argNames); + } + protected abstract ListenableFuture doEval(UUID scriptId, JsScriptInfo jsInfo, String scriptBody); protected abstract ListenableFuture doInvokeFunction(UUID scriptId, JsScriptInfo jsInfo, Object[] args); diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/JsValidator.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/JsValidator.java new file mode 100644 index 0000000000..9c65064802 --- /dev/null +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/JsValidator.java @@ -0,0 +1,120 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.script.api.js; + +import java.util.regex.Pattern; + +public class JsValidator { + + static final Pattern ASYNC_PATTERN = Pattern.compile("\\basync\\b"); + static final Pattern AWAIT_PATTERN = Pattern.compile("\\bawait\\b"); + static final Pattern PROMISE_PATTERN = Pattern.compile("\\bPromise\\b"); + static final Pattern SET_TIMEOUT_PATTERN = Pattern.compile("\\bsetTimeout\\b"); + + public static String validate(String scriptBody) { + if (scriptBody == null || scriptBody.trim().isEmpty()) { + return "Script body is empty"; + } + + //Quick check + if (!ASYNC_PATTERN.matcher(scriptBody).find() + && !AWAIT_PATTERN.matcher(scriptBody).find() + && !PROMISE_PATTERN.matcher(scriptBody).find() + && !SET_TIMEOUT_PATTERN.matcher(scriptBody).find()) { + return null; + } + + //Recheck if quick check failed. Ignoring comments and strings + String[] lines = scriptBody.split("\\r?\\n"); + boolean insideMultilineComment = false; + + for (String line : lines) { + String stripped = line; + + // Handle multiline comments + if (insideMultilineComment) { + if (line.contains("*/")) { + insideMultilineComment = false; + stripped = line.substring(line.indexOf("*/") + 2); // continue after comment + } else { + continue; // skip line inside multiline comment + } + } + + // Check for start of multiline comment + if (stripped.contains("/*")) { + int start = stripped.indexOf("/*"); + int end = stripped.indexOf("*/", start + 2); + + if (end != -1) { + // Inline multiline comment + stripped = stripped.substring(0, start) + stripped.substring(end + 2); + } else { + // Starts a block comment, continues on next lines + insideMultilineComment = true; + stripped = stripped.substring(0, start); + } + } + + stripped = stripInlineComment(stripped); + stripped = stripStringLiterals(stripped); + + if (ASYNC_PATTERN.matcher(stripped).find()) { + return "Script must not contain 'async' keyword."; + } + if (AWAIT_PATTERN.matcher(stripped).find()) { + return "Script must not contain 'await' keyword."; + } + if (PROMISE_PATTERN.matcher(stripped).find()) { + return "Script must not use 'Promise'."; + } + if (SET_TIMEOUT_PATTERN.matcher(stripped).find()) { + return "Script must not use 'setTimeout' method."; + } + } + return null; + } + + private static String stripInlineComment(String line) { + int index = line.indexOf("//"); + return index >= 0 ? line.substring(0, index) : line; + } + + private static String stripStringLiterals(String line) { + StringBuilder sb = new StringBuilder(); + boolean inSingleQuote = false; + boolean inDoubleQuote = false; + + for (int i = 0; i < line.length(); i++) { + char c = line.charAt(i); + + if (c == '"' && !inSingleQuote) { + inDoubleQuote = !inDoubleQuote; + continue; + } else if (c == '\'' && !inDoubleQuote) { + inSingleQuote = !inSingleQuote; + continue; + } + + if (!inSingleQuote && !inDoubleQuote) { + sb.append(c); + } + } + + return sb.toString(); + } + +} diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/js/AbstractJsInvokeServiceTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/js/AbstractJsInvokeServiceTest.java new file mode 100644 index 0000000000..760ca9e3fb --- /dev/null +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/js/AbstractJsInvokeServiceTest.java @@ -0,0 +1,91 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.script.api.js; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import org.thingsboard.server.common.stats.StatsCounter; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.script.api.ScriptType; +import org.thingsboard.server.common.data.id.TenantId; + +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@Slf4j +class AbstractJsInvokeServiceTest { + + AbstractJsInvokeService service; + final UUID id = UUID.randomUUID(); + + @BeforeEach + void setUp() { + service = mock(AbstractJsInvokeService.class, Mockito.RETURNS_DEEP_STUBS); + + ReflectionTestUtils.setField(service, "requestsCounter", mock(StatsCounter.class)); + ReflectionTestUtils.setField(service, "evalCallback", mock(FutureCallback.class)); + + // Make sure core checks always pass + doReturn(true).when(service).isExecEnabled(any()); + doReturn(false).when(service).scriptBodySizeExceeded(anyString()); + doReturn(Futures.immediateFuture(id)).when(service).doEvalScript(any(), any(), anyString(), any(), any(String[].class)); + + // Use real implementation of eval() + doCallRealMethod().when(service).eval(any(), any(), any(), any(String[].class)); + doCallRealMethod().when(service).error(anyString()); + } + + @Test + void shouldReturnValidationErrorFromJsValidator() throws ExecutionException, InterruptedException { + String scriptWithAsync = "async function test() {}"; + + var future = service.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, scriptWithAsync, "a", "b"); + ExecutionException ex = assertThrows(ExecutionException.class, future::get); + assertTrue(ex.getCause().getMessage().contains("Script must not contain 'async' keyword.")); + assertThat(ex.getCause()).isInstanceOf(RuntimeException.class); + verify(service).isExecEnabled(any()); + verify(service).scriptBodySizeExceeded(any()); + } + + @Test + void shouldPassValidationAndCallSuperEval() throws ExecutionException, InterruptedException, TimeoutException { + String validScript = "function test() { return 42; }"; + var result = service.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, validScript, "x", "y"); + + assertThat(result.get(30, TimeUnit.SECONDS)).isEqualTo(id); + // two times, non-optimal + verify(service, times(2)).isExecEnabled(any()); + verify(service, times(2)).scriptBodySizeExceeded(any()); + } + +} diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/js/JsValidatorTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/js/JsValidatorTest.java new file mode 100644 index 0000000000..c1dbf7a58b --- /dev/null +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/js/JsValidatorTest.java @@ -0,0 +1,88 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.script.api.js; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +class JsValidatorTest { + + @ParameterizedTest(name = "should return error for script \"{0}\"") + @ValueSource(strings = { + "async function test() {}", + "const result = await someFunc();", + "const result =\nawait\tsomeFunc();", + "setTimeout(1000);", + "new Promise((resolve) => {});", + "function test() { return 42; } \n\t await test()", + """ + function init() { + await doSomething(); + } + """, + }) + void shouldReturnErrorForInvalidScripts(String script) { + assertNotNull(JsValidator.validate(script)); + } + + @ParameterizedTest(name = "should pass validation for script: \"{0}\"") + @ValueSource(strings = { + "function test() { return 42; }", + "const result = 10 * 2;", + "// async is a keyword but not used: 'const word = \"async\";'", + "let note = 'setTimeout tight';", + + "const word = \"async\";", + "const word = \"setTimeout\";", + "const word = \"Promise\";", + "const word = \"await\";", + + "const word = 'async';", + "const word = 'setTimeout';", + "const word = 'Promise';", + "const word = 'await';", + + "//function test() { return 42; }", + "// const result = 10 * 2;", + "// async is a keyword but not used: 'const word = \"async\";'", + "//setTimeout(1);", + + "a=b+c; // await for a day", + "return new // Promise((resolve) => {", + "hello(); // async is a keyword but not used: 'const word = \"async\";'", + "setGoal(a); //setTimeout(1);", + + " /* new Promise((resolve) => {}); // */ return 'await';", + " /* async */ function calc() {", + "/* async function abc() { \n await new Promise ( \t setTimeout () ) \n } \n*/", + }) + void shouldReturnNullForValidScripts(String script) { + assertNull(JsValidator.validate(script)); + } + + @ParameterizedTest(name = "should return 'Script body is empty' for input: \"{0}\"") + @NullAndEmptySource + @ValueSource(strings = {" ", "\t", "\n"}) + void shouldReturnErrorForEmptyOrNullScripts(String script) { + assertEquals("Script body is empty", JsValidator.validate(script)); + } + +} From 5a46a170e45c67f77ba7062579400ba421618c78 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Wed, 16 Apr 2025 16:47:20 +0200 Subject: [PATCH 18/63] MQTT client log added: Scheduling reconnect in [{}] sec --- .../src/main/java/org/thingsboard/mqtt/MqttClientImpl.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java index 344886165e..59a680662f 100644 --- a/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java +++ b/netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java @@ -194,7 +194,10 @@ final class MqttClientImpl implements MqttClient { if (reconnect) { this.reconnect = true; } - eventLoop.schedule((Runnable) () -> connect(host, port, reconnect), reconnectStrategy.getNextReconnectDelay(), TimeUnit.SECONDS); + + final long nextReconnectDelay = reconnectStrategy.getNextReconnectDelay(); + log.info("[{}] Scheduling reconnect in [{}] sec", channel != null ? channel.id() : "UNKNOWN", nextReconnectDelay); + eventLoop.schedule((Runnable) () -> connect(host, port, reconnect), nextReconnectDelay, TimeUnit.SECONDS); } } From e4e97350742e7e05e598c6bbe866a315535e60dd Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 22 Apr 2025 11:23:42 +0300 Subject: [PATCH 19/63] Cleanup deprecated API in SubscriptionManagerService --- .../queue/DefaultTbCoreConsumerService.java | 17 ++++---------- .../DefaultSubscriptionManagerService.java | 22 +++++++------------ .../SubscriptionManagerService.java | 9 -------- 3 files changed, 12 insertions(+), 36 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index 47a0c473e9..d2fe8a837d 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -552,19 +552,10 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService keys, TbCallback callback) { - onAttributesDelete(tenantId, entityId, scope, keys, false, callback); - } - - @Override - public void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys, boolean notifyDevice, TbCallback callback) { - processAttributesUpdate(entityId, scope, - keys.stream().map(key -> new BaseAttributeKvEntry(0, new StringDataEntry(key, ""))).collect(Collectors.toList())); - if (entityId.getEntityType() == EntityType.DEVICE && TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) { - clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete(tenantId, new DeviceId(entityId.getId()), scope, keys), null); + try { + List deletedEntries = keys.stream() + .map(key -> new BaseAttributeKvEntry(0L, new StringDataEntry(key, ""))) + .toList(); + processAttributesUpdate(entityId, scope, deletedEntries); + } catch (Exception e) { + callback.onFailure(e); + return; } callback.onSuccess(); } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java index 57d4fda5f8..3fd58a1243 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java @@ -41,15 +41,6 @@ public interface SubscriptionManagerService extends ApplicationListener keys, TbCallback empty); - /** - * This method is retained solely for backwards compatibility, specifically to handle - * legacy proto messages that include the notifyDevice field. - * - * @deprecated as of 4.0, this method will be removed in future releases. - */ - @Deprecated(forRemoval = true, since = "4.0") - void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys, boolean notifyDevice, TbCallback empty); - void onTimeSeriesDelete(TenantId tenantId, EntityId entityId, List keys, TbCallback callback); void onAlarmUpdate(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback); From 2a50e2eaa53b445488154297ab67bdcc9a672b30 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Tue, 22 Apr 2025 16:38:52 +0200 Subject: [PATCH 20/63] AbstractScriptInvokeService: validate script refactored --- .../api/AbstractScriptInvokeService.java | 26 ++-- .../api/js/AbstractJsInvokeService.java | 16 +-- .../api/AbstractScriptInvokeServiceTest.java | 122 ++++++++++++++++++ .../api/js/AbstractJsInvokeServiceTest.java | 8 +- 4 files changed, 149 insertions(+), 23 deletions(-) create mode 100644 common/script/script-api/src/test/java/org/thingsboard/script/api/AbstractScriptInvokeServiceTest.java diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java index f7b640d3af..0b3890b4bc 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/AbstractScriptInvokeService.java @@ -134,19 +134,29 @@ public abstract class AbstractScriptInvokeService implements ScriptInvokeService } } - @Override - public ListenableFuture eval(TenantId tenantId, ScriptType scriptType, String scriptBody, String... argNames) { + public String validate(TenantId tenantId, String scriptBody) { if (isExecEnabled(tenantId)) { if (scriptBodySizeExceeded(scriptBody)) { - return error(format("Script body exceeds maximum allowed size of %s symbols", getMaxScriptBodySize())); + return format("Script body exceeds maximum allowed size of %s symbols", getMaxScriptBodySize()); } - UUID scriptId = UUID.randomUUID(); - requestsCounter.increment(); - return withTimeoutAndStatsCallback(scriptId, null, - doEvalScript(tenantId, scriptType, scriptBody, scriptId, argNames), evalCallback, getMaxEvalRequestsTimeout()); } else { - return error("Script Execution is disabled due to API limits!"); + return "Script Execution is disabled due to API limits!"; } + + return null; + } + + @Override + public ListenableFuture eval(TenantId tenantId, ScriptType scriptType, String scriptBody, String... argNames) { + String validationError = validate(tenantId, scriptBody); + if (validationError != null) { + return error(validationError); + } + + UUID scriptId = UUID.randomUUID(); + requestsCounter.increment(); + return withTimeoutAndStatsCallback(scriptId, null, + doEvalScript(tenantId, scriptType, scriptBody, scriptId, argNames), evalCallback, getMaxEvalRequestsTimeout()); } @Override diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java index d8fae31290..c859f49629 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/js/AbstractJsInvokeService.java @@ -96,18 +96,12 @@ public abstract class AbstractJsInvokeService extends AbstractScriptInvokeServic } @Override - public ListenableFuture eval(TenantId tenantId, ScriptType scriptType, String scriptBody, String... argNames) { - if (!isExecEnabled(tenantId)) { - return error("Script Execution is disabled due to API limits!"); + public String validate(TenantId tenantId, String scriptBody) { + String errorMessage = super.validate(tenantId, scriptBody); + if (errorMessage == null) { + return JsValidator.validate(scriptBody); } - if (scriptBodySizeExceeded(scriptBody)) { - return error(format("Script body exceeds maximum allowed size of %s symbols", getMaxScriptBodySize())); - } - final String validationIssue = JsValidator.validate(scriptBody); - if (validationIssue != null ) { - return error(validationIssue); - } - return super.eval(tenantId, scriptType, scriptBody, argNames); + return errorMessage; } protected abstract ListenableFuture doEval(UUID scriptId, JsScriptInfo jsInfo, String scriptBody); diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/AbstractScriptInvokeServiceTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/AbstractScriptInvokeServiceTest.java new file mode 100644 index 0000000000..35f201ed3b --- /dev/null +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/AbstractScriptInvokeServiceTest.java @@ -0,0 +1,122 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.script.api; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.test.util.ReflectionTestUtils; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.stats.StatsCounter; + +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +class AbstractScriptInvokeServiceTest { + + AbstractScriptInvokeService service; + final UUID id = UUID.randomUUID(); + final String scriptBody = "return true;"; + final TenantId tenantId = TenantId.fromUUID(UUID.fromString("2ed9a658-45a5-4812-b212-9931f5749f30")); + + @BeforeEach + void setUp() { + service = mock(AbstractScriptInvokeService.class, Mockito.RETURNS_DEEP_STUBS); + + // Make sure core checks always pass + doReturn(true).when(service).isExecEnabled(any()); + doReturn(50000L).when(service).getMaxScriptBodySize(); + + // Use real implementations + doCallRealMethod().when(service).scriptBodySizeExceeded(anyString()); + doCallRealMethod().when(service).eval(any(), any(), any(), any(String[].class)); + doCallRealMethod().when(service).error(anyString()); + doCallRealMethod().when(service).validate(any(), anyString()); + } + + @Test + void evalWithValidationCallTest() throws ExecutionException, InterruptedException, TimeoutException { + ReflectionTestUtils.setField(service, "requestsCounter", mock(StatsCounter.class)); + ReflectionTestUtils.setField(service, "evalCallback", mock(FutureCallback.class)); + + doReturn(Futures.immediateFuture(id)).when(service).doEvalScript(any(), any(), anyString(), any(), any(String[].class)); + + var future = service.eval(tenantId, ScriptType.RULE_NODE_SCRIPT, scriptBody, "x", "y"); + + assertThat(future.get(30, TimeUnit.SECONDS)).isEqualTo(id); + verify(service).validate(any(), anyString()); + verify(service).validate(tenantId, scriptBody); + verify(service, never()).error(anyString()); + } + + @Test + void evalWithValidationCallErrorTest() throws ExecutionException, InterruptedException, TimeoutException { + doReturn(false).when(service).isExecEnabled(any()); + var future = service.eval(tenantId, ScriptType.RULE_NODE_SCRIPT, scriptBody, "x", "y"); + + ExecutionException ex = assertThrows(ExecutionException.class, future::get); + assertThat(ex.getCause().getMessage()).isEqualTo("Script Execution is disabled due to API limits!"); + assertThat(ex.getCause()).isInstanceOf(RuntimeException.class); + + verify(service).validate(any(), anyString()); + verify(service).validate(tenantId, scriptBody); + verify(service).error(anyString()); + } + + @Test + void validateScriptBodyTestExecEnabledTest() { + assertNull(service.validate(tenantId, scriptBody)); + verify(service).isExecEnabled(tenantId); + } + + @Test + void validateScriptBodyTestExecDisabledTest() { + doReturn(false).when(service).isExecEnabled(tenantId); + assertThat(service.validate(tenantId, scriptBody)).isEqualTo("Script Execution is disabled due to API limits!"); + verify(service).isExecEnabled(tenantId); + } + + @Test + void validateScriptBodySizeOKTest() { + assertNull(service.validate(tenantId, scriptBody)); + verify(service).isExecEnabled(tenantId); + verify(service).scriptBodySizeExceeded(scriptBody); + } + + @Test + void validateScriptBodySizeExceededTest() { + doReturn(10L).when(service).getMaxScriptBodySize(); + assertThat(service.validate(tenantId, scriptBody)).isEqualTo("Script body exceeds maximum allowed size of 10 symbols"); + verify(service).isExecEnabled(tenantId); + verify(service).scriptBodySizeExceeded(scriptBody); + } + +} diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/js/AbstractJsInvokeServiceTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/js/AbstractJsInvokeServiceTest.java index 760ca9e3fb..3f7d61919d 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/js/AbstractJsInvokeServiceTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/js/AbstractJsInvokeServiceTest.java @@ -60,9 +60,10 @@ class AbstractJsInvokeServiceTest { doReturn(false).when(service).scriptBodySizeExceeded(anyString()); doReturn(Futures.immediateFuture(id)).when(service).doEvalScript(any(), any(), anyString(), any(), any(String[].class)); - // Use real implementation of eval() + // Use real implementations doCallRealMethod().when(service).eval(any(), any(), any(), any(String[].class)); doCallRealMethod().when(service).error(anyString()); + doCallRealMethod().when(service).validate(any(), anyString()); } @Test @@ -83,9 +84,8 @@ class AbstractJsInvokeServiceTest { var result = service.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, validScript, "x", "y"); assertThat(result.get(30, TimeUnit.SECONDS)).isEqualTo(id); - // two times, non-optimal - verify(service, times(2)).isExecEnabled(any()); - verify(service, times(2)).scriptBodySizeExceeded(any()); + verify(service, times(1)).isExecEnabled(any()); + verify(service, times(1)).scriptBodySizeExceeded(any()); } } From 1131fda582d3dbfe26883f6c11a67414cfa3aadf Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Tue, 29 Apr 2025 12:00:34 +0300 Subject: [PATCH 21/63] lwm2m: add new field: elemetryObserveStrategy observeStrategy; --- .../device/DeviceBulkImportService.java | 3 +- .../lwm2m/AbstractLwM2MIntegrationTest.java | 14 ++++++ .../TransportConfigurationTest.java | 42 ++++++++++++++++++ .../lwm2m/TelemetryMappingConfiguration.java | 22 +++++++++- .../lwm2m/TelemetryObserveStrategy.java | 44 +++++++++++++++++++ 5 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/TransportConfigurationTest.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryObserveStrategy.java diff --git a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java index 2924f8ddeb..d042fb2657 100644 --- a/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java +++ b/application/src/main/java/org/thingsboard/server/service/device/DeviceBulkImportService.java @@ -68,6 +68,7 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import static org.eclipse.leshan.core.LwM2m.Version.V1_0; +import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.SINGLE; @Service @TbCoreComponent @@ -258,7 +259,7 @@ public class DeviceBulkImportService extends AbstractBulkImportService { Lwm2mDeviceProfileTransportConfiguration transportConfiguration = new Lwm2mDeviceProfileTransportConfiguration(); transportConfiguration.setBootstrap(Collections.emptyList()); transportConfiguration.setClientLwM2mSettings(new OtherConfiguration(false,1, 1, 1, PowerMode.DRX, null, null, null, null, null, V1_0.toString())); - transportConfiguration.setObserveAttr(new TelemetryMappingConfiguration(Collections.emptyMap(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptyMap())); + transportConfiguration.setObserveAttr(new TelemetryMappingConfiguration(Collections.emptyMap(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptyMap(), SINGLE)); DeviceProfileData deviceProfileData = new DeviceProfileData(); DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration(); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index 827112cf2a..76f1811b2d 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -208,6 +208,20 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte " \"pagingTransmissionWindow\": null,\n" + " \"clientOnlyObserveAfterConnect\": 1\n" + " }"; + public static String TELEMETRY_WITH_STRATEGY_COMPOSITE_ALL = + " {\n" + + " \"keyName\": {\n" + + " \"/3_1.2/0/9\": \"batteryLevel\"\n" + + " },\n" + + " \"observe\": [],\n" + + " \"attribute\": [\n" + + " ],\n" + + " \"telemetry\": [\n" + + " \"/3_1.2/0/9\"\n" + + " ],\n" + + " \"attributeLwm2m\": {},\n" + + " \"observeStrategy\": 1\n" + + " }"; protected final Set expectedStatusesRegistrationLwm2mSuccess = new HashSet<>(Arrays.asList(ON_INIT, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS)); protected final Set expectedStatusesRegistrationLwm2mSuccessUpdate = new HashSet<>(Arrays.asList(ON_INIT, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS, ON_UPDATE_STARTED, ON_UPDATE_SUCCESS)); protected final Set expectedStatusesRegistrationBsSuccess = new HashSet<>(Arrays.asList(ON_BOOTSTRAP_STARTED, ON_BOOTSTRAP_SUCCESS, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS)); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/TransportConfigurationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/TransportConfigurationTest.java new file mode 100644 index 0000000000..85181c398d --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/TransportConfigurationTest.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.transportConfiguration; + +import org.junit.Assert; +import org.junit.Test; +import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; +import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; + +import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.COMPOSITE_ALL; +import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.SINGLE; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; + +public class TransportConfigurationTest extends AbstractSecurityLwM2MIntegrationTest { + + @Test + public void testTransportConfigurationObserveStrategyBeforeParseNullAfterParseNotNull_STRATEGY_SINGLE() throws Exception { + Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITHOUT_OBSERVE, getBootstrapServerCredentialsNoSec(NONE)); + Assert.assertNotNull(transportConfiguration.getObserveAttr().getObserveStrategy()); + Assert.assertEquals(SINGLE, transportConfiguration.getObserveAttr().getObserveStrategy()); + } + + @Test + public void testTransportConfigurationObserveStrategyBeforeParseNotNullAfterParseNotNull_STRATEGY_COMPOSITE_ALL() throws Exception { + Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITH_STRATEGY_COMPOSITE_ALL, getBootstrapServerCredentialsNoSec(NONE)); + Assert.assertNotNull(transportConfiguration.getObserveAttr().getObserveStrategy()); + Assert.assertEquals(COMPOSITE_ALL, transportConfiguration.getObserveAttr().getObserveStrategy()); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java index e76f699d7c..1669e1a101 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryMappingConfiguration.java @@ -15,17 +15,18 @@ */ package org.thingsboard.server.common.data.device.profile.lwm2m; -import lombok.AllArgsConstructor; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; +import java.util.Collections; import java.util.Map; import java.util.Set; @Data @NoArgsConstructor -@AllArgsConstructor public class TelemetryMappingConfiguration implements Serializable { private static final long serialVersionUID = -7594999741305410419L; @@ -35,5 +36,22 @@ public class TelemetryMappingConfiguration implements Serializable { private Set attribute; private Set telemetry; private Map attributeLwm2m; + private TelemetryObserveStrategy observeStrategy; + @JsonCreator + public TelemetryMappingConfiguration( + @JsonProperty("keyName") Map keyName, + @JsonProperty("observe") Set observe, + @JsonProperty("attribute") Set attribute, + @JsonProperty("telemetry") Set telemetry, + @JsonProperty("attributeLwm2m") Map attributeLwm2m, + @JsonProperty("observeStrategy") TelemetryObserveStrategy observeStrategy) { + + this.keyName = keyName != null ? keyName : Collections.emptyMap(); + this.observe = observe != null ? observe : Collections.emptySet(); + this.attribute = attribute != null ? attribute : Collections.emptySet(); + this.telemetry = telemetry != null ? telemetry : Collections.emptySet(); + this.attributeLwm2m = attributeLwm2m != null ? attributeLwm2m : Collections.emptyMap(); + this.observeStrategy = observeStrategy != null ? observeStrategy : TelemetryObserveStrategy.SINGLE; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryObserveStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryObserveStrategy.java new file mode 100644 index 0000000000..28d571c0b4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryObserveStrategy.java @@ -0,0 +1,44 @@ +package org.thingsboard.server.common.data.device.profile.lwm2m; + +import lombok.Getter; + +public enum TelemetryObserveStrategy { + + SINGLE("One resource equals one single observe request", 0), + COMPOSITE_ALL("All resources in one composite observe request", 1), + COMPOSITE_BY_OBJECT("Grouped composite observe requests by object", 2); + + @Getter + private final String description; + + @Getter + private final int id; + + TelemetryObserveStrategy(String description, int id) { + this.description = description; + this.id = id; + } + + public static TelemetryObserveStrategy fromDescription(String description) { + for (TelemetryObserveStrategy strategy : values()) { + if (strategy.description.equalsIgnoreCase(description)) { + return strategy; + } + } + return null; + } + + public static TelemetryObserveStrategy fromId(int id) { + for (TelemetryObserveStrategy strategy : values()) { + if (strategy.id == id) { + return strategy; + } + } + return null; + } + + @Override + public String toString() { + return name() + " (" + id + "): " + description; + } +} From 3f2fd6f979d0b70db090d7bff48a54ead932867e Mon Sep 17 00:00:00 2001 From: deaflynx Date: Tue, 29 Apr 2025 17:39:54 +0300 Subject: [PATCH 22/63] UI: lwm2m observe strategy feature. --- ...ile-transport-configuration.component.html | 14 +++++++ ...ofile-transport-configuration.component.ts | 26 ++++++++++++- .../lwm2m/lwm2m-profile-config.models.ts | 39 ++++++++++++++++++- .../assets/locale/locale.constant-en_US.json | 9 +++++ 4 files changed, 85 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html index f4dbafb6e8..ac7d0677fa 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html @@ -20,6 +20,20 @@
+ + device-profile.lwm2m.observe-strategy.observe-strategy + + + {{ observeStrategyMap.get(lwm2mDeviceProfileFormGroup.get('observeStrategy').value)?.name | translate }} + + + {{ observeStrategyMap.get(strategy).name | translate }} + + {{ observeStrategyMap.get(strategy).description | translate }} + + + + object; @Input() @@ -102,6 +107,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro observeAttrTelemetry: [null], bootstrapServerUpdateEnable: [false], bootstrap: [[]], + observeStrategy: [null, []], clientLwM2mSettings: this.fb.group({ clientOnlyObserveAfterConnect: [1, []], useObject19ForOtaInfo: [false], @@ -173,6 +179,10 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro } }); + this.lwm2mDeviceProfileFormGroup.get('objectIds').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => this.updateObserveStrategy(value)); + this.lwm2mDeviceProfileFormGroup.valueChanges.pipe( takeUntil(this.destroy$) ).subscribe((value) => { @@ -261,6 +271,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro observeAttrTelemetry: this.getObserveAttrTelemetryObjects(value), bootstrap: this.configurationValue.bootstrap, bootstrapServerUpdateEnable: this.configurationValue.bootstrapServerUpdateEnable || false, + observeStrategy: this.configurationValue.observeAttr.observeStrategy || ObserveStrategy.SINGLE, clientLwM2mSettings: { clientOnlyObserveAfterConnect: this.configurationValue.clientLwM2mSettings.clientOnlyObserveAfterConnect, useObject19ForOtaInfo: this.configurationValue.clientLwM2mSettings.useObject19ForOtaInfo ?? false, @@ -283,6 +294,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro this.lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.fwUpdateStrategy').updateValueAndValidity({onlySelf: true}); this.lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.swUpdateStrategy').updateValueAndValidity({onlySelf: true}); } + this.updateObserveStrategy(value); this.cd.markForCheck(); } @@ -427,6 +439,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro const telemetryArray: Array = []; const attributes: any = {}; const keyNameNew = {}; + const observeStrategyValue = val.length ? this.lwm2mDeviceProfileFormGroup.get('observeStrategy')?.value : ObserveStrategy.SINGLE; const observeJson: ObjectLwM2M[] = JSON.parse(JSON.stringify(val)); observeJson.forEach(obj => { if (isDefinedAndNotNull(obj.attributes) && !isEmpty(obj.attributes)) { @@ -467,7 +480,8 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro attribute: attributeArray, telemetry: telemetryArray, keyName: this.sortObjectKeyPathJson(KEY_NAME, keyNameNew), - attributeLwm2m: attributes + attributeLwm2m: attributes, + observeStrategy: observeStrategyValue }; } @@ -568,4 +582,12 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro return this.lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings') as UntypedFormGroup; } + private updateObserveStrategy(value: ObjectLwM2M[]) { + if (value.length && !this.disabled) { + this.lwm2mDeviceProfileFormGroup.get('observeStrategy').enable({onlySelf: true}); + } else { + this.lwm2mDeviceProfileFormGroup.get('observeStrategy').disable({onlySelf: true}); + } + } + } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts index 3972964a5b..48b6c3284c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts @@ -136,6 +136,41 @@ export const ObjectIDVerTranslationMap = new Map( ] ); +export interface ObserveStrategyData { + name: string; + description: string; +} + +export enum ObserveStrategy { + SINGLE = 'SINGLE', + COMPOSITE_ALL = 'COMPOSITE_ALL', + COMPOSITE_BY_OBJECT = 'COMPOSITE_BY_OBJECT' +} + +export const ObserveStrategyMap = new Map([ + [ + ObserveStrategy.SINGLE, + { + name: 'device-profile.lwm2m.observe-strategy.single', + description: 'device-profile.lwm2m.observe-strategy.single-description' + } + ], + [ + ObserveStrategy.COMPOSITE_ALL, + { + name: 'device-profile.lwm2m.observe-strategy.composite-all', + description: 'device-profile.lwm2m.observe-strategy.composite-all-description' + } + ], + [ + ObserveStrategy.COMPOSITE_BY_OBJECT, + { + name: 'device-profile.lwm2m.observe-strategy.composite-by-object', + description: 'device-profile.lwm2m.observe-strategy.composite-by-object-description' + } + ] +]); + export interface ServerSecurityConfig { host?: string; port?: number; @@ -187,6 +222,7 @@ export interface ObservableAttributes { telemetry: string[]; keyName: {}; attributeLwm2m: AttributesNameValueMap; + observeStrategy: ObserveStrategy; } export function getDefaultProfileObserveAttrConfig(): ObservableAttributes { @@ -195,7 +231,8 @@ export function getDefaultProfileObserveAttrConfig(): ObservableAttributes { attribute: [], telemetry: [], keyName: {}, - attributeLwm2m: {} + attributeLwm2m: {}, + observeStrategy: ObserveStrategy.SINGLE }; } 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 b160a44b0e..91008c69a6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2209,6 +2209,15 @@ "v1-0": "1.0", "v1-1": "1.1", "v1-2": "1.2" + }, + "observe-strategy": { + "observe-strategy": "Observe strategy", + "single": "Single", + "single-description": "One resource equals one single observe request", + "composite-all": "Composite all", + "composite-all-description": "All resources in one composite observe request", + "composite-by-object": "Composite by objects", + "composite-by-object-description": "Grouped composite observe requests by object" } }, "snmp": { From b9706295ae5ee08ae137acd3ad99ad6b8ce672ed Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Tue, 29 Apr 2025 18:01:32 +0300 Subject: [PATCH 23/63] lwm2m: add new field: elemetryObserveStrategy observeStrategy -license:format --- .../profile/lwm2m/TelemetryObserveStrategy.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryObserveStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryObserveStrategy.java index 28d571c0b4..56d85c4b6f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryObserveStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryObserveStrategy.java @@ -1,3 +1,18 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.thingsboard.server.common.data.device.profile.lwm2m; import lombok.Getter; From cff17821c5546e8b48c852f09c408235b66f8e4b Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Tue, 29 Apr 2025 19:06:29 +0300 Subject: [PATCH 24/63] lwm2m: add observeComposite ti init Client --- .../lwm2m/AbstractLwM2MIntegrationTest.java | 94 +++++++++++++++---- .../client/LwM2mBinaryAppDataContainer.java | 6 +- .../AbstractSecurityLwM2MIntegrationTest.java | 2 +- .../sql/NoSecLwM2MIntegrationTest.java | 26 ++++- .../TransportConfigurationTest.java | 2 +- .../uplink/DefaultLwM2mUplinkMsgHandler.java | 50 ++++++++-- 6 files changed, 143 insertions(+), 37 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index 76f1811b2d..022221ac8e 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -34,6 +34,7 @@ import org.springframework.http.HttpStatus; import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.script.api.tbel.TbDate; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileProvisionType; @@ -196,6 +197,28 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte " \"attributeLwm2m\": {}\n" + " }"; + public static String TELEMETRY_WITH_COMPOSITE_OBSERVE = + " {\n" + + " \"keyName\": {\n" + + " \"/3_1.2/0/9\": \"batteryLevel\",\n" + + " \"/3_1.2/0/20\": \"batteryStatus\",\n" + + " \"/19_1.1/0/2\": \"dataCreationTime\"\n" + + " },\n" + + " \"observe\": [\n" + + " \"/3_1.2/0/9\",\n" + + " \"/3_1.2/0/20\",\n" + + " \"/19_1.1/0/2\"\n" + + " ],\n" + + " \"attribute\": [],\n" + + " \"telemetry\": [\n" + + " \"/3_1.2/0/9\",\n" + + " \"/3_1.2/0/20\",\n" + + " \"/19_1.1/0/2\"\n" + + " ],\n" + + " \"attributeLwm2m\": {},\n" + + " \"observeStrategy\": 1\n" + + " }"; + public static final String CLIENT_LWM2M_SETTINGS = " {\n" + " \"edrxCycle\": null,\n" + @@ -208,20 +231,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte " \"pagingTransmissionWindow\": null,\n" + " \"clientOnlyObserveAfterConnect\": 1\n" + " }"; - public static String TELEMETRY_WITH_STRATEGY_COMPOSITE_ALL = - " {\n" + - " \"keyName\": {\n" + - " \"/3_1.2/0/9\": \"batteryLevel\"\n" + - " },\n" + - " \"observe\": [],\n" + - " \"attribute\": [\n" + - " ],\n" + - " \"telemetry\": [\n" + - " \"/3_1.2/0/9\"\n" + - " ],\n" + - " \"attributeLwm2m\": {},\n" + - " \"observeStrategy\": 1\n" + - " }"; + protected final Set expectedStatusesRegistrationLwm2mSuccess = new HashSet<>(Arrays.asList(ON_INIT, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS)); protected final Set expectedStatusesRegistrationLwm2mSuccessUpdate = new HashSet<>(Arrays.asList(ON_INIT, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS, ON_UPDATE_STARTED, ON_UPDATE_SUCCESS)); protected final Set expectedStatusesRegistrationBsSuccess = new HashSet<>(Arrays.asList(ON_BOOTSTRAP_STARTED, ON_BOOTSTRAP_SUCCESS, ON_REGISTRATION_STARTED, ON_REGISTRATION_SUCCESS)); @@ -261,10 +271,10 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte } } - public void basicTestConnectionObserveTelemetry(Security security, - LwM2MDeviceCredentials deviceCredentials, - String endpoint, - boolean queueMode) throws Exception { + public void basicTestConnectionObserveSingleTelemetry(Security security, + LwM2MDeviceCredentials deviceCredentials, + String endpoint, + boolean queueMode) throws Exception { Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITHOUT_OBSERVE, getBootstrapServerCredentialsNoSec(NONE)); DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + endpoint, transportConfiguration); Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId()); @@ -298,8 +308,56 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte int expectedMin = 5; Assert.assertTrue(expectedMax >= Long.parseLong(tsValue.getValue())); Assert.assertTrue(expectedMin <= Long.parseLong(tsValue.getValue())); + } + + public void basicTestConnectionObserveCompositeTelemetry(Security security, + LwM2MDeviceCredentials deviceCredentials, + String endpoint, + Lwm2mDeviceProfileTransportConfiguration transportConfiguration, + int cntObserve) throws Exception { + + DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + endpoint, transportConfiguration); + Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId()); + + SingleEntityFilter sef = new SingleEntityFilter(); + sef.setSingleEntity(device.getId()); + LatestValueCmd latestCmd = new LatestValueCmd(); + String key1 = "batteryLevel"; + String key2 = "dataCreationTime"; + latestCmd.setKeys(Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, key1))); + latestCmd.setKeys(Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, key2))); + EntityDataQuery edq = new EntityDataQuery(sef, new EntityDataPageLink(1, 0, null, null), + Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + EntityDataCmd cmd = new EntityDataCmd(2, edq, null, latestCmd, null); + getWsClient().send(cmd); + getWsClient().waitForReply(); + getWsClient().registerWaitForUpdate(); + this.createNewClient(security, null, false, endpoint, null, true, device.getId().getId().toString()); + awaitObserveReadAll(cntObserve, lwM2MTestClient.getDeviceIdStr()); + String msg = getWsClient().waitForUpdate(); + + EntityDataUpdate update = JacksonUtil.fromString(msg, EntityDataUpdate.class); + Assert.assertEquals(2, update.getCmdId()); + List eData = update.getUpdate(); + Assert.assertNotNull(eData); + Assert.assertEquals(1, eData.size()); + Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); + Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES)); + var tsValue1 = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get(key1); + var tsValue2 = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get(key2); + if (tsValue1 != null) { + Assert.assertThat(Long.parseLong(tsValue1.getValue()), instanceOf(Long.class)); + int expectedMax = 50; + int expectedMin = 5; + Assert.assertTrue(expectedMax >= Long.parseLong(tsValue1.getValue())); + Assert.assertTrue(expectedMin <= Long.parseLong(tsValue1.getValue())); + } else { + String pattern = "MMM dd, yyyy HH:mm a"; + TbDate d = new TbDate(tsValue2.getValue(), pattern, "en-US"); + Assert.assertNotNull(d); + } } protected DeviceProfile createLwm2mDeviceProfile(String name, Lwm2mDeviceProfileTransportConfiguration transportConfiguration) throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java index 41a2259790..9bd23caea6 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2mBinaryAppDataContainer.java @@ -27,9 +27,6 @@ import org.eclipse.leshan.core.response.WriteResponse; import javax.security.auth.Destroyable; import java.sql.Time; -import java.time.Instant; -import java.time.LocalTime; -import java.time.ZoneId; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -187,8 +184,7 @@ public class LwM2mBinaryAppDataContainer extends BaseInstanceEnabler implements } private Time getTimestamp() { - LocalTime localTime = LocalTime.ofInstant(Instant.now(), ZoneId.systemDefault()); - this.timestamp = Time.valueOf(localTime); + setTimestamp(); return this.timestamp; } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java index 076371055d..2367670d5f 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java @@ -119,7 +119,7 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M protected final PrivateKey clientPrivateKeyFromCertTrust; // client private key used for X509 and RPK protected final X509Certificate clientX509CertTrustNo; // client certificate signed by intermediate, rootCA with a good CN ("host name") protected final PrivateKey clientPrivateKeyFromCertTrustNo; // client private key used for X509 and RPK - private final String[] RESOURCES_SECURITY = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml"}; + private final String[] RESOURCES_SECURITY = new String[]{"1.xml", "2.xml", "3.xml", "5.xml", "9.xml", "19.xml"}; private final LwM2MBootstrapClientCredentials defaultBootstrapCredentials; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java index d8e0b28b76..34683ffc71 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java @@ -17,8 +17,10 @@ package org.thingsboard.server.transport.lwm2m.security.sql; import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; +import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; +import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.COMPOSITE_BY_OBJECT; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOOTSTRAP_ONLY; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOTH; @@ -32,13 +34,31 @@ public class NoSecLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationT public void testWithNoSecConnectLwm2mSuccessAndObserveTelemetry() throws Exception { String clientEndpoint = CLIENT_ENDPOINT_NO_SEC; LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); - super.basicTestConnectionObserveTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, false); + super.basicTestConnectionObserveSingleTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, false); } + + @Test + public void testWithNoSecQueueModeConnectLwm2mSuccessAndObserveSingleTelemetry() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_NO_SEC + "_QueueMode"; + LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); + super.basicTestConnectionObserveSingleTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, true); + } + + @Test + public void testWithNoSecQueueModeConnectLwm2mSuccessAndObserveCompositeAllTelemetry() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_NO_SEC + "_QueueMode"; + LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); + Lwm2mDeviceProfileTransportConfiguration transportConfiguration = super.getTransportConfiguration(TELEMETRY_WITH_COMPOSITE_OBSERVE, getBootstrapServerCredentialsNoSec(NONE)); + super.basicTestConnectionObserveCompositeTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, transportConfiguration, 1); + } + @Test - public void testWithNoSecQueueModeConnectLwm2mSuccessAndObserveTelemetry() throws Exception { + public void testWithNoSecQueueModeConnectLwm2mSuccessAndObserveCompositeByObjectTelemetry() throws Exception { String clientEndpoint = CLIENT_ENDPOINT_NO_SEC + "_QueueMode"; LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); - super.basicTestConnectionObserveTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, true); + Lwm2mDeviceProfileTransportConfiguration transportConfiguration = super.getTransportConfiguration(TELEMETRY_WITH_COMPOSITE_OBSERVE, getBootstrapServerCredentialsNoSec(NONE)); + transportConfiguration.getObserveAttr().setObserveStrategy(COMPOSITE_BY_OBJECT); + super.basicTestConnectionObserveCompositeTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, transportConfiguration, 2); } // Bootstrap + Lwm2m diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/TransportConfigurationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/TransportConfigurationTest.java index 85181c398d..757bc81b41 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/TransportConfigurationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/TransportConfigurationTest.java @@ -35,7 +35,7 @@ public class TransportConfigurationTest extends AbstractSecurityLwM2MIntegration @Test public void testTransportConfigurationObserveStrategyBeforeParseNotNullAfterParseNotNull_STRATEGY_COMPOSITE_ALL() throws Exception { - Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITH_STRATEGY_COMPOSITE_ALL, getBootstrapServerCredentialsNoSec(NONE)); + Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITH_COMPOSITE_OBSERVE, getBootstrapServerCredentialsNoSec(NONE)); Assert.assertNotNull(transportConfiguration.getObserveAttr().getObserveStrategy()); Assert.assertEquals(COMPOSITE_ALL, transportConfiguration.getObserveAttr().getObserveStrategy()); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java index 9976783179..b7d6e0e7f5 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java @@ -60,6 +60,7 @@ import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTrans import org.thingsboard.server.common.data.device.profile.lwm2m.ObjectAttributes; import org.thingsboard.server.common.data.device.profile.lwm2m.OtherConfiguration; import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryMappingConfiguration; +import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.ota.OtaPackageUtil; @@ -92,6 +93,8 @@ import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MReadCallbac import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MReadRequest; import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteAttributesCallback; import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteAttributesRequest; +import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MObserveCompositeCallback; +import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MObserveCompositeRequest; import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService; import org.thingsboard.server.transport.lwm2m.server.model.LwM2MModelConfig; import org.thingsboard.server.transport.lwm2m.server.model.LwM2MModelConfigService; @@ -116,9 +119,11 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; +import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.COMPOSITE_ALL; +import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.COMPOSITE_BY_OBJECT; +import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.SINGLE; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; import static org.thingsboard.server.common.data.util.CollectionsUtil.diffSets; import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.FW_3_VER_ID; @@ -475,7 +480,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl Set supportedObjects = clientContext.getSupportedIdVerInClient(lwM2MClient); if (supportedObjects != null && supportedObjects.size() > 0) { this.sendReadRequests(lwM2MClient, profile, supportedObjects); - this.sendObserveRequests(lwM2MClient, profile, supportedObjects); + this.sendInitObserveRequests(lwM2MClient, profile, supportedObjects); this.sendWriteAttributeRequests(lwM2MClient, profile, supportedObjects); // Removed. Used only for debug. // this.sendDiscoverRequests(lwM2MClient, profile, supportedObjects); @@ -501,16 +506,27 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl } } - private void sendObserveRequests(LwM2mClient lwM2MClient, Lwm2mDeviceProfileTransportConfiguration profile, Set supportedObjects) { + private void sendInitObserveRequests(LwM2mClient lwM2MClient, Lwm2mDeviceProfileTransportConfiguration profile, Set supportedObjects) { try { Set targetIds = profile.getObserveAttr().getObserve(); targetIds = targetIds.stream().filter(target -> isSupportedTargetId(supportedObjects, target)).collect(Collectors.toSet()); - - CountDownLatch latch = new CountDownLatch(targetIds.size()); - targetIds.forEach(targetId -> sendObserveRequest(lwM2MClient, targetId, - new TbLwM2MLatchCallback<>(latch, new TbLwM2MObserveCallback(this, logService, lwM2MClient, targetId)))); - - latch.await(config.getTimeout(), TimeUnit.MILLISECONDS); + if (!targetIds.isEmpty()) { + TelemetryObserveStrategy observeStrategy = profile.getObserveAttr().getObserveStrategy(); + if (SINGLE.equals(observeStrategy)) { + CountDownLatch latch = new CountDownLatch(targetIds.size()); + targetIds.forEach(targetId -> sendObserveRequest(lwM2MClient, targetId, + new TbLwM2MLatchCallback<>(latch, new TbLwM2MObserveCallback(this, logService, lwM2MClient, targetId)))); + latch.await(config.getTimeout(), TimeUnit.MILLISECONDS); + } else if (COMPOSITE_ALL.equals(observeStrategy)) { + String[] versionedIds = targetIds.toArray(new String[0]); + sendObserveCompositeRequest(lwM2MClient, versionedIds); + } else if (COMPOSITE_BY_OBJECT.equals(observeStrategy)) { + Map versionedObjectIds = groupByObjectIdVersionedIds(targetIds); + CountDownLatch latch = new CountDownLatch(versionedObjectIds.size()); + versionedObjectIds.forEach((k, v)-> sendObserveCompositeRequest(lwM2MClient, v)); + latch.await(config.getTimeout(), TimeUnit.MILLISECONDS); + } + } } catch (InterruptedException e) { log.error("[{}] Failed to await Observe requests!", lwM2MClient.getEndpoint(), e); } catch (Exception e) { @@ -547,6 +563,12 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl TbLwM2MObserveRequest request = TbLwM2MObserveRequest.builder().versionedId(versionedId).timeout(clientContext.getRequestTimeout(lwM2MClient)).build(); defaultLwM2MDownlinkMsgHandler.sendObserveRequest(lwM2MClient, request, callback); } + private void sendObserveCompositeRequest(LwM2mClient lwM2MClient, String[] versionedIds) { + + TbLwM2MObserveCompositeRequest request = TbLwM2MObserveCompositeRequest.builder().versionedIds(versionedIds).timeout(clientContext.getRequestTimeout(lwM2MClient)).build(); + var mainCallback = new TbLwM2MObserveCompositeCallback(this, logService, lwM2MClient, versionedIds); + defaultLwM2MDownlinkMsgHandler.sendObserveCompositeRequest(lwM2MClient, request, mainCallback); + } private void sendWriteAttributesRequest(LwM2mClient lwM2MClient, String targetId, ObjectAttributes params) { TbLwM2MWriteAttributesRequest request = TbLwM2MWriteAttributesRequest.builder().versionedId(targetId).attributes(params).timeout(clientContext.getRequestTimeout(lwM2MClient)).build(); @@ -1024,4 +1046,14 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl } } + private Map groupByObjectIdVersionedIds(Set targetIds){ + return targetIds.stream() + .collect(Collectors.groupingBy( + id -> new LwM2mPath(fromVersionedIdToObjectId(id)).getObjectId(), + Collectors.collectingAndThen( + Collectors.toList(), + list -> list.toArray(new String[0]) + ) + )); + } } From 64bd969bee5dd15c3cb18deb53caf7f3d7ad6b9d Mon Sep 17 00:00:00 2001 From: deaflynx Date: Wed, 30 Apr 2025 12:42:47 +0300 Subject: [PATCH 25/63] UI: lwm2m observe strategies updated descriptions. --- ...2m-device-profile-transport-configuration.component.html | 2 +- ui-ngx/src/assets/locale/locale.constant-en_US.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html index ac7d0677fa..4897f94e11 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html @@ -28,7 +28,7 @@ {{ observeStrategyMap.get(strategy).name | translate }} - + {{ observeStrategyMap.get(strategy).description | translate }} 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 91008c69a6..8c8c7dd9f3 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2213,11 +2213,11 @@ "observe-strategy": { "observe-strategy": "Observe strategy", "single": "Single", - "single-description": "One resource equals one single observe request", + "single-description": "One Observe request per resource (higher precision, more network traffic)", "composite-all": "Composite all", - "composite-all-description": "All resources in one composite observe request", + "composite-all-description": "All resources are observed with a single Composite Observe request (more efficient, less flexible)", "composite-by-object": "Composite by objects", - "composite-by-object-description": "Grouped composite observe requests by object" + "composite-by-object-description": "Resources are grouped by object type and observed using separate Composite Observe requests (balanced approach)" } }, "snmp": { From 8c840f7c6f0928bc1038cad5460fa75782ce36ec Mon Sep 17 00:00:00 2001 From: deaflynx Date: Tue, 29 Apr 2025 17:39:54 +0300 Subject: [PATCH 26/63] UI: lwm2m observe strategy feature. --- ...ile-transport-configuration.component.html | 14 +++++++ ...ofile-transport-configuration.component.ts | 26 ++++++++++++- .../lwm2m/lwm2m-profile-config.models.ts | 39 ++++++++++++++++++- .../assets/locale/locale.constant-en_US.json | 9 +++++ 4 files changed, 85 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html index f4dbafb6e8..ac7d0677fa 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html @@ -20,6 +20,20 @@
+ + device-profile.lwm2m.observe-strategy.observe-strategy + + + {{ observeStrategyMap.get(lwm2mDeviceProfileFormGroup.get('observeStrategy').value)?.name | translate }} + + + {{ observeStrategyMap.get(strategy).name | translate }} + + {{ observeStrategyMap.get(strategy).description | translate }} + + + + object; @Input() @@ -102,6 +107,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro observeAttrTelemetry: [null], bootstrapServerUpdateEnable: [false], bootstrap: [[]], + observeStrategy: [null, []], clientLwM2mSettings: this.fb.group({ clientOnlyObserveAfterConnect: [1, []], useObject19ForOtaInfo: [false], @@ -173,6 +179,10 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro } }); + this.lwm2mDeviceProfileFormGroup.get('objectIds').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe(value => this.updateObserveStrategy(value)); + this.lwm2mDeviceProfileFormGroup.valueChanges.pipe( takeUntil(this.destroy$) ).subscribe((value) => { @@ -261,6 +271,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro observeAttrTelemetry: this.getObserveAttrTelemetryObjects(value), bootstrap: this.configurationValue.bootstrap, bootstrapServerUpdateEnable: this.configurationValue.bootstrapServerUpdateEnable || false, + observeStrategy: this.configurationValue.observeAttr.observeStrategy || ObserveStrategy.SINGLE, clientLwM2mSettings: { clientOnlyObserveAfterConnect: this.configurationValue.clientLwM2mSettings.clientOnlyObserveAfterConnect, useObject19ForOtaInfo: this.configurationValue.clientLwM2mSettings.useObject19ForOtaInfo ?? false, @@ -283,6 +294,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro this.lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.fwUpdateStrategy').updateValueAndValidity({onlySelf: true}); this.lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings.swUpdateStrategy').updateValueAndValidity({onlySelf: true}); } + this.updateObserveStrategy(value); this.cd.markForCheck(); } @@ -427,6 +439,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro const telemetryArray: Array = []; const attributes: any = {}; const keyNameNew = {}; + const observeStrategyValue = val.length ? this.lwm2mDeviceProfileFormGroup.get('observeStrategy')?.value : ObserveStrategy.SINGLE; const observeJson: ObjectLwM2M[] = JSON.parse(JSON.stringify(val)); observeJson.forEach(obj => { if (isDefinedAndNotNull(obj.attributes) && !isEmpty(obj.attributes)) { @@ -467,7 +480,8 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro attribute: attributeArray, telemetry: telemetryArray, keyName: this.sortObjectKeyPathJson(KEY_NAME, keyNameNew), - attributeLwm2m: attributes + attributeLwm2m: attributes, + observeStrategy: observeStrategyValue }; } @@ -568,4 +582,12 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro return this.lwm2mDeviceProfileFormGroup.get('clientLwM2mSettings') as UntypedFormGroup; } + private updateObserveStrategy(value: ObjectLwM2M[]) { + if (value.length && !this.disabled) { + this.lwm2mDeviceProfileFormGroup.get('observeStrategy').enable({onlySelf: true}); + } else { + this.lwm2mDeviceProfileFormGroup.get('observeStrategy').disable({onlySelf: true}); + } + } + } diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts index 3972964a5b..48b6c3284c 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-profile-config.models.ts @@ -136,6 +136,41 @@ export const ObjectIDVerTranslationMap = new Map( ] ); +export interface ObserveStrategyData { + name: string; + description: string; +} + +export enum ObserveStrategy { + SINGLE = 'SINGLE', + COMPOSITE_ALL = 'COMPOSITE_ALL', + COMPOSITE_BY_OBJECT = 'COMPOSITE_BY_OBJECT' +} + +export const ObserveStrategyMap = new Map([ + [ + ObserveStrategy.SINGLE, + { + name: 'device-profile.lwm2m.observe-strategy.single', + description: 'device-profile.lwm2m.observe-strategy.single-description' + } + ], + [ + ObserveStrategy.COMPOSITE_ALL, + { + name: 'device-profile.lwm2m.observe-strategy.composite-all', + description: 'device-profile.lwm2m.observe-strategy.composite-all-description' + } + ], + [ + ObserveStrategy.COMPOSITE_BY_OBJECT, + { + name: 'device-profile.lwm2m.observe-strategy.composite-by-object', + description: 'device-profile.lwm2m.observe-strategy.composite-by-object-description' + } + ] +]); + export interface ServerSecurityConfig { host?: string; port?: number; @@ -187,6 +222,7 @@ export interface ObservableAttributes { telemetry: string[]; keyName: {}; attributeLwm2m: AttributesNameValueMap; + observeStrategy: ObserveStrategy; } export function getDefaultProfileObserveAttrConfig(): ObservableAttributes { @@ -195,7 +231,8 @@ export function getDefaultProfileObserveAttrConfig(): ObservableAttributes { attribute: [], telemetry: [], keyName: {}, - attributeLwm2m: {} + attributeLwm2m: {}, + observeStrategy: ObserveStrategy.SINGLE }; } 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 b160a44b0e..91008c69a6 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2209,6 +2209,15 @@ "v1-0": "1.0", "v1-1": "1.1", "v1-2": "1.2" + }, + "observe-strategy": { + "observe-strategy": "Observe strategy", + "single": "Single", + "single-description": "One resource equals one single observe request", + "composite-all": "Composite all", + "composite-all-description": "All resources in one composite observe request", + "composite-by-object": "Composite by objects", + "composite-by-object-description": "Grouped composite observe requests by object" } }, "snmp": { From de589fb41cd130a830839c43fa76076fc98c1b57 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Wed, 30 Apr 2025 12:42:47 +0300 Subject: [PATCH 27/63] UI: lwm2m observe strategies updated descriptions. --- ...2m-device-profile-transport-configuration.component.html | 2 +- ui-ngx/src/assets/locale/locale.constant-en_US.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html index ac7d0677fa..4897f94e11 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html @@ -28,7 +28,7 @@ {{ observeStrategyMap.get(strategy).name | translate }} - + {{ observeStrategyMap.get(strategy).description | translate }} 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 91008c69a6..8c8c7dd9f3 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -2213,11 +2213,11 @@ "observe-strategy": { "observe-strategy": "Observe strategy", "single": "Single", - "single-description": "One resource equals one single observe request", + "single-description": "One Observe request per resource (higher precision, more network traffic)", "composite-all": "Composite all", - "composite-all-description": "All resources in one composite observe request", + "composite-all-description": "All resources are observed with a single Composite Observe request (more efficient, less flexible)", "composite-by-object": "Composite by objects", - "composite-by-object-description": "Grouped composite observe requests by object" + "composite-by-object-description": "Resources are grouped by object type and observed using separate Composite Observe requests (balanced approach)" } }, "snmp": { From be323e3c208207507ca44336ef49d38396055f31 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Wed, 30 Apr 2025 17:38:21 +0300 Subject: [PATCH 28/63] UI: lwm2m observe strategies minor updates. --- ...vice-profile-transport-configuration.component.html | 10 +++++----- ...device-profile-transport-configuration.component.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html index 4897f94e11..94b4b70207 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html @@ -20,6 +20,11 @@
+ + device-profile.lwm2m.observe-strategy.observe-strategy @@ -34,11 +39,6 @@ - - diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts index dc6016d5c0..c1189103f9 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts @@ -439,7 +439,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro const telemetryArray: Array = []; const attributes: any = {}; const keyNameNew = {}; - const observeStrategyValue = val.length ? this.lwm2mDeviceProfileFormGroup.get('observeStrategy')?.value : ObserveStrategy.SINGLE; + const observeStrategyValue = this.lwm2mDeviceProfileFormGroup.get('observeStrategy').value; const observeJson: ObjectLwM2M[] = JSON.parse(JSON.stringify(val)); observeJson.forEach(obj => { if (isDefinedAndNotNull(obj.attributes) && !isEmpty(obj.attributes)) { From 8ebf996565321523e22dba1f54a3f86cf48de248 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Wed, 30 Apr 2025 17:38:21 +0300 Subject: [PATCH 29/63] UI: lwm2m observe strategies minor updates. --- ...vice-profile-transport-configuration.component.html | 10 +++++----- ...device-profile-transport-configuration.component.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html index 4897f94e11..94b4b70207 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.html @@ -20,6 +20,11 @@
+ + device-profile.lwm2m.observe-strategy.observe-strategy @@ -34,11 +39,6 @@ - - diff --git a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts index dc6016d5c0..c1189103f9 100644 --- a/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-device-profile-transport-configuration.component.ts @@ -439,7 +439,7 @@ export class Lwm2mDeviceProfileTransportConfigurationComponent implements Contro const telemetryArray: Array = []; const attributes: any = {}; const keyNameNew = {}; - const observeStrategyValue = val.length ? this.lwm2mDeviceProfileFormGroup.get('observeStrategy')?.value : ObserveStrategy.SINGLE; + const observeStrategyValue = this.lwm2mDeviceProfileFormGroup.get('observeStrategy').value; const observeJson: ObjectLwM2M[] = JSON.parse(JSON.stringify(val)); observeJson.forEach(obj => { if (isDefinedAndNotNull(obj.attributes) && !isEmpty(obj.attributes)) { From 3066d172944a72cf717f52ada4a2f5640a04b6c1 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 1 May 2025 08:47:12 +0300 Subject: [PATCH 30/63] UI: Hint for dynamic settings and refactoring flow animation --- .../bottom-right-elbow-connector-hp.svg | 18 +- .../scada_symbols/bottom-tee-connector-hp.svg | 16 +- .../scada_symbols/cross-connector-hp.svg | 16 +- .../scada_symbols/horizontal-connector-hp.svg | 19 +- .../left-bottom-elbow-connector-hp.svg | 23 ++- .../scada_symbols/left-tee-connector-hp.svg | 18 +- .../left-top-elbow-connector-hp.svg | 18 +- .../long-horizontal-connector-hp.svg | 25 ++- .../long-vertical-connector-hp.svg | 22 ++- .../scada_symbols/right-tee-connector-hp.svg | 16 +- .../top-right-elbow-connector-hp.svg | 18 +- .../scada_symbols/top-tee-connector-hp.svg | 16 +- .../scada_symbols/vertical-connector-hp.svg | 22 ++- .../widget/lib/scada/scada-symbol.models.ts | 171 ++++++++++++++++++ ...dynamic-form-property-panel.component.html | 6 + .../dynamic-form-property-panel.component.ts | 1 + .../dynamic-form/dynamic-form.component.html | 10 +- .../app/shared/models/dynamic-form.models.ts | 3 + .../assets/locale/locale.constant-en_US.json | 1 + 19 files changed, 357 insertions(+), 82 deletions(-) diff --git a/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg index fa273dc8ec..b76f7399cc 100644 --- a/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg @@ -3,7 +3,7 @@ "description": "Bottom right elbow connector", "widgetSizeX": 1, "widgetSizeY": 1, - "stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `-${dashWidth + (dashGap || dashWidth)}` : `${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(``);\n}\n", + "stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 200,100 L 125,100 Q 100,100 100,125 L 100, 200';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n", "tags": [ { "tag": "line", @@ -140,13 +140,14 @@ }, { "id": "mainLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 6, "required": true, "subLabel": "Main", "divider": true, "fieldSuffix": "px", + "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, @@ -155,21 +156,23 @@ }, { "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 2, "required": true, "subLabel": "Secondary", + "divider": true, "fieldSuffix": "px", + "condition": "return !model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, - "visible": true + "visible": false }, { "id": "lineColor", - "name": "{i18n:scada.symbol.line-color}", + "name": "{i18n:scada.symbol.main-line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -182,6 +185,7 @@ "type": "number", "default": 4, "subLabel": "Width", + "divider": true, "fieldSuffix": "px", "min": 1, "step": 1, @@ -200,6 +204,7 @@ { "id": "flowStyleDash", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, @@ -215,6 +220,7 @@ { "id": "flowStyleGap", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, @@ -250,5 +256,5 @@ } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg index 2feb95e9ff..28e0788d35 100644 --- a/application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg @@ -3,7 +3,7 @@ "description": "Bottom tee connector", "widgetSizeX": 1, "widgetSizeY": 1, - "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst rightLine = \"M100 100H200\";\nconst bottomLine = \"M 100,200 V 103\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('right', rightLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(``);\n}", + "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst leftLineReversed = \"M 100,100 H 0\";\nconst rightLine = \"M100 100H200\";\nconst rightLineReversed = \"M 200,100 H 100\";\nconst bottomLine = \"M 100,200 V 103\";\nconst bottomLineReversed = \"M 100,103 V 200\";\n\nprepareFlowAnimation('left', leftLine, leftLineReversed);\nprepareFlowAnimation('right', rightLine, rightLineReversed);\nprepareFlowAnimation('bottom', bottomLine, bottomLineReversed);\n\nfunction prepareFlowAnimation(prefix, line, reversedLine) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const duration = 1 / flowAnimationSpeed;\n \n let animateFlow = ctx.api.connectorAnimation(animation);\n \n if (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, reversedLine).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n } else {\n if (animateFlow) {\n animateFlow.finish();\n }\n }\n}\n", "tags": [ { "tag": "line", @@ -377,13 +377,14 @@ }, { "id": "mainLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 6, "required": true, "subLabel": "Main", "divider": true, "fieldSuffix": "px", + "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, @@ -392,21 +393,23 @@ }, { "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 2, "required": true, "subLabel": "Secondary", + "divider": true, "fieldSuffix": "px", + "condition": "return !model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, - "visible": true + "visible": false }, { "id": "lineColor", - "name": "{i18n:scada.symbol.line-color}", + "name": "{i18n:scada.symbol.main-line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -419,6 +422,7 @@ "type": "number", "default": 4, "subLabel": "Width", + "divider": true, "fieldSuffix": "px", "min": 1, "step": 1, @@ -437,6 +441,7 @@ { "id": "flowStyleDash", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, @@ -452,6 +457,7 @@ { "id": "flowStyleGap", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, diff --git a/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg index ab6798b48b..480666a589 100644 --- a/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg @@ -3,7 +3,7 @@ "description": "Cross connector", "widgetSizeX": 1, "widgetSizeY": 1, - "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst topLine = \"M100 97L100 0\";\nconst rightLine = \"M100 100H200\";\nconst bottomLine = \"M 100,200 V 103\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('right', rightLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(``);\n}", + "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst leftLineReversed = \"M 100,100 H 0\";\nconst topLine = \"M100 97L100 0\";\nconst topLineReversed = \"M 100,0 V 97\";\nconst rightLine = \"M100 100H200\";\nconst rightLineReversed = \"M 200,100 H 100\";\nconst bottomLine = \"M 100,200 V 103\";\nconst bottomLineReversed = \"M 100,103 V 200\";\n\nprepareFlowAnimation('left', leftLine, leftLineReversed);\nprepareFlowAnimation('top', topLine, topLineReversed);\nprepareFlowAnimation('right', rightLine, rightLineReversed);\nprepareFlowAnimation('bottom', bottomLine, bottomLineReversed);\n\nfunction prepareFlowAnimation(prefix, line, reversedLine) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const duration = 1 / flowAnimationSpeed;\n \n let animateFlow = ctx.api.connectorAnimation(animation);\n \n if (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, reversedLine).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n } else {\n if (animateFlow) {\n animateFlow.finish();\n }\n }\n}", "tags": [ { "tag": "line", @@ -493,13 +493,14 @@ }, { "id": "mainLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 6, "required": true, "subLabel": "Main", "divider": true, "fieldSuffix": "px", + "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, @@ -508,21 +509,23 @@ }, { "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 2, "required": true, "subLabel": "Secondary", + "divider": true, "fieldSuffix": "px", + "condition": "return !model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, - "visible": true + "visible": false }, { "id": "lineColor", - "name": "{i18n:scada.symbol.line-color}", + "name": "{i18n:scada.symbol.main-line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -535,6 +538,7 @@ "type": "number", "default": 4, "subLabel": "Width", + "divider": true, "fieldSuffix": "px", "min": 1, "step": 1, @@ -553,6 +557,7 @@ { "id": "flowStyleDash", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, @@ -568,6 +573,7 @@ { "id": "flowStyleGap", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg index 74d048e884..230c7b68a5 100644 --- a/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg @@ -3,7 +3,7 @@ "description": "Horizontal connector with an optional directional arrow to visually indicate flow.", "widgetSizeX": 1, "widgetSizeY": 1, - "stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(``);\n}\n", + "stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 200,100 H 0';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n\n", "tags": [ { "tag": "arrow", @@ -176,13 +176,14 @@ }, { "id": "mainLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 6, "required": true, "subLabel": "Main", "divider": true, "fieldSuffix": "px", + "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, @@ -191,21 +192,23 @@ }, { "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 2, "required": true, "subLabel": "Secondary", + "divider": true, "fieldSuffix": "px", + "condition": "return !model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, - "visible": true + "visible": false }, { "id": "lineColor", - "name": "{i18n:scada.symbol.line-color}", + "name": "{i18n:scada.symbol.main-line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -229,11 +232,14 @@ "name": "{i18n:scada.symbol.flow}", "group": "{i18n:scada.symbol.animation}", "type": "color", - "default": "#C8DFF7" + "default": "#C8DFF7", + "disabled": false, + "visible": true }, { "id": "flowStyleDash", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, @@ -249,6 +255,7 @@ { "id": "flowStyleGap", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, diff --git a/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg index fd14834d12..1dbeec5f92 100644 --- a/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg @@ -3,7 +3,7 @@ "description": "Left bottom elbow connector", "widgetSizeX": 1, "widgetSizeY": 1, - "stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(``);\n}\n", + "stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 100,200 L 100,125 Q 100,100 75,100 L 0, 100';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n", "tags": [ { "tag": "line", @@ -140,13 +140,14 @@ }, { "id": "mainLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 6, "required": true, "subLabel": "Main", "divider": true, "fieldSuffix": "px", + "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, @@ -155,21 +156,24 @@ }, { "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 2, "required": true, "subLabel": "Secondary", + "divider": true, "fieldSuffix": "px", + "disableOnProperty": "mainLine", + "condition": "return !model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, - "visible": true + "visible": false }, { "id": "lineColor", - "name": "{i18n:scada.symbol.line-color}", + "name": "{i18n:scada.symbol.main-line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -182,6 +186,7 @@ "type": "number", "default": 4, "subLabel": "Width", + "divider": true, "fieldSuffix": "px", "min": 1, "step": 1, @@ -193,11 +198,14 @@ "name": "{i18n:scada.symbol.flow}", "group": "{i18n:scada.symbol.animation}", "type": "color", - "default": "#C8DFF7" + "default": "#C8DFF7", + "disabled": false, + "visible": true }, { "id": "flowStyleDash", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, @@ -213,6 +221,7 @@ { "id": "flowStyleGap", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, @@ -248,5 +257,5 @@ } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg index af83a4abb3..82b8babcf7 100644 --- a/application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg @@ -3,7 +3,7 @@ "description": "Left tee connector", "widgetSizeX": 1, "widgetSizeY": 1, - "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H97\";\nconst topLine = \"M100 100L100 0\";\nconst bottomLine = \"M 100,200 V 100\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(``);\n}", + "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H97\";\nconst leftLineReversed = \"M 97,100 H 0\";\nconst topLine = \"M100 100L100 0\";\nconst topLineReversed = \"M 100,0 V 100\";\nconst bottomLine = \"M 100,200 V 100\";\nconst bottomLineReversed = \"M 100,100 V 200\";\n\nprepareFlowAnimation('left', leftLine, leftLineReversed);\nprepareFlowAnimation('top', topLine, topLineReversed);\nprepareFlowAnimation('bottom', bottomLine, bottomLineReversed);\n\nfunction prepareFlowAnimation(prefix, line, reversedLine) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const duration = 1 / flowAnimationSpeed;\n \n let animateFlow = ctx.api.connectorAnimation(animation);\n \n if (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, reversedLine).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n } else {\n if (animateFlow) {\n animateFlow.finish();\n }\n }\n}", "tags": [ { "tag": "line", @@ -377,13 +377,14 @@ }, { "id": "mainLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 6, "required": true, "subLabel": "Main", "divider": true, "fieldSuffix": "px", + "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, @@ -392,21 +393,23 @@ }, { "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 2, "required": true, "subLabel": "Secondary", + "divider": true, "fieldSuffix": "px", + "condition": "return !model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, - "visible": true + "visible": false }, { "id": "lineColor", - "name": "{i18n:scada.symbol.line-color}", + "name": "{i18n:scada.symbol.main-line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -419,6 +422,7 @@ "type": "number", "default": 4, "subLabel": "Width", + "divider": true, "fieldSuffix": "px", "min": 1, "step": 1, @@ -437,6 +441,7 @@ { "id": "flowStyleDash", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, @@ -452,6 +457,7 @@ { "id": "flowStyleGap", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, @@ -487,5 +493,5 @@ } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg index 5b6d30ab65..3c811a2372 100644 --- a/application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg @@ -3,7 +3,7 @@ "description": "Left top elbow connector", "widgetSizeX": 1, "widgetSizeY": 1, - "stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(``);\n}\n", + "stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 100,0 L 100,75 Q 100,100 75,100 L 0, 100';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n", "tags": [ { "tag": "line", @@ -140,13 +140,14 @@ }, { "id": "mainLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 6, "required": true, "subLabel": "Main", "divider": true, "fieldSuffix": "px", + "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, @@ -155,21 +156,23 @@ }, { "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 2, "required": true, "subLabel": "Secondary", + "divider": true, "fieldSuffix": "px", + "condition": "return !model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, - "visible": true + "visible": false }, { "id": "lineColor", - "name": "{i18n:scada.symbol.line-color}", + "name": "{i18n:scada.symbol.main-line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -182,6 +185,7 @@ "type": "number", "default": 4, "subLabel": "Width", + "divider": true, "fieldSuffix": "px", "min": 1, "step": 1, @@ -200,6 +204,7 @@ { "id": "flowStyleDash", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, @@ -215,6 +220,7 @@ { "id": "flowStyleGap", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, @@ -250,5 +256,5 @@ } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg index 3e65bd6b04..3a3296544d 100644 --- a/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg @@ -1,10 +1,9 @@ - - { +<svg xmlns="http://www.w3.org/2000/svg" xmlns:tb="https://thingsboard.io/svg" width="400" height="200" fill="none" version="1.1" viewBox="0 0 400 200"><tb:metadata xmlns=""><![CDATA[{ "title": "HP Long horizontal connector", "description": "Long horizontal connector with an optional directional arrow to visually indicate flow.", "widgetSizeX": 2, "widgetSizeY": 1, - "stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(`<path style=\"stroke-dasharray: ${dashArray}; stroke-linecap: ${dashCap}; stroke-dashoffset: 0;\" d=\"${line}\" stroke-miterlimit=\"10\" fill=\"none\" stroke=\"${lineColor}\" stroke-width=\"${lineWidth}\"><animate attributeName=\"stroke-dashoffset\" values=\"${value};0\" dur=\"${duration}s\" begin=\"-${offset}ms\" calcMode=\"linear\" repeatCount=\"indefinite\" /></path>`);\n}\n", + "stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 400,100 H 0';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n", "tags": [ { "tag": "arrow", @@ -177,27 +176,30 @@ }, { "id": "mainLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 6, "required": true, "subLabel": "Main", "divider": true, "fieldSuffix": "px", + "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, - "visible": true + "visible": false }, { "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 2, "required": true, "subLabel": "Secondary", + "divider": true, "fieldSuffix": "px", + "condition": "return !model.mainLine;", "min": 0, "max": 99, "step": 1, @@ -206,7 +208,7 @@ }, { "id": "lineColor", - "name": "{i18n:scada.symbol.line-color}", + "name": "{i18n:scada.symbol.main-line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -219,6 +221,7 @@ "type": "number", "default": 4, "subLabel": "Width", + "divider": true, "fieldSuffix": "px", "min": 1, "step": 1, @@ -230,11 +233,14 @@ "name": "{i18n:scada.symbol.flow}", "group": "{i18n:scada.symbol.animation}", "type": "color", - "default": "#C8DFF7" + "default": "#C8DFF7", + "disabled": false, + "visible": true }, { "id": "flowStyleDash", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, @@ -250,6 +256,7 @@ { "id": "flowStyleGap", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, @@ -285,5 +292,5 @@ } ] } - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg index b5c9730842..e66fd62c2e 100644 --- a/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg @@ -3,7 +3,7 @@ "description": "Long vertical connector with an optional directional arrow to visually indicate flow.", "widgetSizeX": 1, "widgetSizeY": 2, - "stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(``);\n}\n", + "stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 100,0 V 400';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n\n", "tags": [ { "tag": "arrow", @@ -176,13 +176,14 @@ }, { "id": "mainLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 6, "required": true, "subLabel": "Main", "divider": true, "fieldSuffix": "px", + "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, @@ -191,21 +192,23 @@ }, { "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 2, "required": true, "subLabel": "Secondary", + "divider": true, "fieldSuffix": "px", + "condition": "return !model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, - "visible": true + "visible": false }, { "id": "lineColor", - "name": "{i18n:scada.symbol.line-color}", + "name": "{i18n:scada.symbol.main-line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -218,6 +221,7 @@ "type": "number", "default": 4, "subLabel": "Width", + "divider": true, "fieldSuffix": "px", "min": 1, "step": 1, @@ -229,11 +233,14 @@ "name": "{i18n:scada.symbol.flow}", "group": "{i18n:scada.symbol.animation}", "type": "color", - "default": "#C8DFF7" + "default": "#C8DFF7", + "disabled": false, + "visible": true }, { "id": "flowStyleDash", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, @@ -249,6 +256,7 @@ { "id": "flowStyleGap", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, @@ -284,5 +292,5 @@ } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg index 62aecb065d..e40936c152 100644 --- a/application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg @@ -3,7 +3,7 @@ "description": "Right tee connector", "widgetSizeX": 1, "widgetSizeY": 1, - "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst topLine = \"M100 100L100 0\";\nconst rightLine = \"M103 100H200\";\nconst bottomLine = \"M 100,200 V 100\";\n\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('right', rightLine);\nprepareFlowAnimation('bottom', bottomLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(``);\n}", + "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst topLine = \"M100 100L100 0\";\nconst topLineReversed = \"M 100,0 V 100\";\nconst rightLine = \"M103 100H200\";\nconst rightLineReversed = \"M 200,100 H 103\";\nconst bottomLine = \"M 100,200 V 100\";\nconst bottomLineReversed = \"M 100,100 V 200\";\n\nprepareFlowAnimation('top', topLine, topLineReversed);\nprepareFlowAnimation('right', rightLine, rightLineReversed);\nprepareFlowAnimation('bottom', bottomLine, bottomLineReversed);\n\nfunction prepareFlowAnimation(prefix, line, reversedLine) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const duration = 1 / flowAnimationSpeed;\n \n let animateFlow = ctx.api.connectorAnimation(animation);\n \n if (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, reversedLine).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n } else {\n if (animateFlow) {\n animateFlow.finish();\n }\n }\n}", "tags": [ { "tag": "line", @@ -377,13 +377,14 @@ }, { "id": "mainLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 6, "required": true, "subLabel": "Main", "divider": true, "fieldSuffix": "px", + "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, @@ -392,21 +393,23 @@ }, { "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 2, "required": true, "subLabel": "Secondary", + "divider": true, "fieldSuffix": "px", + "condition": "return !model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, - "visible": true + "visible": false }, { "id": "lineColor", - "name": "{i18n:scada.symbol.line-color}", + "name": "{i18n:scada.symbol.main-line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -419,6 +422,7 @@ "type": "number", "default": 4, "subLabel": "Width", + "divider": true, "fieldSuffix": "px", "min": 1, "step": 1, @@ -437,6 +441,7 @@ { "id": "flowStyleDash", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, @@ -452,6 +457,7 @@ { "id": "flowStyleGap", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, diff --git a/application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg index 8ec4e3cc65..8c5224c9f8 100644 --- a/application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg @@ -3,7 +3,7 @@ "description": "Top right elbow connector", "widgetSizeX": 1, "widgetSizeY": 1, - "stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `-${dashWidth + (dashGap || dashWidth)}` : `${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(``);\n}\n", + "stateRenderFunction": "const {\n flowAnimation,\n animationDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 200,100 L 125,100 Q 100,100 100,75 L 100, 0';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n", "tags": [ { "tag": "line", @@ -140,13 +140,14 @@ }, { "id": "mainLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 6, "required": true, "subLabel": "Main", "divider": true, "fieldSuffix": "px", + "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, @@ -155,21 +156,23 @@ }, { "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 2, "required": true, "subLabel": "Secondary", + "divider": true, "fieldSuffix": "px", + "condition": "return !model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, - "visible": true + "visible": false }, { "id": "lineColor", - "name": "{i18n:scada.symbol.line-color}", + "name": "{i18n:scada.symbol.main-line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -182,6 +185,7 @@ "type": "number", "default": 4, "subLabel": "Width", + "divider": true, "fieldSuffix": "px", "min": 1, "step": 1, @@ -200,6 +204,7 @@ { "id": "flowStyleDash", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, @@ -215,6 +220,7 @@ { "id": "flowStyleGap", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, @@ -250,5 +256,5 @@ } ] }]]> - + \ No newline at end of file diff --git a/application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg index e4561a8347..101799fd4b 100644 --- a/application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg @@ -3,7 +3,7 @@ "description": "Top tee connector", "widgetSizeX": 1, "widgetSizeY": 1, - "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst topLine = \"M100 97L100 0\";\nconst rightLine = \"M100 100H200\";\n\nprepareFlowAnimation('left', leftLine);\nprepareFlowAnimation('top', topLine);\nprepareFlowAnimation('right', rightLine);\n\nfunction prepareFlowAnimation(prefix, line) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const offset = Date.now() % 1000;\n const duration = 1 / flowAnimationSpeed;\n \n const prevFlowAnimation = animation.remember('flowAnimation');\n const prevFlowDirection = animation.remember('flowDirection');\n const prevFlowDuration = animation.remember('flowDuration');\n \n if (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(animation, offset, flowDirection, duration, line);\n } else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n } else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n }\n}\n\nfunction animateFlow(group, offset, flowDirection, duration, line) {\n group.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n group.add(``);\n}", + "stateRenderFunction": "const {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\n\nconst leftLine = \"M0 100H100\";\nconst leftLineReversed = \"M 100,100 H 0\";\nconst topLine = \"M100 97L100 0\";\nconst topLineReversed = \"M 100,0 V 97\";\nconst rightLine = \"M100 100H200\";\nconst rightLineReversed = \"M 200,100 H 100\";\n\nprepareFlowAnimation('left', leftLine, leftLineReversed);\nprepareFlowAnimation('top', topLine, topLineReversed);\nprepareFlowAnimation('right', rightLine, rightLineReversed);\n\nfunction prepareFlowAnimation(prefix, line, reversedLine) {\n const flowAnimation = ctx.values[prefix + 'Flow'];\n const flowDirection = ctx.values[prefix + 'FlowDirection'];\n const flowAnimationSpeed = ctx.values[prefix + 'FlowAnimationSpeed'];\n\n const animation = ctx.tags[prefix + 'Line'][0];\n const duration = 1 / flowAnimationSpeed;\n \n let animateFlow = ctx.api.connectorAnimation(animation);\n \n if (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, reversedLine).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n } else {\n if (animateFlow) {\n animateFlow.finish();\n }\n }\n}", "tags": [ { "tag": "line", @@ -377,13 +377,14 @@ }, { "id": "mainLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 6, "required": true, "subLabel": "Main", "divider": true, "fieldSuffix": "px", + "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, @@ -392,21 +393,23 @@ }, { "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 2, "required": true, "subLabel": "Secondary", + "divider": true, "fieldSuffix": "px", + "condition": "return !model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, - "visible": true + "visible": false }, { "id": "lineColor", - "name": "{i18n:scada.symbol.line-color}", + "name": "{i18n:scada.symbol.main-line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -419,6 +422,7 @@ "type": "number", "default": 4, "subLabel": "Width", + "divider": true, "fieldSuffix": "px", "min": 1, "step": 1, @@ -437,6 +441,7 @@ { "id": "flowStyleDash", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, @@ -452,6 +457,7 @@ { "id": "flowStyleGap", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, diff --git a/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg index cfaf6793ea..698a910f8d 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg @@ -3,7 +3,7 @@ "description": "Vertical connector with an optional directional arrow to visually indicate flow.", "widgetSizeX": 1, "widgetSizeY": 1, - "stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst animation = ctx.tags.animationGroup[0];\nconst offset = Date.now() % 1000;\nconst duration = 1 / flowAnimationSpeed;\n\nconst prevFlowAnimation = animation.remember('flowAnimation');\nconst prevFlowDirection = animation.remember('flowDirection');\nconst prevFlowDuration = animation.remember('flowDuration');\n\nif (flowAnimation && flowAnimation !== prevFlowAnimation) {\n animation.remember('flowAnimation', flowAnimation);\n animation.remember('flowDuration', duration);\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && flowDirection !== prevFlowDirection) {\n animation.remember('flowDirection', flowDirection);\n animateFlow(offset, flowDirection);\n} else if (flowAnimation && duration !== prevFlowDuration) {\n animation.remember('flowDuration', duration);\n animation.findOne('animate').attr('dur', `${duration}s`) ;\n} else if (!flowAnimation && prevFlowAnimation) {\n animation.remember('flowAnimation', null);\n animation.clear();\n}\n\nfunction animateFlow(offset, flowDirection) {\n animation.clear();\n const dashArray = `${dashWidth}${dashGap ? ` ${dashGap}` : ''}`;\n const value = flowDirection ? `${dashWidth + (dashGap || dashWidth)}` : `-${dashWidth + (dashGap || dashWidth)}`;\n\n animation.add(``);\n}\n", + "stateRenderFunction": "const {\n flowAnimation,\n arrowDirection: flowDirection,\n flowAnimationSpeed\n} = ctx.values;\nconst {\n flowAnimationWidth: lineWidth,\n flowAnimationColor: lineColor,\n flowStyleDash: dashWidth,\n flowStyleGap: dashGap,\n flowDashCap: dashCap\n} = ctx.properties;\nconst line = ctx.tags.line[0].attr('d');\nconst lineReversed = 'M 100,0 V 200';\nconst animation = ctx.tags.animationGroup[0];\nconst duration = 1 / flowAnimationSpeed;\n\nlet animateFlow = ctx.api.connectorAnimation(animation);\n\nif (flowAnimation) {\n if (!animateFlow) {\n animateFlow = ctx.api.connectorAnimate(animation, line, lineReversed).flowAppearance(lineWidth, lineColor, dashCap, dashWidth, dashGap).duration(duration).direction(flowDirection).play();\n } else {\n animateFlow.duration(duration).direction(flowDirection).play();\n }\n} else {\n if (animateFlow) {\n animateFlow.finish();\n }\n}\n", "tags": [ { "tag": "arrow", @@ -176,13 +176,14 @@ }, { "id": "mainLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 6, "required": true, "subLabel": "Main", "divider": true, "fieldSuffix": "px", + "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, @@ -191,21 +192,23 @@ }, { "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.line}", + "name": "{i18n:scada.symbol.main-line}", "type": "number", "default": 2, "required": true, "subLabel": "Secondary", + "divider": true, "fieldSuffix": "px", + "condition": "return !model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, - "visible": true + "visible": false }, { "id": "lineColor", - "name": "{i18n:scada.symbol.line-color}", + "name": "{i18n:scada.symbol.main-line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -218,6 +221,7 @@ "type": "number", "default": 4, "subLabel": "Width", + "divider": true, "fieldSuffix": "px", "min": 1, "step": 1, @@ -229,11 +233,14 @@ "name": "{i18n:scada.symbol.flow}", "group": "{i18n:scada.symbol.animation}", "type": "color", - "default": "#C8DFF7" + "default": "#C8DFF7", + "disabled": false, + "visible": true }, { "id": "flowStyleDash", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, @@ -249,6 +256,7 @@ { "id": "flowStyleGap", "name": "{i18n:scada.symbol.flow-style}", + "hint": "{i18n:scada.symbol.flow-style-hint}", "group": "{i18n:scada.symbol.animation}", "type": "number", "default": 10, @@ -284,5 +292,5 @@ } ] }]]> - + \ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts index abe570fa3c..ee63dc59a6 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts @@ -83,6 +83,10 @@ export interface ScadaSymbolApi { cssAnimation: (element: Element) => ScadaSymbolAnimation | undefined; resetCssAnimation: (element: Element) => void; finishCssAnimation: (element: Element) => void; + connectorAnimation:(element: Element) => ConnectorScadaSymbolAnimation | undefined; + connectorAnimate:(element: Element) => ConnectorScadaSymbolAnimation; + resetConnectorAnimation: (element: Element) => void; + finishConnectorAnimation: (element: Element) => void; disable: (element: Element | Element[]) => void; enable: (element: Element | Element[]) => void; callAction: (event: Event, behaviorId: string, value?: any, observer?: Partial>) => void; @@ -186,6 +190,8 @@ const tbNamespaceRegex = //gm const tbTagRegex = /tb:tag="([^"]*)"/gms; +let syncTime = Date.now(); + const generateElementId = () => { const id = guid(); const firstChar = id.charAt(0); @@ -485,6 +491,7 @@ export class ScadaSymbolObject { private settings: ScadaSymbolObjectSettings; private context: ScadaSymbolContext; private cssAnimations: CssScadaSymbolAnimations; + private connectorAnimations: ScadaSymbolFlowConnectorAnimations; private svgShape: Svg; private box: Box; @@ -604,6 +611,7 @@ export class ScadaSymbolObject { private init() { this.cssAnimations = new CssScadaSymbolAnimations(this.svgShape, this.raf); + this.connectorAnimations = new ScadaSymbolFlowConnectorAnimations(); this.context = { api: { generateElementId: () => generateElementId(), @@ -615,6 +623,10 @@ export class ScadaSymbolObject { cssAnimation: this.cssAnimation.bind(this), resetCssAnimation: this.resetCssAnimation.bind(this), finishCssAnimation: this.finishCssAnimation.bind(this), + connectorAnimation: this.connectorAnimation.bind(this), + connectorAnimate: this.connectorAnimate.bind(this), + resetConnectorAnimation: this.resetConnectorAnimation.bind(this), + finishConnectorAnimation: this.finishConnectorAnimation.bind(this), disable: this.disableElement.bind(this), enable: this.enableElement.bind(this), callAction: this.callAction.bind(this), @@ -959,6 +971,22 @@ export class ScadaSymbolObject { this.cssAnimations.finishAnimation(element); } + private connectorAnimate(element: Element, path: string, reversedPath: string): ConnectorScadaSymbolAnimation { + return this.connectorAnimations.animate(element, path, reversedPath); + } + + private connectorAnimation(element: Element): ConnectorScadaSymbolAnimation | undefined { + return this.connectorAnimations.animation(element); + } + + private resetConnectorAnimation(element: Element) { + this.connectorAnimations.resetAnimation(element); + } + + private finishConnectorAnimation(element: Element) { + this.connectorAnimations.finishAnimation(element); + } + private disableElement(e: Element | Element[]) { this.elements(e).forEach(element => { element.attr({'pointer-events': 'none'}); @@ -1108,6 +1136,20 @@ interface ScadaSymbolAnimation { } +const scadaSymbolConnectorFlowAnimationId = 'scadaSymbolConnectorFlowAnimation'; + +type StrokeLineCap = 'butt' | 'round '| 'square'; + +interface ConnectorScadaSymbolAnimation { + play(): void; + stop(): void; + finish(): void; + + flowAppearance(width: number, color: string, lineCap: StrokeLineCap, dashWidth: number, dashGap: number): ConnectorScadaSymbolAnimation; + duration(speed: number): ConnectorScadaSymbolAnimation; + direction(direction: boolean): ConnectorScadaSymbolAnimation; +} + class CssScadaSymbolAnimations { constructor(private svgShape: Svg, private raf: RafService) {} @@ -1159,6 +1201,135 @@ class CssScadaSymbolAnimations { } } +class ScadaSymbolFlowConnectorAnimations { + constructor() {} + + public animate(element: Element, path = '', reversedPath = ''): ConnectorScadaSymbolAnimation { + this.checkOldAnimation(element); + return this.setupAnimation(element, this.createAnimation(element, path, reversedPath)); + } + + public animation(element: Element): ConnectorScadaSymbolAnimation | undefined { + return element.remember(scadaSymbolConnectorFlowAnimationId); + } + + public resetAnimation(element: Element) { + const animation: ConnectorScadaSymbolAnimation = element.remember(scadaSymbolConnectorFlowAnimationId); + if (animation) { + animation.stop(); + element.remember(scadaSymbolConnectorFlowAnimationId, null); + } + } + + public finishAnimation(element: Element) { + const animation: ConnectorScadaSymbolAnimation = element.remember(scadaSymbolConnectorFlowAnimationId); + if (animation) { + animation.finish(); + element.remember(scadaSymbolConnectorFlowAnimationId, null); + } + } + + private setupAnimation(element: Element, animation: ConnectorScadaSymbolAnimation): ConnectorScadaSymbolAnimation { + element.remember(scadaSymbolConnectorFlowAnimationId, animation); + return animation; + } + + private checkOldAnimation(element: Element) { + const previousAnimation: ConnectorScadaSymbolAnimation = element.remember(scadaSymbolConnectorFlowAnimationId); + if (previousAnimation) { + previousAnimation.finish(); + } + } + + private createAnimation(element: Element, path: string, reversedPath: string): ConnectorScadaSymbolAnimation { + return new FlowConnectorAnimation(element, path, reversedPath); + } +} + +class FlowConnectorAnimation implements ConnectorScadaSymbolAnimation { + + private readonly _path: string; + private readonly _reversedPath: string; + private readonly _animation: Element; + + private _duration: number = 1; + private _lineColor: string = '#C8DFF7'; + private _lineWidth: number = 4; + private _strokeLineCap: StrokeLineCap = 'butt'; + private _dashWidth: number = 10; + private _dashGap: number = 10; + private _direction: boolean = true; + + constructor(private element: Element, + path: string, + pathReversed: string) { + this._path = path; + this._reversedPath = pathReversed; + + const dashArray = `${this._dashWidth} ${this._dashGap}`; + const values = `${this._dashWidth + this._dashGap};0`; + + this._animation = SVG( + `` + + `` + ); + } + + public play() { + if (!this.element.node.childElementCount) { + this.element.add(this._animation); + } + if (!syncTime) { + syncTime = Date.now(); + } + const animateElement = this.element.node.getElementsByTagName('animate')[0]; + const offset = ((Date.now() - syncTime) % 1000) * -1; + (animateElement as SVGAnimationElement).beginElementAt(offset); + } + + public stop() { + const animateElement = this.element.node.getElementsByTagName('animate')[0]; + (animateElement as SVGAnimationElement)?.endElement(); + } + + public finish() { + this.element.findOne('path')?.remove(); + } + + public flowAppearance(width: number, color: string, linecap: StrokeLineCap, dashWidth: number, dashGap: number): this { + const totalLength = (this._animation.node as SVGPathElement).getTotalLength(); + let offset = 0; + if ((totalLength % 100) !== 0) { + const clientWidth = totalLength < 100 ? 100 : this.element.node.ownerSVGElement.clientWidth; + const clientWidthDash = clientWidth / (dashWidth + dashGap); + const totalLengthDash = totalLength / clientWidthDash; + offset = ((dashWidth + dashGap) - totalLengthDash) / 2; + } + this._lineColor = color; + this._lineWidth = width; + this._strokeLineCap = linecap; + this._dashWidth = dashWidth - offset; + this._dashGap = dashGap - offset; + const dashArray = `${this._dashWidth}${this._dashGap ? ` ${this._dashGap}` : ''}`; + const values = `${this._dashWidth + (this._dashGap || this._dashWidth)};0`; + this._animation.stroke({width, color, linecap, dasharray: dashArray}); + this._animation.findOne('animate').attr('values', values); + return this; + } + + public duration(speed: number): this { + this._duration = speed; + this._animation.findOne('animate').attr('dur', `${speed}s`); + return this; + } + + public direction(direction: boolean): this { + this._direction = direction; + this._animation.attr('d', direction ? this._path : this._reversedPath); + return this; + } +} + interface ScadaSymbolAnimationKeyframe { stop: string; style: any; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html index b32c7b5ab1..af59457abe 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.html @@ -30,6 +30,12 @@ +
+
scada.behavior.hint
+ + + +
dynamic-form.property.group-title
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.ts index 2df62e1259..00a614636d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form-property-panel.component.ts @@ -104,6 +104,7 @@ export class DynamicFormPropertyPanelComponent implements OnInit { { id: [this.property.id, [Validators.required]], name: [this.property.name, [Validators.required]], + hint: [this.property.hint, []], group: [this.property.group, []], type: [this.property.type, [Validators.required]], arrayItemType: [this.property.arrayItemType, [Validators.required]], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html index a64c178eb6..449af2b240 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/dynamic-form/dynamic-form.component.html @@ -91,9 +91,15 @@
- {{ propertyRow.label | customTranslate }} +
+ {{ propertyRow.label | customTranslate }} +
-
{{ propertyRow.label | customTranslate }}
+
+
+ {{ propertyRow.label | customTranslate }} +
+
diff --git a/ui-ngx/src/app/shared/models/dynamic-form.models.ts b/ui-ngx/src/app/shared/models/dynamic-form.models.ts index 4132eeff94..b1cc83dbd7 100644 --- a/ui-ngx/src/app/shared/models/dynamic-form.models.ts +++ b/ui-ngx/src/app/shared/models/dynamic-form.models.ts @@ -87,6 +87,7 @@ export type PropertyConditionFunction = (property: FormProperty, model: any) => export interface FormPropertyBase { id: string; name: string; + hint?: string; group?: string; type: FormPropertyType; default: any; @@ -237,6 +238,7 @@ export interface FormPropertyContainerBase { } export interface FormPropertyRow extends FormPropertyContainerBase { + hint?: string; properties?: FormProperty[]; switch?: FormProperty; rowClass?: string; @@ -362,6 +364,7 @@ const toPropertyContainers = (properties: FormProperty[], if (!propertyRow) { propertyRow = { label: property.name, + hint: property.hint, type: FormPropertyContainerType.row, properties: [], rowClass: property.rowClass, 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 72afcfc0b9..a3ba654258 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3446,6 +3446,7 @@ "flow-animation-hint": "Indicates whether animation is present in connector.", "flow": "Flow", "flow-style": "Flow style", + "flow-style-hint": "Set the Dash and Gap values so that their sum is divisible by 100 without a remainder for perfect animation synchronization.", "flow-dash-cap": "Flow dash cap", "dash-cap-butt": "Butt", "dash-cap-round": "Round", From 166475a5246fd669726e9b306e9a9885d21f16eb Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 1 May 2025 16:21:14 +0300 Subject: [PATCH 31/63] UI: Refactoring connector settings --- .../bottom-right-elbow-connector-hp.svg | 65 +++------- .../scada_symbols/bottom-tee-connector-hp.svg | 73 ++++------- .../scada_symbols/cross-connector-hp.svg | 74 ++++------- .../scada_symbols/horizontal-connector-hp.svg | 111 +++++++---------- .../left-bottom-elbow-connector-hp.svg | 74 ++++------- .../scada_symbols/left-tee-connector-hp.svg | 73 ++++------- .../left-top-elbow-connector-hp.svg | 65 +++------- .../long-horizontal-connector-hp.svg | 113 +++++++---------- .../long-vertical-connector-hp.svg | 117 +++++++----------- .../scada_symbols/right-tee-connector-hp.svg | 73 ++++------- .../top-right-elbow-connector-hp.svg | 65 +++------- .../scada_symbols/top-tee-connector-hp.svg | 73 ++++------- .../scada_symbols/vertical-connector-hp.svg | 113 +++++++---------- .../widget/lib/scada/scada-symbol.models.ts | 7 +- .../assets/locale/locale.constant-en_US.json | 13 +- 15 files changed, 376 insertions(+), 733 deletions(-) diff --git a/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg index b76f7399cc..f7ffd5f167 100644 --- a/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg @@ -7,7 +7,7 @@ "tags": [ { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", "actions": null } ], @@ -53,8 +53,8 @@ }, { "id": "animationDirection", - "name": "{i18n:scada.symbol.animation-direction}", - "hint": "{i18n:scada.symbol.animation-direction-hint}", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", @@ -131,48 +131,22 @@ ], "properties": [ { - "id": "mainLine", - "name": "{i18n:scada.symbol.main-line}", - "type": "switch", - "default": true, - "disabled": false, - "visible": true - }, - { - "id": "mainLineSize", - "name": "{i18n:scada.symbol.main-line}", + "id": "lineSize", + "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, "required": true, - "subLabel": "Main", - "divider": true, + "divider": false, "fieldSuffix": "px", - "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, "visible": true }, - { - "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.main-line}", - "type": "number", - "default": 2, - "required": true, - "subLabel": "Secondary", - "divider": true, - "fieldSuffix": "px", - "condition": "return !model.mainLine;", - "min": 0, - "max": 99, - "step": 1, - "disabled": false, - "visible": false - }, { "id": "lineColor", - "name": "{i18n:scada.symbol.main-line}", + "name": "{i18n:scada.symbol.line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -180,12 +154,11 @@ }, { "id": "flowAnimationWidth", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 4, - "subLabel": "Width", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 1, "step": 1, @@ -194,8 +167,8 @@ }, { "id": "flowAnimationColor", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "color", "default": "#C8DFF7", "disabled": false, @@ -203,14 +176,14 @@ }, { "id": "flowStyleDash", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "required": true, "subLabel": "{i18n:scada.symbol.dash}", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 0, "step": 1, @@ -219,14 +192,14 @@ }, { "id": "flowStyleGap", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "subLabel": "{i18n:scada.symbol.gap}", "fieldSuffix": "px", - "min": 1, + "min": 0, "step": 1, "disabled": false, "visible": true @@ -234,7 +207,7 @@ { "id": "flowDashCap", "name": "{i18n:scada.symbol.flow-dash-cap}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "select", "default": "butt", "items": [ diff --git a/application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg index 28e0788d35..bdbbdca81f 100644 --- a/application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg @@ -7,7 +7,7 @@ "tags": [ { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", "actions": null }, { @@ -58,8 +58,8 @@ }, { "id": "leftFlowDirection", - "name": "{i18n:scada.symbol.animation-direction}", - "hint": "{i18n:scada.symbol.animation-direction-hint}", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": "{i18n:scada.symbol.left-connector}", "type": "value", "valueType": "BOOLEAN", @@ -174,8 +174,8 @@ }, { "id": "rightFlowDirection", - "name": "{i18n:scada.symbol.flow-direction}", - "hint": "{i18n:scada.symbol.flow-direction-hint}", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": "{i18n:scada.symbol.right-connector}", "type": "value", "valueType": "BOOLEAN", @@ -290,8 +290,8 @@ }, { "id": "bottomFlowDirection", - "name": "{i18n:scada.symbol.animation-direction}", - "hint": "{i18n:scada.symbol.animation-direction-hint}", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": "{i18n:scada.symbol.bottom-connector}", "type": "value", "valueType": "BOOLEAN", @@ -368,48 +368,22 @@ ], "properties": [ { - "id": "mainLine", - "name": "{i18n:scada.symbol.main-line}", - "type": "switch", - "default": true, - "disabled": false, - "visible": true - }, - { - "id": "mainLineSize", - "name": "{i18n:scada.symbol.main-line}", + "id": "lineSize", + "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, "required": true, - "subLabel": "Main", - "divider": true, + "divider": false, "fieldSuffix": "px", - "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, "visible": true }, - { - "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.main-line}", - "type": "number", - "default": 2, - "required": true, - "subLabel": "Secondary", - "divider": true, - "fieldSuffix": "px", - "condition": "return !model.mainLine;", - "min": 0, - "max": 99, - "step": 1, - "disabled": false, - "visible": false - }, { "id": "lineColor", - "name": "{i18n:scada.symbol.main-line}", + "name": "{i18n:scada.symbol.line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -417,12 +391,11 @@ }, { "id": "flowAnimationWidth", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 4, - "subLabel": "Width", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 1, "step": 1, @@ -431,8 +404,8 @@ }, { "id": "flowAnimationColor", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "color", "default": "#C8DFF7", "disabled": false, @@ -440,14 +413,14 @@ }, { "id": "flowStyleDash", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "required": true, "subLabel": "{i18n:scada.symbol.dash}", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 0, "step": 1, @@ -456,14 +429,14 @@ }, { "id": "flowStyleGap", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "subLabel": "{i18n:scada.symbol.gap}", "fieldSuffix": "px", - "min": 1, + "min": 0, "step": 1, "disabled": false, "visible": true @@ -471,7 +444,7 @@ { "id": "flowDashCap", "name": "{i18n:scada.symbol.flow-dash-cap}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "select", "default": "butt", "items": [ diff --git a/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg index 480666a589..db6b091e0e 100644 --- a/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg @@ -7,7 +7,7 @@ "tags": [ { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", "actions": null }, { @@ -58,8 +58,8 @@ }, { "id": "leftFlowDirection", - "name": "{i18n:scada.symbol.animation-direction}", - "hint": "{i18n:scada.symbol.animation-direction-hint}", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": "{i18n:scada.symbol.left-connector}", "type": "value", "valueType": "BOOLEAN", @@ -174,8 +174,8 @@ }, { "id": "topFlowDirection", - "name": "{i18n:scada.symbol.animation-direction}", - "hint": "{i18n:scada.symbol.animation-direction-hint}", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": "{i18n:scada.symbol.top-connector}", "type": "value", "valueType": "BOOLEAN", @@ -290,8 +290,8 @@ }, { "id": "rightFlowDirection", - "name": "{i18n:scada.symbol.flow-direction}", - "hint": "{i18n:scada.symbol.flow-direction-hint}", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": "{i18n:scada.symbol.right-connector}", "type": "value", "valueType": "BOOLEAN", @@ -406,8 +406,8 @@ }, { "id": "bottomFlowDirection", - "name": "{i18n:scada.symbol.animation-direction}", - "hint": "{i18n:scada.symbol.animation-direction-hint}", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": "{i18n:scada.symbol.bottom-connector}", "type": "value", "valueType": "BOOLEAN", @@ -483,17 +483,9 @@ } ], "properties": [ - { - "id": "mainLine", - "name": "{i18n:scada.symbol.main-line}", - "type": "switch", - "default": true, - "disabled": false, - "visible": true - }, { "id": "mainLineSize", - "name": "{i18n:scada.symbol.main-line}", + "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, "required": true, @@ -504,28 +496,11 @@ "min": 0, "max": 99, "step": 1, - "disabled": false, - "visible": true - }, - { - "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.main-line}", - "type": "number", - "default": 2, - "required": true, - "subLabel": "Secondary", - "divider": true, - "fieldSuffix": "px", - "condition": "return !model.mainLine;", - "min": 0, - "max": 99, - "step": 1, - "disabled": false, - "visible": false + "disabled": false }, { "id": "lineColor", - "name": "{i18n:scada.symbol.main-line}", + "name": "{i18n:scada.symbol.line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -533,12 +508,11 @@ }, { "id": "flowAnimationWidth", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 4, - "subLabel": "Width", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 1, "step": 1, @@ -547,8 +521,8 @@ }, { "id": "flowAnimationColor", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "color", "default": "#C8DFF7", "disabled": false, @@ -556,14 +530,14 @@ }, { "id": "flowStyleDash", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "required": true, "subLabel": "{i18n:scada.symbol.dash}", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 0, "step": 1, @@ -572,14 +546,14 @@ }, { "id": "flowStyleGap", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "subLabel": "{i18n:scada.symbol.gap}", "fieldSuffix": "px", - "min": 1, + "min": 0, "step": 1, "disabled": false, "visible": true @@ -587,7 +561,7 @@ { "id": "flowDashCap", "name": "{i18n:scada.symbol.flow-dash-cap}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "select", "default": "butt", "items": [ diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg index 230c7b68a5..93ef81f57b 100644 --- a/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg @@ -12,7 +12,7 @@ }, { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", "actions": null } ], @@ -53,18 +53,18 @@ "defaultWidgetActionSettings": null }, { - "id": "arrowDirection", - "name": "{i18n:scada.symbol.arrow-direction}", - "hint": "{i18n:scada.symbol.arrow-direction-hint}", + "id": "flowAnimation", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", - "trueLabel": "{i18n:scada.symbol.forward}", - "falseLabel": "{i18n:scada.symbol.reverse}", - "stateLabel": "{i18n:scada.symbol.forward}", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", "defaultGetValueSettings": { "action": "DO_NOTHING", - "defaultValue": true, + "defaultValue": false, "executeRpc": { "method": "getState", "requestTimeout": 5000, @@ -72,34 +72,38 @@ "persistentPollingInterval": 1000 }, "getAttribute": { - "scope": null, - "key": "state" + "key": "state", + "scope": null }, "getTimeSeries": { "key": "state" }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, "dataToValue": { "type": "NONE", - "dataToValueFunction": "/* Should return boolean value */\nreturn data;", - "compareToValue": true + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" } }, "defaultSetValueSettings": null, "defaultWidgetActionSettings": null }, { - "id": "flowAnimation", - "name": "{i18n:scada.symbol.flow-animation}", - "hint": "{i18n:scada.symbol.flow-animation-hint}", + "id": "arrowDirection", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", - "trueLabel": "{i18n:scada.symbol.present}", - "falseLabel": "{i18n:scada.symbol.absent}", - "stateLabel": "{i18n:scada.symbol.flow-present}", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", "defaultGetValueSettings": { "action": "DO_NOTHING", - "defaultValue": false, + "defaultValue": true, "executeRpc": { "method": "getState", "requestTimeout": 5000, @@ -107,20 +111,16 @@ "persistentPollingInterval": 1000 }, "getAttribute": { - "key": "state", - "scope": null + "scope": null, + "key": "state" }, "getTimeSeries": { "key": "state" }, - "getAlarmStatus": { - "severityList": null, - "typeList": null - }, "dataToValue": { "type": "NONE", - "compareToValue": true, - "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true } }, "defaultSetValueSettings": null, @@ -167,48 +167,22 @@ ], "properties": [ { - "id": "mainLine", - "name": "{i18n:scada.symbol.main-line}", - "type": "switch", - "default": true, - "disabled": false, - "visible": true - }, - { - "id": "mainLineSize", - "name": "{i18n:scada.symbol.main-line}", + "id": "lineSize", + "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, "required": true, - "subLabel": "Main", - "divider": true, + "divider": false, "fieldSuffix": "px", - "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, "visible": true }, - { - "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.main-line}", - "type": "number", - "default": 2, - "required": true, - "subLabel": "Secondary", - "divider": true, - "fieldSuffix": "px", - "condition": "return !model.mainLine;", - "min": 0, - "max": 99, - "step": 1, - "disabled": false, - "visible": false - }, { "id": "lineColor", - "name": "{i18n:scada.symbol.main-line}", + "name": "{i18n:scada.symbol.line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -216,11 +190,10 @@ }, { "id": "flowAnimationWidth", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 4, - "subLabel": "Width", "fieldSuffix": "px", "min": 1, "step": 1, @@ -229,8 +202,8 @@ }, { "id": "flowAnimationColor", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "color", "default": "#C8DFF7", "disabled": false, @@ -238,14 +211,14 @@ }, { "id": "flowStyleDash", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "required": true, "subLabel": "{i18n:scada.symbol.dash}", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 0, "step": 1, @@ -254,14 +227,14 @@ }, { "id": "flowStyleGap", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "subLabel": "{i18n:scada.symbol.gap}", "fieldSuffix": "px", - "min": 1, + "min": 0, "step": 1, "disabled": false, "visible": true @@ -269,7 +242,7 @@ { "id": "flowDashCap", "name": "{i18n:scada.symbol.flow-dash-cap}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "select", "default": "butt", "items": [ diff --git a/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg index 1dbeec5f92..1c24cca2e0 100644 --- a/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg @@ -7,7 +7,7 @@ "tags": [ { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", "actions": null } ], @@ -32,8 +32,8 @@ "persistentPollingInterval": 1000 }, "getAttribute": { - "key": "state", - "scope": null + "scope": "SHARED_SCOPE", + "key": "flow" }, "getTimeSeries": { "key": "state" @@ -44,8 +44,8 @@ }, "dataToValue": { "type": "NONE", - "compareToValue": true, - "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true } }, "defaultSetValueSettings": null, @@ -53,8 +53,8 @@ }, { "id": "animationDirection", - "name": "{i18n:scada.symbol.animation-direction}", - "hint": "{i18n:scada.symbol.animation-direction-hint}", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", @@ -131,49 +131,22 @@ ], "properties": [ { - "id": "mainLine", - "name": "{i18n:scada.symbol.main-line}", - "type": "switch", - "default": true, - "disabled": false, - "visible": true - }, - { - "id": "mainLineSize", - "name": "{i18n:scada.symbol.main-line}", + "id": "lineSize", + "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, "required": true, - "subLabel": "Main", - "divider": true, + "divider": false, "fieldSuffix": "px", - "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, "visible": true }, - { - "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.main-line}", - "type": "number", - "default": 2, - "required": true, - "subLabel": "Secondary", - "divider": true, - "fieldSuffix": "px", - "disableOnProperty": "mainLine", - "condition": "return !model.mainLine;", - "min": 0, - "max": 99, - "step": 1, - "disabled": false, - "visible": false - }, { "id": "lineColor", - "name": "{i18n:scada.symbol.main-line}", + "name": "{i18n:scada.symbol.line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -181,12 +154,11 @@ }, { "id": "flowAnimationWidth", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 4, - "subLabel": "Width", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 1, "step": 1, @@ -195,8 +167,8 @@ }, { "id": "flowAnimationColor", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "color", "default": "#C8DFF7", "disabled": false, @@ -204,14 +176,14 @@ }, { "id": "flowStyleDash", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "required": true, "subLabel": "{i18n:scada.symbol.dash}", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 0, "step": 1, @@ -220,14 +192,14 @@ }, { "id": "flowStyleGap", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "subLabel": "{i18n:scada.symbol.gap}", "fieldSuffix": "px", - "min": 1, + "min": 0, "step": 1, "disabled": false, "visible": true @@ -235,7 +207,7 @@ { "id": "flowDashCap", "name": "{i18n:scada.symbol.flow-dash-cap}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "select", "default": "butt", "items": [ diff --git a/application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg index 82b8babcf7..461e91fe94 100644 --- a/application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg @@ -7,7 +7,7 @@ "tags": [ { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", "actions": null }, { @@ -58,8 +58,8 @@ }, { "id": "leftFlowDirection", - "name": "{i18n:scada.symbol.animation-direction}", - "hint": "{i18n:scada.symbol.animation-direction-hint}", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": "{i18n:scada.symbol.left-connector}", "type": "value", "valueType": "BOOLEAN", @@ -174,8 +174,8 @@ }, { "id": "topFlowDirection", - "name": "{i18n:scada.symbol.animation-direction}", - "hint": "{i18n:scada.symbol.animation-direction-hint}", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": "{i18n:scada.symbol.top-connector}", "type": "value", "valueType": "BOOLEAN", @@ -290,8 +290,8 @@ }, { "id": "bottomFlowDirection", - "name": "{i18n:scada.symbol.flow-direction}", - "hint": "{i18n:scada.symbol.flow-direction-hint}", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": "{i18n:scada.symbol.bottom-connector}", "type": "value", "valueType": "BOOLEAN", @@ -368,48 +368,22 @@ ], "properties": [ { - "id": "mainLine", - "name": "{i18n:scada.symbol.main-line}", - "type": "switch", - "default": true, - "disabled": false, - "visible": true - }, - { - "id": "mainLineSize", - "name": "{i18n:scada.symbol.main-line}", + "id": "lineSize", + "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, "required": true, - "subLabel": "Main", - "divider": true, + "divider": false, "fieldSuffix": "px", - "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, "visible": true }, - { - "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.main-line}", - "type": "number", - "default": 2, - "required": true, - "subLabel": "Secondary", - "divider": true, - "fieldSuffix": "px", - "condition": "return !model.mainLine;", - "min": 0, - "max": 99, - "step": 1, - "disabled": false, - "visible": false - }, { "id": "lineColor", - "name": "{i18n:scada.symbol.main-line}", + "name": "{i18n:scada.symbol.line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -417,12 +391,11 @@ }, { "id": "flowAnimationWidth", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 4, - "subLabel": "Width", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 1, "step": 1, @@ -431,8 +404,8 @@ }, { "id": "flowAnimationColor", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "color", "default": "#C8DFF7", "disabled": false, @@ -440,14 +413,14 @@ }, { "id": "flowStyleDash", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "required": true, "subLabel": "{i18n:scada.symbol.dash}", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 0, "step": 1, @@ -456,14 +429,14 @@ }, { "id": "flowStyleGap", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "subLabel": "{i18n:scada.symbol.gap}", "fieldSuffix": "px", - "min": 1, + "min": 0, "step": 1, "disabled": false, "visible": true @@ -471,7 +444,7 @@ { "id": "flowDashCap", "name": "{i18n:scada.symbol.flow-dash-cap}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "select", "default": "butt", "items": [ diff --git a/application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg index 3c811a2372..19e217a428 100644 --- a/application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg @@ -7,7 +7,7 @@ "tags": [ { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", "actions": null } ], @@ -53,8 +53,8 @@ }, { "id": "animationDirection", - "name": "{i18n:scada.symbol.animation-direction}", - "hint": "{i18n:scada.symbol.animation-direction-hint}", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", @@ -131,48 +131,22 @@ ], "properties": [ { - "id": "mainLine", - "name": "{i18n:scada.symbol.main-line}", - "type": "switch", - "default": true, - "disabled": false, - "visible": true - }, - { - "id": "mainLineSize", - "name": "{i18n:scada.symbol.main-line}", + "id": "lineSize", + "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, "required": true, - "subLabel": "Main", - "divider": true, + "divider": false, "fieldSuffix": "px", - "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, "visible": true }, - { - "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.main-line}", - "type": "number", - "default": 2, - "required": true, - "subLabel": "Secondary", - "divider": true, - "fieldSuffix": "px", - "condition": "return !model.mainLine;", - "min": 0, - "max": 99, - "step": 1, - "disabled": false, - "visible": false - }, { "id": "lineColor", - "name": "{i18n:scada.symbol.main-line}", + "name": "{i18n:scada.symbol.line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -180,12 +154,11 @@ }, { "id": "flowAnimationWidth", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 4, - "subLabel": "Width", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 1, "step": 1, @@ -194,8 +167,8 @@ }, { "id": "flowAnimationColor", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "color", "default": "#C8DFF7", "disabled": false, @@ -203,14 +176,14 @@ }, { "id": "flowStyleDash", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "required": true, "subLabel": "{i18n:scada.symbol.dash}", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 0, "step": 1, @@ -219,14 +192,14 @@ }, { "id": "flowStyleGap", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "subLabel": "{i18n:scada.symbol.gap}", "fieldSuffix": "px", - "min": 1, + "min": 0, "step": 1, "disabled": false, "visible": true @@ -234,7 +207,7 @@ { "id": "flowDashCap", "name": "{i18n:scada.symbol.flow-dash-cap}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "select", "default": "butt", "items": [ diff --git a/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg index 3a3296544d..0fe805c9a7 100644 --- a/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg @@ -12,7 +12,7 @@ }, { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", "actions": null } ], @@ -53,18 +53,18 @@ "defaultWidgetActionSettings": null }, { - "id": "arrowDirection", - "name": "{i18n:scada.symbol.arrow-direction}", - "hint": "{i18n:scada.symbol.arrow-direction-hint}", + "id": "flowAnimation", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", - "trueLabel": "{i18n:scada.symbol.forward}", - "falseLabel": "{i18n:scada.symbol.reverse}", - "stateLabel": "{i18n:scada.symbol.forward}", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", "defaultGetValueSettings": { "action": "DO_NOTHING", - "defaultValue": true, + "defaultValue": false, "executeRpc": { "method": "getState", "requestTimeout": 5000, @@ -72,34 +72,38 @@ "persistentPollingInterval": 1000 }, "getAttribute": { - "scope": null, - "key": "state" + "key": "state", + "scope": null }, "getTimeSeries": { "key": "state" }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, "dataToValue": { "type": "NONE", - "dataToValueFunction": "/* Should return boolean value */\nreturn data;", - "compareToValue": true + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" } }, "defaultSetValueSettings": null, "defaultWidgetActionSettings": null }, { - "id": "flowAnimation", - "name": "{i18n:scada.symbol.flow-animation}", - "hint": "{i18n:scada.symbol.flow-animation-hint}", + "id": "arrowDirection", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", - "trueLabel": "{i18n:scada.symbol.present}", - "falseLabel": "{i18n:scada.symbol.absent}", - "stateLabel": "{i18n:scada.symbol.flow-present}", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", "defaultGetValueSettings": { "action": "DO_NOTHING", - "defaultValue": false, + "defaultValue": true, "executeRpc": { "method": "getState", "requestTimeout": 5000, @@ -107,20 +111,16 @@ "persistentPollingInterval": 1000 }, "getAttribute": { - "key": "state", - "scope": null + "scope": null, + "key": "state" }, "getTimeSeries": { "key": "state" }, - "getAlarmStatus": { - "severityList": null, - "typeList": null - }, "dataToValue": { "type": "NONE", - "compareToValue": true, - "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true } }, "defaultSetValueSettings": null, @@ -167,39 +167,13 @@ ], "properties": [ { - "id": "mainLine", - "name": "{i18n:scada.symbol.main-line}", - "type": "switch", - "default": true, - "disabled": false, - "visible": true - }, - { - "id": "mainLineSize", - "name": "{i18n:scada.symbol.main-line}", + "id": "lineSize", + "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, "required": true, - "subLabel": "Main", - "divider": true, - "fieldSuffix": "px", - "condition": "return model.mainLine;", - "min": 0, - "max": 99, - "step": 1, - "disabled": false, - "visible": false - }, - { - "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.main-line}", - "type": "number", - "default": 2, - "required": true, - "subLabel": "Secondary", - "divider": true, + "divider": false, "fieldSuffix": "px", - "condition": "return !model.mainLine;", "min": 0, "max": 99, "step": 1, @@ -208,7 +182,7 @@ }, { "id": "lineColor", - "name": "{i18n:scada.symbol.main-line}", + "name": "{i18n:scada.symbol.line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -216,12 +190,11 @@ }, { "id": "flowAnimationWidth", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 4, - "subLabel": "Width", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 1, "step": 1, @@ -230,8 +203,8 @@ }, { "id": "flowAnimationColor", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "color", "default": "#C8DFF7", "disabled": false, @@ -239,14 +212,14 @@ }, { "id": "flowStyleDash", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "required": true, "subLabel": "{i18n:scada.symbol.dash}", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 0, "step": 1, @@ -255,14 +228,14 @@ }, { "id": "flowStyleGap", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "subLabel": "{i18n:scada.symbol.gap}", "fieldSuffix": "px", - "min": 1, + "min": 0, "step": 1, "disabled": false, "visible": true @@ -270,7 +243,7 @@ { "id": "flowDashCap", "name": "{i18n:scada.symbol.flow-dash-cap}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "select", "default": "butt", "items": [ diff --git a/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg index e66fd62c2e..411859b5ed 100644 --- a/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg @@ -12,7 +12,7 @@ }, { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", "actions": null } ], @@ -53,18 +53,18 @@ "defaultWidgetActionSettings": null }, { - "id": "arrowDirection", - "name": "{i18n:scada.symbol.arrow-direction}", - "hint": "{i18n:scada.symbol.arrow-direction-hint}", + "id": "flowAnimation", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", - "trueLabel": "{i18n:scada.symbol.forward}", - "falseLabel": "{i18n:scada.symbol.reverse}", - "stateLabel": "{i18n:scada.symbol.forward}", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", "defaultGetValueSettings": { "action": "DO_NOTHING", - "defaultValue": true, + "defaultValue": false, "executeRpc": { "method": "getState", "requestTimeout": 5000, @@ -72,34 +72,38 @@ "persistentPollingInterval": 1000 }, "getAttribute": { - "scope": null, - "key": "state" + "key": "state", + "scope": null }, "getTimeSeries": { "key": "state" }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, "dataToValue": { "type": "NONE", - "dataToValueFunction": "/* Should return boolean value */\nreturn data;", - "compareToValue": true + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" } }, "defaultSetValueSettings": null, "defaultWidgetActionSettings": null }, { - "id": "flowAnimation", - "name": "{i18n:scada.symbol.flow-animation}", - "hint": "{i18n:scada.symbol.flow-animation-hint}", + "id": "arrowDirection", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", - "trueLabel": "{i18n:scada.symbol.present}", - "falseLabel": "{i18n:scada.symbol.absent}", - "stateLabel": "{i18n:scada.symbol.flow-present}", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", "defaultGetValueSettings": { "action": "DO_NOTHING", - "defaultValue": false, + "defaultValue": true, "executeRpc": { "method": "getState", "requestTimeout": 5000, @@ -107,20 +111,16 @@ "persistentPollingInterval": 1000 }, "getAttribute": { - "key": "state", - "scope": null + "scope": null, + "key": "state" }, "getTimeSeries": { "key": "state" }, - "getAlarmStatus": { - "severityList": null, - "typeList": null - }, "dataToValue": { "type": "NONE", - "compareToValue": true, - "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true } }, "defaultSetValueSettings": null, @@ -167,48 +167,22 @@ ], "properties": [ { - "id": "mainLine", - "name": "{i18n:scada.symbol.main-line}", - "type": "switch", - "default": true, - "disabled": false, - "visible": true - }, - { - "id": "mainLineSize", - "name": "{i18n:scada.symbol.main-line}", + "id": "lineSize", + "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, "required": true, - "subLabel": "Main", - "divider": true, + "divider": false, "fieldSuffix": "px", - "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, "visible": true }, - { - "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.main-line}", - "type": "number", - "default": 2, - "required": true, - "subLabel": "Secondary", - "divider": true, - "fieldSuffix": "px", - "condition": "return !model.mainLine;", - "min": 0, - "max": 99, - "step": 1, - "disabled": false, - "visible": false - }, { "id": "lineColor", - "name": "{i18n:scada.symbol.main-line}", + "name": "{i18n:scada.symbol.line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -216,12 +190,11 @@ }, { "id": "flowAnimationWidth", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 4, - "subLabel": "Width", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 1, "step": 1, @@ -230,8 +203,8 @@ }, { "id": "flowAnimationColor", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "color", "default": "#C8DFF7", "disabled": false, @@ -239,14 +212,14 @@ }, { "id": "flowStyleDash", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "required": true, "subLabel": "{i18n:scada.symbol.dash}", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 0, "step": 1, @@ -255,22 +228,20 @@ }, { "id": "flowStyleGap", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "subLabel": "{i18n:scada.symbol.gap}", "fieldSuffix": "px", - "min": 1, - "step": 1, - "disabled": false, - "visible": true + "min": 0, + "step": 1 }, { "id": "flowDashCap", "name": "{i18n:scada.symbol.flow-dash-cap}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "select", "default": "butt", "items": [ diff --git a/application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg index e40936c152..dafcf53647 100644 --- a/application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg @@ -7,7 +7,7 @@ "tags": [ { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", "actions": null }, { @@ -58,8 +58,8 @@ }, { "id": "topFlowDirection", - "name": "{i18n:scada.symbol.animation-direction}", - "hint": "{i18n:scada.symbol.animation-direction-hint}", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": "{i18n:scada.symbol.top-connector}", "type": "value", "valueType": "BOOLEAN", @@ -174,8 +174,8 @@ }, { "id": "rightFlowDirection", - "name": "{i18n:scada.symbol.flow-direction}", - "hint": "{i18n:scada.symbol.flow-direction-hint}", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": "{i18n:scada.symbol.right-connector}", "type": "value", "valueType": "BOOLEAN", @@ -290,8 +290,8 @@ }, { "id": "bottomFlowDirection", - "name": "{i18n:scada.symbol.animation-direction}", - "hint": "{i18n:scada.symbol.animation-direction-hint}", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": "{i18n:scada.symbol.bottom-connector}", "type": "value", "valueType": "BOOLEAN", @@ -368,48 +368,22 @@ ], "properties": [ { - "id": "mainLine", - "name": "{i18n:scada.symbol.main-line}", - "type": "switch", - "default": true, - "disabled": false, - "visible": true - }, - { - "id": "mainLineSize", - "name": "{i18n:scada.symbol.main-line}", + "id": "lineSize", + "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, "required": true, - "subLabel": "Main", - "divider": true, + "divider": false, "fieldSuffix": "px", - "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, "visible": true }, - { - "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.main-line}", - "type": "number", - "default": 2, - "required": true, - "subLabel": "Secondary", - "divider": true, - "fieldSuffix": "px", - "condition": "return !model.mainLine;", - "min": 0, - "max": 99, - "step": 1, - "disabled": false, - "visible": false - }, { "id": "lineColor", - "name": "{i18n:scada.symbol.main-line}", + "name": "{i18n:scada.symbol.line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -417,12 +391,11 @@ }, { "id": "flowAnimationWidth", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 4, - "subLabel": "Width", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 1, "step": 1, @@ -431,8 +404,8 @@ }, { "id": "flowAnimationColor", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "color", "default": "#C8DFF7", "disabled": false, @@ -440,14 +413,14 @@ }, { "id": "flowStyleDash", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "required": true, "subLabel": "{i18n:scada.symbol.dash}", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 0, "step": 1, @@ -456,14 +429,14 @@ }, { "id": "flowStyleGap", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "subLabel": "{i18n:scada.symbol.gap}", "fieldSuffix": "px", - "min": 1, + "min": 0, "step": 1, "disabled": false, "visible": true @@ -471,7 +444,7 @@ { "id": "flowDashCap", "name": "{i18n:scada.symbol.flow-dash-cap}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "select", "default": "butt", "items": [ diff --git a/application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg index 8c5224c9f8..f9f2fe20d2 100644 --- a/application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg @@ -7,7 +7,7 @@ "tags": [ { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", "actions": null } ], @@ -53,8 +53,8 @@ }, { "id": "animationDirection", - "name": "{i18n:scada.symbol.animation-direction}", - "hint": "{i18n:scada.symbol.animation-direction-hint}", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", @@ -131,48 +131,22 @@ ], "properties": [ { - "id": "mainLine", - "name": "{i18n:scada.symbol.main-line}", - "type": "switch", - "default": true, - "disabled": false, - "visible": true - }, - { - "id": "mainLineSize", - "name": "{i18n:scada.symbol.main-line}", + "id": "lineSize", + "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, "required": true, - "subLabel": "Main", - "divider": true, + "divider": false, "fieldSuffix": "px", - "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, "visible": true }, - { - "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.main-line}", - "type": "number", - "default": 2, - "required": true, - "subLabel": "Secondary", - "divider": true, - "fieldSuffix": "px", - "condition": "return !model.mainLine;", - "min": 0, - "max": 99, - "step": 1, - "disabled": false, - "visible": false - }, { "id": "lineColor", - "name": "{i18n:scada.symbol.main-line}", + "name": "{i18n:scada.symbol.line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -180,12 +154,11 @@ }, { "id": "flowAnimationWidth", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 4, - "subLabel": "Width", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 1, "step": 1, @@ -194,8 +167,8 @@ }, { "id": "flowAnimationColor", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "color", "default": "#C8DFF7", "disabled": false, @@ -203,14 +176,14 @@ }, { "id": "flowStyleDash", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "required": true, "subLabel": "{i18n:scada.symbol.dash}", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 0, "step": 1, @@ -219,14 +192,14 @@ }, { "id": "flowStyleGap", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "subLabel": "{i18n:scada.symbol.gap}", "fieldSuffix": "px", - "min": 1, + "min": 0, "step": 1, "disabled": false, "visible": true @@ -234,7 +207,7 @@ { "id": "flowDashCap", "name": "{i18n:scada.symbol.flow-dash-cap}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "select", "default": "butt", "items": [ diff --git a/application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg index 101799fd4b..f61bf85ccd 100644 --- a/application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg @@ -7,7 +7,7 @@ "tags": [ { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", "actions": null }, { @@ -58,8 +58,8 @@ }, { "id": "leftFlowDirection", - "name": "{i18n:scada.symbol.animation-direction}", - "hint": "{i18n:scada.symbol.animation-direction-hint}", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": "{i18n:scada.symbol.left-connector}", "type": "value", "valueType": "BOOLEAN", @@ -174,8 +174,8 @@ }, { "id": "topFlowDirection", - "name": "{i18n:scada.symbol.animation-direction}", - "hint": "{i18n:scada.symbol.animation-direction-hint}", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": "{i18n:scada.symbol.top-connector}", "type": "value", "valueType": "BOOLEAN", @@ -290,8 +290,8 @@ }, { "id": "rightFlowDirection", - "name": "{i18n:scada.symbol.flow-direction}", - "hint": "{i18n:scada.symbol.flow-direction-hint}", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": "{i18n:scada.symbol.right-connector}", "type": "value", "valueType": "BOOLEAN", @@ -368,48 +368,22 @@ ], "properties": [ { - "id": "mainLine", - "name": "{i18n:scada.symbol.main-line}", - "type": "switch", - "default": true, - "disabled": false, - "visible": true - }, - { - "id": "mainLineSize", - "name": "{i18n:scada.symbol.main-line}", + "id": "lineSize", + "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, "required": true, - "subLabel": "Main", - "divider": true, + "divider": false, "fieldSuffix": "px", - "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, "visible": true }, - { - "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.main-line}", - "type": "number", - "default": 2, - "required": true, - "subLabel": "Secondary", - "divider": true, - "fieldSuffix": "px", - "condition": "return !model.mainLine;", - "min": 0, - "max": 99, - "step": 1, - "disabled": false, - "visible": false - }, { "id": "lineColor", - "name": "{i18n:scada.symbol.main-line}", + "name": "{i18n:scada.symbol.line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -417,12 +391,11 @@ }, { "id": "flowAnimationWidth", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 4, - "subLabel": "Width", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 1, "step": 1, @@ -431,8 +404,8 @@ }, { "id": "flowAnimationColor", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "color", "default": "#C8DFF7", "disabled": false, @@ -440,14 +413,14 @@ }, { "id": "flowStyleDash", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "required": true, "subLabel": "{i18n:scada.symbol.dash}", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 0, "step": 1, @@ -456,14 +429,14 @@ }, { "id": "flowStyleGap", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "subLabel": "{i18n:scada.symbol.gap}", "fieldSuffix": "px", - "min": 1, + "min": 0, "step": 1, "disabled": false, "visible": true @@ -471,7 +444,7 @@ { "id": "flowDashCap", "name": "{i18n:scada.symbol.flow-dash-cap}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "select", "default": "butt", "items": [ diff --git a/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg index 698a910f8d..9667928e4e 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg @@ -12,7 +12,7 @@ }, { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nif (ctx.properties.mainLine) {\n element.attr({'stroke-width': ctx.properties.mainLineSize});\n} else {\n element.attr({'stroke-width': ctx.properties.secondaryLineSize});\n}", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", "actions": null } ], @@ -53,18 +53,18 @@ "defaultWidgetActionSettings": null }, { - "id": "arrowDirection", - "name": "{i18n:scada.symbol.arrow-direction}", - "hint": "{i18n:scada.symbol.arrow-direction-hint}", + "id": "flowAnimation", + "name": "{i18n:scada.symbol.flow-animation}", + "hint": "{i18n:scada.symbol.flow-animation-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", - "trueLabel": "{i18n:scada.symbol.forward}", - "falseLabel": "{i18n:scada.symbol.reverse}", - "stateLabel": "{i18n:scada.symbol.forward}", + "trueLabel": "{i18n:scada.symbol.present}", + "falseLabel": "{i18n:scada.symbol.absent}", + "stateLabel": "{i18n:scada.symbol.flow-present}", "defaultGetValueSettings": { "action": "DO_NOTHING", - "defaultValue": true, + "defaultValue": false, "executeRpc": { "method": "getState", "requestTimeout": 5000, @@ -72,34 +72,38 @@ "persistentPollingInterval": 1000 }, "getAttribute": { - "scope": null, - "key": "state" + "key": "state", + "scope": null }, "getTimeSeries": { "key": "state" }, + "getAlarmStatus": { + "severityList": null, + "typeList": null + }, "dataToValue": { "type": "NONE", - "dataToValueFunction": "/* Should return boolean value */\nreturn data;", - "compareToValue": true + "compareToValue": true, + "dataToValueFunction": "/* Should return boolean value */\nreturn data;" } }, "defaultSetValueSettings": null, "defaultWidgetActionSettings": null }, { - "id": "flowAnimation", - "name": "{i18n:scada.symbol.flow-animation}", - "hint": "{i18n:scada.symbol.flow-animation-hint}", + "id": "arrowDirection", + "name": "{i18n:scada.symbol.arrow-direction}", + "hint": "{i18n:scada.symbol.arrow-direction-hint}", "group": null, "type": "value", "valueType": "BOOLEAN", - "trueLabel": "{i18n:scada.symbol.present}", - "falseLabel": "{i18n:scada.symbol.absent}", - "stateLabel": "{i18n:scada.symbol.flow-present}", + "trueLabel": "{i18n:scada.symbol.forward}", + "falseLabel": "{i18n:scada.symbol.reverse}", + "stateLabel": "{i18n:scada.symbol.forward}", "defaultGetValueSettings": { "action": "DO_NOTHING", - "defaultValue": false, + "defaultValue": true, "executeRpc": { "method": "getState", "requestTimeout": 5000, @@ -107,20 +111,16 @@ "persistentPollingInterval": 1000 }, "getAttribute": { - "key": "state", - "scope": null + "scope": null, + "key": "state" }, "getTimeSeries": { "key": "state" }, - "getAlarmStatus": { - "severityList": null, - "typeList": null - }, "dataToValue": { "type": "NONE", - "compareToValue": true, - "dataToValueFunction": "/* Should return boolean value */\nreturn data;" + "dataToValueFunction": "/* Should return boolean value */\nreturn data;", + "compareToValue": true } }, "defaultSetValueSettings": null, @@ -167,48 +167,22 @@ ], "properties": [ { - "id": "mainLine", - "name": "{i18n:scada.symbol.main-line}", - "type": "switch", - "default": true, - "disabled": false, - "visible": true - }, - { - "id": "mainLineSize", - "name": "{i18n:scada.symbol.main-line}", + "id": "lineSize", + "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, "required": true, - "subLabel": "Main", - "divider": true, + "divider": false, "fieldSuffix": "px", - "condition": "return model.mainLine;", "min": 0, "max": 99, "step": 1, "disabled": false, "visible": true }, - { - "id": "secondaryLineSize", - "name": "{i18n:scada.symbol.main-line}", - "type": "number", - "default": 2, - "required": true, - "subLabel": "Secondary", - "divider": true, - "fieldSuffix": "px", - "condition": "return !model.mainLine;", - "min": 0, - "max": 99, - "step": 1, - "disabled": false, - "visible": false - }, { "id": "lineColor", - "name": "{i18n:scada.symbol.main-line}", + "name": "{i18n:scada.symbol.line}", "type": "color", "default": "#1A1A1A", "disabled": false, @@ -216,12 +190,11 @@ }, { "id": "flowAnimationWidth", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 4, - "subLabel": "Width", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 1, "step": 1, @@ -230,8 +203,8 @@ }, { "id": "flowAnimationColor", - "name": "{i18n:scada.symbol.flow}", - "group": "{i18n:scada.symbol.animation}", + "name": "{i18n:scada.symbol.flow-line}", + "group": "{i18n:scada.symbol.flow}", "type": "color", "default": "#C8DFF7", "disabled": false, @@ -239,14 +212,14 @@ }, { "id": "flowStyleDash", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "required": true, "subLabel": "{i18n:scada.symbol.dash}", - "divider": true, + "divider": false, "fieldSuffix": "px", "min": 0, "step": 1, @@ -255,14 +228,14 @@ }, { "id": "flowStyleGap", - "name": "{i18n:scada.symbol.flow-style}", + "name": "{i18n:scada.symbol.flow-line-style}", "hint": "{i18n:scada.symbol.flow-style-hint}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "number", "default": 10, "subLabel": "{i18n:scada.symbol.gap}", "fieldSuffix": "px", - "min": 1, + "min": 0, "step": 1, "disabled": false, "visible": true @@ -270,7 +243,7 @@ { "id": "flowDashCap", "name": "{i18n:scada.symbol.flow-dash-cap}", - "group": "{i18n:scada.symbol.animation}", + "group": "{i18n:scada.symbol.flow}", "type": "select", "default": "butt", "items": [ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts index ee63dc59a6..9e0d07fed4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts @@ -190,7 +190,7 @@ const tbNamespaceRegex = //gm const tbTagRegex = /tb:tag="([^"]*)"/gms; -let syncTime = Date.now(); +const syncTime = Date.now(); const generateElementId = () => { const id = guid(); @@ -1279,9 +1279,6 @@ class FlowConnectorAnimation implements ConnectorScadaSymbolAnimation { if (!this.element.node.childElementCount) { this.element.add(this._animation); } - if (!syncTime) { - syncTime = Date.now(); - } const animateElement = this.element.node.getElementsByTagName('animate')[0]; const offset = ((Date.now() - syncTime) % 1000) * -1; (animateElement as SVGAnimationElement).beginElementAt(offset); @@ -1310,7 +1307,7 @@ class FlowConnectorAnimation implements ConnectorScadaSymbolAnimation { this._strokeLineCap = linecap; this._dashWidth = dashWidth - offset; this._dashGap = dashGap - offset; - const dashArray = `${this._dashWidth}${this._dashGap ? ` ${this._dashGap}` : ''}`; + const dashArray = `${this._dashWidth} ${this._dashGap}`; const values = `${this._dashWidth + (this._dashGap || this._dashWidth)};0`; this._animation.stroke({width, color, linecap, dasharray: dashArray}); this._animation.findOne('animate').attr('values', values); 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 a3ba654258..b9eaf3cb82 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -3438,16 +3438,15 @@ "arrow-presence": "Arrow presence", "arrow-presence-hint": "Indicates whether arrow is present in connector.", "arrow-present": "Arrow present", - "arrow-direction": "Arrow/Animation direction", + "arrow-direction": "Flow direction", "arrow-direction-hint": "Indicates flow direction.", - "animation-direction": "Flow animation direction", - "animation-direction-hint": "Indicates animation flow direction.", - "flow-animation": "Flow animation", - "flow-animation-hint": "Indicates whether animation is present in connector.", + "flow-animation": "Flow presence", + "flow-animation-hint": "Indicates whether fluid is flowing in connector.", "flow": "Flow", - "flow-style": "Flow style", + "flow-line": "Line", + "flow-line-style": "Line style", "flow-style-hint": "Set the Dash and Gap values so that their sum is divisible by 100 without a remainder for perfect animation synchronization.", - "flow-dash-cap": "Flow dash cap", + "flow-dash-cap": "Dash cap", "dash-cap-butt": "Butt", "dash-cap-round": "Round", "dash-cap-square": "Square", From 777228afe4d235b4b835403cbd2c80a0e4e2dbc2 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Thu, 1 May 2025 16:39:46 +0300 Subject: [PATCH 32/63] UI: Rename id property --- .../system/scada_symbols/bottom-right-elbow-connector-hp.svg | 4 ++-- .../json/system/scada_symbols/bottom-tee-connector-hp.svg | 4 ++-- .../data/json/system/scada_symbols/cross-connector-hp.svg | 2 +- .../json/system/scada_symbols/horizontal-connector-hp.svg | 4 ++-- .../system/scada_symbols/left-bottom-elbow-connector-hp.svg | 4 ++-- .../data/json/system/scada_symbols/left-tee-connector-hp.svg | 4 ++-- .../json/system/scada_symbols/left-top-elbow-connector-hp.svg | 4 ++-- .../system/scada_symbols/long-horizontal-connector-hp.svg | 4 ++-- .../json/system/scada_symbols/long-vertical-connector-hp.svg | 4 ++-- .../data/json/system/scada_symbols/right-tee-connector-hp.svg | 4 ++-- .../system/scada_symbols/top-right-elbow-connector-hp.svg | 4 ++-- .../data/json/system/scada_symbols/top-tee-connector-hp.svg | 4 ++-- .../data/json/system/scada_symbols/vertical-connector-hp.svg | 4 ++-- 13 files changed, 25 insertions(+), 25 deletions(-) diff --git a/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg index f7ffd5f167..a0a56e37e5 100644 --- a/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/bottom-right-elbow-connector-hp.svg @@ -7,7 +7,7 @@ "tags": [ { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});", "actions": null } ], @@ -131,7 +131,7 @@ ], "properties": [ { - "id": "lineSize", + "id": "mainLineSize", "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, diff --git a/application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg index bdbbdca81f..90cfe6ed2b 100644 --- a/application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/bottom-tee-connector-hp.svg @@ -7,7 +7,7 @@ "tags": [ { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});", "actions": null }, { @@ -368,7 +368,7 @@ ], "properties": [ { - "id": "lineSize", + "id": "mainLineSize", "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, diff --git a/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg index db6b091e0e..5ac0af8248 100644 --- a/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/cross-connector-hp.svg @@ -7,7 +7,7 @@ "tags": [ { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});", "actions": null }, { diff --git a/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg index 93ef81f57b..f2fc9fb2e0 100644 --- a/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/horizontal-connector-hp.svg @@ -12,7 +12,7 @@ }, { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});", "actions": null } ], @@ -167,7 +167,7 @@ ], "properties": [ { - "id": "lineSize", + "id": "mainLineSize", "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, diff --git a/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg index 1c24cca2e0..8fd4ee09df 100644 --- a/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/left-bottom-elbow-connector-hp.svg @@ -7,7 +7,7 @@ "tags": [ { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});", "actions": null } ], @@ -131,7 +131,7 @@ ], "properties": [ { - "id": "lineSize", + "id": "mainLineSize", "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, diff --git a/application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg index 461e91fe94..c3b4e67d56 100644 --- a/application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/left-tee-connector-hp.svg @@ -7,7 +7,7 @@ "tags": [ { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});", "actions": null }, { @@ -368,7 +368,7 @@ ], "properties": [ { - "id": "lineSize", + "id": "mainLineSize", "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, diff --git a/application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg index 19e217a428..961c760f63 100644 --- a/application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/left-top-elbow-connector-hp.svg @@ -7,7 +7,7 @@ "tags": [ { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});", "actions": null } ], @@ -131,7 +131,7 @@ ], "properties": [ { - "id": "lineSize", + "id": "mainLineSize", "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, diff --git a/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg index 0fe805c9a7..f5f68c4934 100644 --- a/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/long-horizontal-connector-hp.svg @@ -12,7 +12,7 @@ }, { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});", "actions": null } ], @@ -167,7 +167,7 @@ ], "properties": [ { - "id": "lineSize", + "id": "mainLineSize", "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, diff --git a/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg index 411859b5ed..f344823082 100644 --- a/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/long-vertical-connector-hp.svg @@ -12,7 +12,7 @@ }, { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});", "actions": null } ], @@ -167,7 +167,7 @@ ], "properties": [ { - "id": "lineSize", + "id": "mainLineSize", "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, diff --git a/application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg index dafcf53647..346d134f30 100644 --- a/application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/right-tee-connector-hp.svg @@ -7,7 +7,7 @@ "tags": [ { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});", "actions": null }, { @@ -368,7 +368,7 @@ ], "properties": [ { - "id": "lineSize", + "id": "mainLineSize", "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, diff --git a/application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg index f9f2fe20d2..ca255abf14 100644 --- a/application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/top-right-elbow-connector-hp.svg @@ -7,7 +7,7 @@ "tags": [ { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});", "actions": null } ], @@ -131,7 +131,7 @@ ], "properties": [ { - "id": "lineSize", + "id": "mainLineSize", "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, diff --git a/application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg index f61bf85ccd..e63d465d8e 100644 --- a/application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/top-tee-connector-hp.svg @@ -7,7 +7,7 @@ "tags": [ { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});", "actions": null }, { @@ -368,7 +368,7 @@ ], "properties": [ { - "id": "lineSize", + "id": "mainLineSize", "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, diff --git a/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg b/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg index 9667928e4e..87c3ef23ee 100644 --- a/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg +++ b/application/src/main/data/json/system/scada_symbols/vertical-connector-hp.svg @@ -12,7 +12,7 @@ }, { "tag": "line", - "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.lineSize});", + "stateRenderFunction": "element.stroke(ctx.properties.lineColor);\nelement.attr({'stroke-width': ctx.properties.mainLineSize});", "actions": null } ], @@ -167,7 +167,7 @@ ], "properties": [ { - "id": "lineSize", + "id": "mainLineSize", "name": "{i18n:scada.symbol.line}", "type": "number", "default": 6, From 357a1512e863150366fa0f234f9814daf8a57db1 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 2 May 2025 11:26:06 +0300 Subject: [PATCH 33/63] UI: Improved dashboard autocomplete - ignoreError and add error handling --- .../components/dashboard-autocomplete.component.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts b/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts index 08893efa91..4a74faaf66 100644 --- a/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/dashboard-autocomplete.component.ts @@ -190,15 +190,22 @@ export class DashboardAutocompleteComponent implements ControlValueAccessor, OnI this.searchText = ''; if (value != null) { if (typeof value === 'string') { - this.dashboardService.getDashboardInfo(value).subscribe( - (dashboard) => { + this.dashboardService.getDashboardInfo(value, {ignoreLoading: true, ignoreErrors: true}).subscribe({ + next: (dashboard) => { this.modelValue = this.useIdValue ? dashboard.id.id : dashboard; if (this.useDashboardLink) { this.dashboardURL = getEntityDetailsPageURL(this.modelValue as string, EntityType.DASHBOARD); } this.selectDashboardFormGroup.get('dashboard').patchValue(dashboard, {emitEvent: false}); + }, + error: () => { + this.modelValue = null; + this.selectDashboardFormGroup.get('dashboard').patchValue('', {emitEvent: false}); + if (this.required) { + this.propagateChange(this.modelValue); + } } - ); + }); } else { this.modelValue = this.useIdValue ? value.id.id : value; this.selectDashboardFormGroup.get('dashboard').patchValue(value, {emitEvent: false}); From b7081eed0314ca8cee655744096c4f27b99151b7 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 2 May 2025 11:27:28 +0300 Subject: [PATCH 34/63] UI: Add completion for new scada api --- .../widget/lib/scada/scada-symbol.models.ts | 2 +- .../scada-symbol-editor.models.ts | 64 +++++++++++++++++++ ui-ngx/src/app/shared/models/constants.ts | 1 + 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts index 9e0d07fed4..7bd835de88 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/scada/scada-symbol.models.ts @@ -84,7 +84,7 @@ export interface ScadaSymbolApi { resetCssAnimation: (element: Element) => void; finishCssAnimation: (element: Element) => void; connectorAnimation:(element: Element) => ConnectorScadaSymbolAnimation | undefined; - connectorAnimate:(element: Element) => ConnectorScadaSymbolAnimation; + connectorAnimate:(element: Element, path: string, reversedPath: string) => ConnectorScadaSymbolAnimation; resetConnectorAnimation: (element: Element) => void; finishConnectorAnimation: (element: Element) => void; disable: (element: Element | Element[]) => void; diff --git a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts index 13b55d4f67..56d2801670 100644 --- a/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts +++ b/ui-ngx/src/app/modules/home/pages/scada-symbol/scada-symbol-editor.models.ts @@ -1129,6 +1129,8 @@ export const scadaSymbolContextCompletion = (metadata: ScadaSymbolMetadata, tags const scadaSymbolAnimationLink = HelpLinks.linksMap.scadaSymbolDevAnimation; const scadaSymbolAnimation = `ScadaSymbolAnimation`; + const connectorScadaSymbolAnimationLink = HelpLinks.linksMap.scadaSymbolDevConnectorAnimation; + const connectorScadaSymbolAnimation = `ConnectorScadaSymbolAnimation`; const properties: TbEditorCompletion = { meta: 'object', @@ -1229,6 +1231,68 @@ export const scadaSymbolContextCompletion = (metadata: ScadaSymbolMetadata, tags }, ] }, + connectorAnimate: { + meta: 'function', + description: 'Finishes any previous connector animation and starts a new connector animation for the SVG element along the specified path.', + args: [ + { + name: 'element', + description: 'SVG element', + type: 'Element' + }, + { + name: 'path', + description: 'Path defining the animation trajectory', + type: 'string' + }, + { + name: 'reversedPath', + description: 'Path for the reversed animation trajectory', + type: 'string' + } + ], + return: { + description: `Instance of ${connectorScadaSymbolAnimation} class with API to setup and control the connector animation.`, + type: connectorScadaSymbolAnimation + } + }, + connectorAnimation: { + meta: 'function', + description: 'Gets the current connector animation applied to the SVG element.', + args: [ + { + name: 'element', + description: 'SVG element', + type: 'Element' + } + ], + return: { + description: `Instance of ${connectorScadaSymbolAnimation} class with API to setup and control the connector animation, or undefined if no animation is applied.`, + type: 'ConnectorScadaSymbolAnimation | undefined' + } + }, + resetConnectorAnimation: { + meta: 'function', + description: 'Stops the connector animation if any and restores the SVG element to its initial state, removes the connector animation instance.', + args: [ + { + name: 'element', + description: 'SVG element', + type: 'Element' + } + ] + }, + finishConnectorAnimation: { + meta: 'function', + description: 'Finishes the connector animation if any, updates the SVG element state according to the end animation values, removes the connector animation instance.', + args: [ + { + name: 'element', + description: 'SVG element', + type: 'Element' + } + ] + }, generateElementId: { meta: 'function', description: 'Generates new unique element id.', diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index e1306e577c..0f22de90b9 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -194,6 +194,7 @@ export const HelpLinks = { scada: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada`, scadaSymbolDev: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/`, scadaSymbolDevAnimation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/#scadasymbolanimation`, + scadaSymbolDevConnectorAnimation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/scada/scada-symbols-dev-guide/#connectorscadasymbolanimation`, domains: `${helpBaseUrl}/docs${docPlatformPrefix}/domains`, mobileApplication: `${helpBaseUrl}/docs${docPlatformPrefix}/mobile-center/applications/`, mobileBundle: `${helpBaseUrl}/docs${docPlatformPrefix}/mobile-center/mobile-center/`, From 71724b95d6a0e903c24df7148f02bddde423bd76 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 2 May 2025 13:14:51 +0300 Subject: [PATCH 35/63] UI: Fixed percent value for doughnut chart --- .../home/components/widget/lib/chart/pie-chart.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.ts index 2cb5c5a031..c985fcb55a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.ts @@ -122,17 +122,10 @@ export class TbPieChart extends TbLatestChart { seriesData.push( {id: dataItem.id, value: dataItem.value, name: dataItem.dataKey.label, itemStyle: {color: dataItem.dataKey.color}} ); - if (this.settings.doughnut && enabledDataItems.length > 1) { - seriesData.push({ - value: 0, name: '', itemStyle: {color: 'transparent'}, emphasis: {disabled: true} - }); - } } } if (this.settings.doughnut) { - for (let i = 1; i < seriesData.length; i += 2) { - seriesData[i].value = this.total / 100; - } + this.latestChartOption.series[0].padAngle = 2; } this.latestChartOption.series[0].data = seriesData; } From 1c5fc16b58eb37d82d08b3fc55f853e3cfc47f86 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 2 May 2025 13:19:40 +0300 Subject: [PATCH 36/63] UI: Remove unused constant --- .../app/modules/home/components/widget/lib/chart/pie-chart.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.ts index c985fcb55a..ec56ce5478 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.ts @@ -116,7 +116,6 @@ export class TbPieChart extends TbLatestChart { protected doUpdateSeriesData() { const seriesData: PieDataItemOption[] = []; - const enabledDataItems = this.dataItems.filter(item => item.enabled && item.hasValue); for (const dataItem of this.dataItems) { if (dataItem.enabled && dataItem.hasValue) { seriesData.push( From 23ded89dea5498c626b84cc0d4637a14e11c71ed Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 2 May 2025 13:31:37 +0300 Subject: [PATCH 37/63] UI: Refactoring --- .../app/modules/home/components/widget/lib/chart/pie-chart.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.ts index ec56ce5478..d4279caa69 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart.ts @@ -116,6 +116,7 @@ export class TbPieChart extends TbLatestChart { protected doUpdateSeriesData() { const seriesData: PieDataItemOption[] = []; + const enabledDataItems = this.dataItems.filter(item => item.enabled && item.hasValue); for (const dataItem of this.dataItems) { if (dataItem.enabled && dataItem.hasValue) { seriesData.push( @@ -124,7 +125,7 @@ export class TbPieChart extends TbLatestChart { } } if (this.settings.doughnut) { - this.latestChartOption.series[0].padAngle = 2; + this.latestChartOption.series[0].padAngle = enabledDataItems.length > 1 ? 2 : 0; } this.latestChartOption.series[0].data = seriesData; } From 3aab8b7197862a81806e374f58435dd6b8c21a08 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Fri, 2 May 2025 20:31:36 +0300 Subject: [PATCH 38/63] lwm2m: create updateAttrTelemetry with updateResource - all change in one ArrayList to core --- .../lwm2m/AbstractLwM2MIntegrationTest.java | 2 +- .../rpc/AbstractRpcLwM2MIntegrationTest.java | 3 +- .../sql/RpcLwm2mIntegrationCreateTest.java | 6 +- .../sql/RpcLwm2mIntegrationObserveTest.java | 8 +- .../rpc/sql/RpcLwm2mIntegrationReadTest.java | 14 +- .../sql/NoSecLwM2MIntegrationTest.java | 26 -- ...veStrategyTransportConfigurationTest.java} | 2 +- ...StrategyWithNoSecQueueModeConnectTest.java | 52 ++++ .../lwm2m/server/client/LwM2mClient.java | 16 -- .../server/client/ResultUpdateResource.java | 19 ++ .../TbLwM2MCreateResponseCallback.java | 2 +- .../uplink/DefaultLwM2mUplinkMsgHandler.java | 236 ++++++++++-------- .../server/uplink/LwM2mUplinkMsgHandler.java | 3 +- 13 files changed, 225 insertions(+), 164 deletions(-) rename application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/{TransportConfigurationTest.java => ObserveStrategyTransportConfigurationTest.java} (95%) create mode 100644 application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/ObserveStrategyWithNoSecQueueModeConnectTest.java create mode 100644 common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultUpdateResource.java diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index 022221ac8e..5fec5107bd 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -354,7 +354,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte Assert.assertTrue(expectedMax >= Long.parseLong(tsValue1.getValue())); Assert.assertTrue(expectedMin <= Long.parseLong(tsValue1.getValue())); } else { - String pattern = "MMM dd, yyyy HH:mm a"; + String pattern = "MMM d, yyyy HH:mm a"; TbDate d = new TbDate(tsValue2.getValue(), pattern, "en-US"); Assert.assertNotNull(d); } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java index a409ea0491..fe571eb9b3 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java @@ -29,6 +29,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServerHelper; +import org.thingsboard.server.transport.lwm2m.server.client.ResultUpdateResource; import java.util.List; import java.util.Set; @@ -258,7 +259,7 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg .filter(invocation -> invocation.getMethod().getName().equals("updateAttrTelemetry") && invocation.getArguments().length > 1 && - idVerRez.equals(invocation.getArguments()[1]) + ((ResultUpdateResource)invocation.getArguments()[0]).getPaths().toString().contains(idVerRez) ) .count(); } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationCreateTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationCreateTest.java index 6b39b51a65..ec0dcd7949 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationCreateTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationCreateTest.java @@ -67,7 +67,7 @@ public class RpcLwm2mIntegrationCreateTest extends AbstractRpcLwM2MIntegrationTe assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); String expected = "instance " + OBJECT_INSTANCE_ID_0 + " already exists"; String actual = rpcActualResult.get("error").asText(); - assertTrue(actual.equals(expected)); + assertEquals(actual, expected); } /** @@ -84,7 +84,7 @@ public class RpcLwm2mIntegrationCreateTest extends AbstractRpcLwM2MIntegrationTe assertEquals(ResponseCode.BAD_REQUEST.getName(), rpcActualResult.get("result").asText()); String expected = "Path " + expectedPath + ". Object must be Multiple !"; String actual = rpcActualResult.get("error").asText(); - assertTrue(actual.equals(expected)); + assertEquals(actual, expected); } /** @@ -122,7 +122,7 @@ public class RpcLwm2mIntegrationCreateTest extends AbstractRpcLwM2MIntegrationTe LwM2mPath expectedPathId = new LwM2mPath(expectedObjectId); String expected = "Specified object id " + expectedPathId.getObjectId() + " absent in the list supported objects of the client or is security object!"; String actual = rpcActualResult.get("error").asText(); - assertTrue(actual.equals(expected)); + assertEquals(actual, expected); } private String sendRPCreateById(String path, String value) throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java index b1ab47717b..1ba89720ae 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java @@ -20,11 +20,11 @@ import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.LwM2m.Version; import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.node.LwM2mPath; -import org.eclipse.leshan.server.registration.Registration; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; +import org.thingsboard.server.transport.lwm2m.server.client.ResultUpdateResource; import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL; import static org.junit.Assert.assertEquals; @@ -80,7 +80,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT int cntUpdate = 3; verify(defaultUplinkMsgHandlerTest, timeout(10000).atLeast(cntUpdate)) - .updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9), eq(null)); + .updateAttrTelemetry(Mockito.any(ResultUpdateResource.class), eq(null)); } /** @@ -95,7 +95,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT int cntUpdate = 3; verify(defaultUplinkMsgHandlerTest, timeout(10000).atLeast(cntUpdate)) - .updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9), eq(null)); + .updateAttrTelemetry(Mockito.any(ResultUpdateResource.class), eq(null)); } /** @@ -328,7 +328,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT int cntUpdate = 10; verify(defaultUplinkMsgHandlerTest, timeout(50000).atLeast(cntUpdate)) - .updateAttrTelemetry(Mockito.any(Registration.class), eq(idVer_3_0_9), eq(null)); + .updateAttrTelemetry(Mockito.any(ResultUpdateResource.class), eq(null)); } private void sendRpcObserveWithWithTwoResource(String expectedId_1, String expectedId_2) throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java index 9f45f02d7a..90b373b16c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationReadTest.java @@ -19,12 +19,10 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.node.LwM2mPath; -import org.junit.Before; import org.junit.Test; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; -import static org.eclipse.leshan.core.LwM2mId.SERVER; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -140,24 +138,24 @@ public class RpcLwm2mIntegrationReadTest extends AbstractRpcLwM2MIntegrationTest } /** - * ReadComposite {"ids":["/1_1.2", "/3_1.0/0/1", "/3_1.0/0/11"]} + * ReadComposite {"ids":["/19_1.1", "/3_1.0/0/1", "/3_1.0/0/11"]} */ @Test public void testReadCompositeSingleResourceByIds_Result_CONTENT_Value_IsObjectIsLwM2mSingleResourceIsLwM2mMultipleResource() throws Exception { - String expectedIdVer_1 = (String) expectedObjectIdVers.stream().filter(path -> (!((String) path).contains("/" + BINARY_APP_DATA_CONTAINER) && ((String) path).contains("/" + SERVER))).findFirst().get(); - String objectId_1 = pathIdVerToObjectId(expectedIdVer_1); + String expectedIdVer_19 = "/" + BINARY_APP_DATA_CONTAINER + "_" + lwM2MTestClient.getLeshanClient().getObjectTree().getModel().getObjectModel(BINARY_APP_DATA_CONTAINER).version; + String objectId_19 = pathIdVerToObjectId(expectedIdVer_19); String expectedIdVer3_0_1 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_1; String expectedIdVer3_0_11 = objectInstanceIdVer_3 + "/" + RESOURCE_ID_11; String objectInstanceId_3 = pathIdVerToObjectId(objectInstanceIdVer_3); - String expectedIds = "[\"" + expectedIdVer_1 + "\", \"" + expectedIdVer3_0_1 + "\", \"" + expectedIdVer3_0_11 + "\"]"; + String expectedIds = "[\"" + expectedIdVer_19 + "\", \"" + expectedIdVer3_0_1 + "\", \"" + expectedIdVer3_0_11 + "\"]"; String actualResult = sendCompositeRPCByIds(expectedIds); ObjectNode rpcActualResult = JacksonUtil.fromString(actualResult, ObjectNode.class); assertEquals(ResponseCode.CONTENT.getName(), rpcActualResult.get("result").asText()); - String expected1 = objectId_1 + "=LwM2mObject [id=" + new LwM2mPath(objectId_1).getObjectId() + ", instances={"; + String expected19 = objectId_19 + "=LwM2mObject [id=" + new LwM2mPath(objectId_19).getObjectId() + ", instances={"; String expected3_0_1 = objectInstanceId_3 + "/" + RESOURCE_ID_1 + "=LwM2mSingleResource [id=" + RESOURCE_ID_1 + ", value="; String expected3_0_11 = objectInstanceId_3 + "/" + RESOURCE_ID_11 + "=LwM2mMultipleResource [id=" + RESOURCE_ID_11 + ", values={"; String actualValues = rpcActualResult.get("value").asText(); - assertTrue(actualValues.contains(expected1)); + assertTrue(actualValues.contains(expected19)); assertTrue(actualValues.contains(expected3_0_1)); assertTrue(actualValues.contains(expected3_0_11)); } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java index 34683ffc71..192dc6d1d0 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java @@ -17,10 +17,8 @@ package org.thingsboard.server.transport.lwm2m.security.sql; import org.junit.Test; import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; -import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; -import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.COMPOSITE_BY_OBJECT; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_REGISTRATION_SUCCESS; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOOTSTRAP_ONLY; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.BOTH; @@ -37,30 +35,6 @@ public class NoSecLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationT super.basicTestConnectionObserveSingleTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, false); } - @Test - public void testWithNoSecQueueModeConnectLwm2mSuccessAndObserveSingleTelemetry() throws Exception { - String clientEndpoint = CLIENT_ENDPOINT_NO_SEC + "_QueueMode"; - LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); - super.basicTestConnectionObserveSingleTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, true); - } - - @Test - public void testWithNoSecQueueModeConnectLwm2mSuccessAndObserveCompositeAllTelemetry() throws Exception { - String clientEndpoint = CLIENT_ENDPOINT_NO_SEC + "_QueueMode"; - LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); - Lwm2mDeviceProfileTransportConfiguration transportConfiguration = super.getTransportConfiguration(TELEMETRY_WITH_COMPOSITE_OBSERVE, getBootstrapServerCredentialsNoSec(NONE)); - super.basicTestConnectionObserveCompositeTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, transportConfiguration, 1); - } - - @Test - public void testWithNoSecQueueModeConnectLwm2mSuccessAndObserveCompositeByObjectTelemetry() throws Exception { - String clientEndpoint = CLIENT_ENDPOINT_NO_SEC + "_QueueMode"; - LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); - Lwm2mDeviceProfileTransportConfiguration transportConfiguration = super.getTransportConfiguration(TELEMETRY_WITH_COMPOSITE_OBSERVE, getBootstrapServerCredentialsNoSec(NONE)); - transportConfiguration.getObserveAttr().setObserveStrategy(COMPOSITE_BY_OBJECT); - super.basicTestConnectionObserveCompositeTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, transportConfiguration, 2); - } - // Bootstrap + Lwm2m @Test public void testWithNoSecConnectBsSuccess_UpdateTwoSectionsBootstrapAndLm2m_ConnectLwm2mSuccess() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/TransportConfigurationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/ObserveStrategyTransportConfigurationTest.java similarity index 95% rename from application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/TransportConfigurationTest.java rename to application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/ObserveStrategyTransportConfigurationTest.java index 757bc81b41..c6a6bdad6d 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/TransportConfigurationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/ObserveStrategyTransportConfigurationTest.java @@ -24,7 +24,7 @@ import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryO import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.SINGLE; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; -public class TransportConfigurationTest extends AbstractSecurityLwM2MIntegrationTest { +public class ObserveStrategyTransportConfigurationTest extends AbstractSecurityLwM2MIntegrationTest { @Test public void testTransportConfigurationObserveStrategyBeforeParseNullAfterParseNotNull_STRATEGY_SINGLE() throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/ObserveStrategyWithNoSecQueueModeConnectTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/ObserveStrategyWithNoSecQueueModeConnectTest.java new file mode 100644 index 0000000000..85b17a0f41 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/ObserveStrategyWithNoSecQueueModeConnectTest.java @@ -0,0 +1,52 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.transportConfiguration; + +import org.junit.Test; +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; +import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; +import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; + +import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.COMPOSITE_BY_OBJECT; +import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; + +public class ObserveStrategyWithNoSecQueueModeConnectTest extends AbstractSecurityLwM2MIntegrationTest { + + @Test + public void testWithNoSecQueueModeConnectLwm2mSuccessAndObserveSingleTelemetry() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_NO_SEC + "_ObserveSingle"; + LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); + super.basicTestConnectionObserveSingleTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, true); + } + + @Test + public void testWithNoSecQueueModeConnectLwm2mSuccessAndObserveCompositeAllTelemetry() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_NO_SEC + "_ObserveCompositeAll"; + LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); + Lwm2mDeviceProfileTransportConfiguration transportConfiguration = super.getTransportConfiguration(TELEMETRY_WITH_COMPOSITE_OBSERVE, getBootstrapServerCredentialsNoSec(NONE)); + super.basicTestConnectionObserveCompositeTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, transportConfiguration, 1); + } + + @Test + public void testWithNoSecQueueModeConnectLwm2mSuccessAndObserveCompositeByObjectTelemetry() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_NO_SEC + "_ObserveCompositeByObject"; + LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); + Lwm2mDeviceProfileTransportConfiguration transportConfiguration = super.getTransportConfiguration(TELEMETRY_WITH_COMPOSITE_OBSERVE, getBootstrapServerCredentialsNoSec(NONE)); + transportConfiguration.getObserveAttr().setObserveStrategy(COMPOSITE_BY_OBJECT); + super.basicTestConnectionObserveCompositeTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, transportConfiguration, 2); + } +} + diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java index 6c470cf3b0..64597df9c5 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClient.java @@ -294,22 +294,6 @@ public class LwM2mClient { } - public Collection getNewResourceForInstance(String pathRezIdVer, Object params, LwM2mModelProvider modelProvider, - LwM2mValueConverter converter) { - LwM2mPath pathIds = getLwM2mPathFromString(pathRezIdVer); - Collection resources = ConcurrentHashMap.newKeySet(); - Map resourceModels = modelProvider.getObjectModel(registration) - .getObjectModel(pathIds.getObjectId()).resources; - resourceModels.forEach((resId, resourceModel) -> { - if (resId.equals(pathIds.getResourceId())) { - resources.add(LwM2mSingleResource.newResource(resId, converter.convertValue(params, - equalsResourceTypeGetSimpleName(params), resourceModel.type, pathIds), resourceModel.type)); - - } - }); - return resources; - } - /** * The instance must have all the resources that have the property * Mandatory diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultUpdateResource.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultUpdateResource.java new file mode 100644 index 0000000000..9a6c179eea --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultUpdateResource.java @@ -0,0 +1,19 @@ +package org.thingsboard.server.transport.lwm2m.server.client; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.HashSet; +import java.util.Set; + +@Data +@AllArgsConstructor +public class ResultUpdateResource { + LwM2mClient lwM2MClient; + Set paths; + + public ResultUpdateResource(LwM2mClient lwM2MClient) { + this.lwM2MClient = lwM2MClient; + this.paths = new HashSet<>(); + } +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/TbLwM2MCreateResponseCallback.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/TbLwM2MCreateResponseCallback.java index f13a8524b8..20e40f6838 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/TbLwM2MCreateResponseCallback.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/TbLwM2MCreateResponseCallback.java @@ -30,7 +30,7 @@ public class TbLwM2MCreateResponseCallback extends TbLwM2MUplinkTargetedCallback @Override public void onSuccess(CreateRequest request, CreateResponse response) { super.onSuccess(request, response); - handler.onCreateResponseOk(client, versionedId, request); + handler.onCreatebjectInstancesResponseOk(client, versionedId, request); } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java index b7d6e0e7f5..8ac8296c38 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java @@ -19,7 +19,6 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import com.google.gson.reflect.TypeToken; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.Getter; @@ -80,6 +79,7 @@ import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientStateExce import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; import org.thingsboard.server.transport.lwm2m.server.client.ParametersAnalyzeResult; +import org.thingsboard.server.transport.lwm2m.server.client.ResultUpdateResource; import org.thingsboard.server.transport.lwm2m.server.client.ResultsAddKeyValueProto; import org.thingsboard.server.transport.lwm2m.server.common.LwM2MExecutorAwareService; import org.thingsboard.server.transport.lwm2m.server.downlink.DownlinkRequestCallback; @@ -112,11 +112,11 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Random; import java.util.Set; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -319,40 +319,41 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl LwM2mClient lwM2MClient = clientContext.getClientByEndpoint(registration.getEndpoint()); ObjectModel objectModelVersion = lwM2MClient.getObjectModel(path, modelProvider); if (objectModelVersion != null) { + ResultUpdateResource updateResource = new ResultUpdateResource(lwM2MClient); int responseCode = response.getCode().getCode(); if (content instanceof LwM2mObject) { - LwM2mObject lwM2mObject = (LwM2mObject) content; - this.updateObjectResourceValue(lwM2MClient, lwM2mObject, path, responseCode); + this.updateObjectResourceValue(updateResource, (LwM2mObject) content, path, responseCode); } else if (content instanceof LwM2mObjectInstance) { - LwM2mObjectInstance lwM2mObjectInstance = (LwM2mObjectInstance) content; - this.updateObjectInstanceResourceValue(lwM2MClient, lwM2mObjectInstance, path, responseCode); + this.updateObjectInstanceResourceValue(updateResource, (LwM2mObjectInstance) content, path, responseCode); } else if (content instanceof LwM2mResource) { - LwM2mResource lwM2mResource = (LwM2mResource) content; - this.updateResourcesValue(lwM2MClient, lwM2mResource, path, Mode.UPDATE, responseCode); + this.updateResourcesValue(updateResource, (LwM2mResource) content, path, Mode.UPDATE, responseCode); } + this.updateAttrTelemetry(updateResource, null); } tryAwake(lwM2MClient); } } public void onUpdateValueAfterReadCompositeResponse(Registration registration, ReadCompositeResponse response) { - log.trace("ReadCompositeResponse: [{}]", response); + log.trace("ReadCompositeResponse before onUpdateValueAfterReadCompositeResponse: [{}]", response); if (response.getContent() != null) { LwM2mClient lwM2MClient = clientContext.getClientByEndpoint(registration.getEndpoint()); + ResultUpdateResource updateResource = new ResultUpdateResource(lwM2MClient); response.getContent().forEach((k, v) -> { if (v != null) { int responseCode = response.getCode().getCode(); if (v instanceof LwM2mObject) { - this.updateObjectResourceValue(lwM2MClient, (LwM2mObject) v, k.toString(), responseCode); + this.updateObjectResourceValue(updateResource, (LwM2mObject) v, k.toString(), responseCode); } else if (v instanceof LwM2mObjectInstance) { - this.updateObjectInstanceResourceValue(lwM2MClient, (LwM2mObjectInstance) v, k.toString(), responseCode); + this.updateObjectInstanceResourceValue(updateResource, (LwM2mObjectInstance) v, k.toString(), responseCode); } else if (v instanceof LwM2mResource) { - this.updateResourcesValue(lwM2MClient, (LwM2mResource) v, k.toString(), Mode.UPDATE, responseCode); + this.updateResourcesValue(updateResource, (LwM2mResource) v, k.toString(), Mode.UPDATE, responseCode); } } else { this.onErrorObservation(registration, k + ": value in composite response is null"); } }); + this.updateAttrTelemetry(updateResource, null); clientContext.update(lwM2MClient); tryAwake(lwM2MClient); } @@ -380,16 +381,15 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl LwM2mClient lwM2MClient = clientContext.getClientByEndpoint(registration.getEndpoint()); ObjectModel objectModelVersion = lwM2MClient.getObjectModel(path.toString(), modelProvider); if (objectModelVersion != null) { + ResultUpdateResource updateResource = new ResultUpdateResource(lwM2MClient); if (node instanceof LwM2mObject) { - LwM2mObject lwM2mObject = (LwM2mObject) node; - this.updateObjectResourceValue(lwM2MClient, lwM2mObject, path.toString(), 0); + this.updateObjectResourceValue(updateResource, (LwM2mObject) node, path.toString(), 0); } else if (node instanceof LwM2mObjectInstance) { - LwM2mObjectInstance lwM2mObjectInstance = (LwM2mObjectInstance) node; - this.updateObjectInstanceResourceValue(lwM2MClient, lwM2mObjectInstance, path.toString(), 0); + this.updateObjectInstanceResourceValue(updateResource, (LwM2mObjectInstance) node, path.toString(), 0); } else if (node instanceof LwM2mResource) { - LwM2mResource lwM2mResource = (LwM2mResource) node; - this.updateResourcesValueWithTs(lwM2MClient, lwM2mResource, path.toString(), Mode.UPDATE, ts); + this.updateResourcesValue(updateResource, (LwM2mResource) node, path.toString(), Mode.UPDATE, 0); } + this.updateAttrTelemetry(updateResource, ts); } tryAwake(lwM2MClient); } @@ -580,18 +580,18 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl defaultLwM2MDownlinkMsgHandler.sendCancelObserveRequest(client, request, new TbLwM2MCancelObserveCallback(logService, client, versionedId)); } - private void updateObjectResourceValue(LwM2mClient client, LwM2mObject lwM2mObject, String pathIdVer, int code) { + private void updateObjectResourceValue(ResultUpdateResource updateResource, LwM2mObject lwM2mObject, String pathIdVer, int code) { LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathIdVer)); lwM2mObject.getInstances().forEach((instanceId, instance) -> { String pathInstance = pathIds.toString() + "/" + instanceId; - this.updateObjectInstanceResourceValue(client, instance, pathInstance, code); + this.updateObjectInstanceResourceValue(updateResource, instance, pathInstance, code); }); } - private void updateObjectInstanceResourceValue(LwM2mClient client, LwM2mObjectInstance lwM2mObjectInstance, String pathIdVer, int code) { + private void updateObjectInstanceResourceValue(ResultUpdateResource updateResource, LwM2mObjectInstance lwM2mObjectInstance, String pathIdVer, int code) { lwM2mObjectInstance.getResources().forEach((resourceId, resource) -> { String pathRez = pathIdVer + "/" + resourceId; - this.updateResourcesValue(client, resource, pathRez, Mode.UPDATE, code); + this.updateResourcesValue(updateResource, resource, pathRez, Mode.UPDATE, code); }); } @@ -601,56 +601,50 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl * #2 Update new Resources (replace old Resource Value on new Resource Value) * #3 If fr_update -> UpdateFirmware * #4 updateAttrTelemetry - * @param lwM2MClient - Registration LwM2M Client + * @param updateResource - result update resource by LwM2M Client * @param lwM2mResource - LwM2mSingleResource response.getContent() * @param stringPath - resource * @param mode - Replace, Update */ - private void updateResourcesValue(LwM2mClient lwM2MClient, LwM2mResource lwM2mResource, String stringPath, Mode mode, int code) { - Registration registration = lwM2MClient.getRegistration(); + private void updateResourcesValue(ResultUpdateResource updateResource, LwM2mResource lwM2mResource, String stringPath, Mode mode, int code) { + LwM2mClient lwM2MClient = updateResource.getLwM2MClient(); String path = convertObjectIdToVersionedId(stringPath, lwM2MClient); - if (lwM2MClient.saveResourceValue(path, lwM2mResource, modelProvider, mode)) { - if (path.equals(convertObjectIdToVersionedId(FW_NAME_ID, lwM2MClient))) { - otaService.onCurrentFirmwareNameUpdate(lwM2MClient, (String) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(FW_3_VER_ID, lwM2MClient))) { - otaService.onCurrentFirmwareVersion3Update(lwM2MClient, (String) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(FW_VER_ID, lwM2MClient))) { - otaService.onCurrentFirmwareVersionUpdate(lwM2MClient, (String) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(FW_STATE_ID, lwM2MClient))) { - otaService.onCurrentFirmwareStateUpdate(lwM2MClient, (Long) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(FW_RESULT_ID, lwM2MClient))) { - otaService.onCurrentFirmwareResultUpdate(lwM2MClient, (Long) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(FW_DELIVERY_METHOD, lwM2MClient))) { - otaService.onCurrentFirmwareDeliveryMethodUpdate(lwM2MClient, (Long) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(SW_NAME_ID, lwM2MClient))) { - otaService.onCurrentSoftwareNameUpdate(lwM2MClient, (String) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(SW_VER_ID, lwM2MClient))) { - otaService.onCurrentSoftwareVersionUpdate(lwM2MClient, (String) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(SW_3_VER_ID, lwM2MClient))) { - otaService.onCurrentSoftwareVersion3Update(lwM2MClient, (String) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(SW_STATE_ID, lwM2MClient))) { - otaService.onCurrentSoftwareStateUpdate(lwM2MClient, (Long) lwM2mResource.getValue()); - } else if (path.equals(convertObjectIdToVersionedId(SW_RESULT_ID, lwM2MClient))) { - otaService.onCurrentSoftwareResultUpdate(lwM2MClient, (Long) lwM2mResource.getValue()); - } + if (path != null && lwM2MClient.saveResourceValue(path, lwM2mResource, modelProvider, mode)) { + this.updateOtaResource(lwM2MClient, lwM2mResource, path); if (ResponseCode.BAD_REQUEST.getCode() > code) { - this.updateAttrTelemetry(registration, path, null); + updateResource.getPaths().add(path); } } else { log.error("Fail update path [{}] Resource [{}]", path, lwM2mResource); } } - private void updateResourcesValueWithTs(LwM2mClient lwM2MClient, LwM2mResource lwM2mResource, String stringPath, Mode mode, Instant ts) { - Registration registration = lwM2MClient.getRegistration(); - String path = convertObjectIdToVersionedId(stringPath, lwM2MClient); - if (lwM2MClient.saveResourceValue(path, lwM2mResource, modelProvider, mode)) { - this.updateAttrTelemetry(registration, path, ts); - } else { - log.error("Fail update path [{}] Resource [{}] with ts.", path, lwM2mResource); + + private void updateOtaResource(LwM2mClient lwM2MClient, LwM2mResource lwM2mResource, String path) { + if (path.equals(convertObjectIdToVersionedId(FW_NAME_ID, lwM2MClient))) { + otaService.onCurrentFirmwareNameUpdate(lwM2MClient, (String) lwM2mResource.getValue()); + } else if (path.equals(convertObjectIdToVersionedId(FW_3_VER_ID, lwM2MClient))) { + otaService.onCurrentFirmwareVersion3Update(lwM2MClient, (String) lwM2mResource.getValue()); + } else if (path.equals(convertObjectIdToVersionedId(FW_VER_ID, lwM2MClient))) { + otaService.onCurrentFirmwareVersionUpdate(lwM2MClient, (String) lwM2mResource.getValue()); + } else if (path.equals(convertObjectIdToVersionedId(FW_STATE_ID, lwM2MClient))) { + otaService.onCurrentFirmwareStateUpdate(lwM2MClient, (Long) lwM2mResource.getValue()); + } else if (path.equals(convertObjectIdToVersionedId(FW_RESULT_ID, lwM2MClient))) { + otaService.onCurrentFirmwareResultUpdate(lwM2MClient, (Long) lwM2mResource.getValue()); + } else if (path.equals(convertObjectIdToVersionedId(FW_DELIVERY_METHOD, lwM2MClient))) { + otaService.onCurrentFirmwareDeliveryMethodUpdate(lwM2MClient, (Long) lwM2mResource.getValue()); + } else if (path.equals(convertObjectIdToVersionedId(SW_NAME_ID, lwM2MClient))) { + otaService.onCurrentSoftwareNameUpdate(lwM2MClient, (String) lwM2mResource.getValue()); + } else if (path.equals(convertObjectIdToVersionedId(SW_VER_ID, lwM2MClient))) { + otaService.onCurrentSoftwareVersionUpdate(lwM2MClient, (String) lwM2mResource.getValue()); + } else if (path.equals(convertObjectIdToVersionedId(SW_3_VER_ID, lwM2MClient))) { + otaService.onCurrentSoftwareVersion3Update(lwM2MClient, (String) lwM2mResource.getValue()); + } else if (path.equals(convertObjectIdToVersionedId(SW_STATE_ID, lwM2MClient))) { + otaService.onCurrentSoftwareStateUpdate(lwM2MClient, (Long) lwM2mResource.getValue()); + } else if (path.equals(convertObjectIdToVersionedId(SW_RESULT_ID, lwM2MClient))) { + otaService.onCurrentSoftwareResultUpdate(lwM2MClient, (Long) lwM2mResource.getValue()); } } - /** * send Attribute and Telemetry to Thingsboard * #1 - get AttrName/TelemetryName with value from LwM2MClient: @@ -658,20 +652,20 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl * -- AttrName/TelemetryName == resourceName from ModelObject.objectModel, value from ModelObject.instance.resource(resourceId) * #2 - set Attribute/Telemetry * - * @param registration - Registration LwM2M Client + * @param updateResource - updateResource resource of LwM2M Client */ - public void updateAttrTelemetry(Registration registration, String path, Instant ts) { - log.trace("UpdateAttrTelemetry paths [{}]", path); + public void updateAttrTelemetry(ResultUpdateResource updateResource, Instant ts) { + log.trace("UpdateAttrTelemetry paths [{}]", updateResource.getPaths()); try { - ResultsAddKeyValueProto results = this.getParametersFromProfile(registration, path); - SessionInfoProto sessionInfo = this.getSessionInfoOrCloseSession(registration); + ResultsAddKeyValueProto results = this.getParametersFromProfile(updateResource); + SessionInfoProto sessionInfo = this.getSessionInfoOrCloseSession(updateResource.getLwM2MClient().getRegistration()); if (results != null && sessionInfo != null) { if (results.getResultAttributes().size() > 0) { - log.trace("UpdateAttribute paths [{}] value [{}]", path, results.getResultAttributes().get(0).toString()); + log.trace("UpdateAttribute paths [{}] value [{}]", updateResource.getPaths(), results.getResultAttributes().get(0).toString()); this.helper.sendParametersOnThingsboardAttribute(results.getResultAttributes(), sessionInfo); } if (results.getResultTelemetries().size() > 0) { - log.trace("UpdateTelemetry paths [{}] value [{}] ts [{}]", path, results.getResultTelemetries().get(0).toString(), ts == null ? "null" : ts.toEpochMilli()); + log.trace("UpdateTelemetry paths [{}] value [{}] ts [{}]", updateResource.getPaths(), results.getResultTelemetries().get(0).toString(), ts == null ? "null" : ts.toEpochMilli()); this.helper.sendParametersOnThingsboardTelemetry(results.getResultTelemetries(), sessionInfo, null, ts); } } @@ -695,13 +689,6 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl return false; } - private ConcurrentHashMap getPathForWriteAttributes(JsonObject objectJson) { - ConcurrentHashMap pathAttributes = new Gson().fromJson(objectJson.toString(), - new TypeToken>() { - }.getType()); - return pathAttributes; - } - private void onDeviceUpdate(LwM2mClient lwM2MClient, Device device, Optional deviceProfileOpt) { var oldProfile = clientContext.getProfile(lwM2MClient.getProfileId()); deviceProfileOpt.ifPresent(deviceProfile -> this.onDeviceProfileUpdate(Collections.singletonList(lwM2MClient), oldProfile, deviceProfile)); @@ -712,31 +699,68 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl * // * @param attributes - new JsonObject * // * @param telemetry - new JsonObject * - * @param registration - Registration LwM2M Client - * @param path - + * @param updateResource - updateResource resource of LwM2M Client */ - private ResultsAddKeyValueProto getParametersFromProfile(Registration registration, String path) { + private ResultsAddKeyValueProto getParametersFromProfile(ResultUpdateResource updateResource) { + Registration registration = updateResource.getLwM2MClient().getRegistration(); + Set paths = updateResource.getPaths(); + ResultsAddKeyValueProto results = new ResultsAddKeyValueProto(); + var profile = clientContext.getProfile(registration); + List resultAttributes = new ArrayList<>(); + Set attributes = profile.getObserveAttr().getAttribute().stream() + .filter(paths::contains) + .collect(Collectors.toSet()); + if (!attributes.isEmpty()){ + attributes.stream() + .map(attr -> this.getKvToThingsBoard(attr, registration)) + .filter(Objects::nonNull) + .forEach(resultAttributes::add); + } + List resultTelemetries = new ArrayList<>(); + Set telemetries = profile.getObserveAttr().getTelemetry().stream() + .filter(paths::contains) + .collect(Collectors.toSet()); + if (!telemetries.isEmpty()){ + telemetries.stream() + .map(telemetry -> this.getKvToThingsBoard(telemetry, registration)) + .filter(Objects::nonNull) + .forEach(resultTelemetries::add); + } + if (resultAttributes.size() > 0) { + results.setResultAttributes(resultAttributes); + } + if (resultTelemetries.size() > 0) { + results.setResultTelemetries(resultTelemetries); + } + return results; + } + + private ResultsAddKeyValueProto getParametersFromProfile(Registration registration, Set path) { if (!path.isEmpty()) { ResultsAddKeyValueProto results = new ResultsAddKeyValueProto(); var profile = clientContext.getProfile(registration); List resultAttributes = new ArrayList<>(); - profile.getObserveAttr().getAttribute().forEach(pathIdVer -> { - if (path.equals(pathIdVer)) { - TransportProtos.KeyValueProto kvAttr = this.getKvToThingsBoard(pathIdVer, registration); - if (kvAttr != null) { - resultAttributes.add(kvAttr); - } - } - }); + Set attributes = profile.getObserveAttr().getAttribute().stream() + .map(LwM2MTransportUtil::fromVersionedIdToObjectId) + .filter(path::contains) + .collect(Collectors.toSet()); + if (!attributes.isEmpty()){ + attributes.stream() + .map(attr -> this.getKvToThingsBoard(attr, registration)) + .filter(Objects::nonNull) + .forEach(resultAttributes::add); + } List resultTelemetries = new ArrayList<>(); - profile.getObserveAttr().getTelemetry().forEach(pathIdVer -> { - if (path.contains(pathIdVer)) { - TransportProtos.KeyValueProto kvAttr = this.getKvToThingsBoard(pathIdVer, registration); - if (kvAttr != null) { - resultTelemetries.add(kvAttr); - } - } - }); + Set telemetries = profile.getObserveAttr().getTelemetry().stream() + .map(LwM2MTransportUtil::fromVersionedIdToObjectId) + .filter(path::contains) + .collect(Collectors.toSet()); + if (!telemetries.isEmpty()){ + telemetries.stream() + .map(telemetry -> this.getKvToThingsBoard(telemetry, registration)) + .filter(Objects::nonNull) + .forEach(resultTelemetries::add); + } if (resultAttributes.size() > 0) { results.setResultAttributes(resultAttributes); } @@ -792,41 +816,49 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl } @Override - public void onWriteResponseOk(LwM2mClient client, String path, WriteRequest request, int code) { + public void onWriteResponseOk(LwM2mClient lwM2MClient, String path, WriteRequest request, int code) { + ResultUpdateResource updateResource = new ResultUpdateResource(lwM2MClient); if (request.getNode() instanceof LwM2mResource) { - this.updateResourcesValue(client, ((LwM2mResource) request.getNode()), path, request.isReplaceRequest() ? Mode.REPLACE : Mode.UPDATE, code); + this.updateResourcesValue(updateResource, ((LwM2mResource) request.getNode()), path, request.isReplaceRequest() ? Mode.REPLACE : Mode.UPDATE, code); } else if (request.getNode() instanceof LwM2mObjectInstance) { ((LwM2mObjectInstance) request.getNode()).getResources().forEach((resId, resource) -> { - this.updateResourcesValue(client, resource, path + "/" + resId, request.isReplaceRequest() ? Mode.REPLACE : Mode.UPDATE, code); + this.updateResourcesValue(updateResource, resource, path + "/" + resId, request.isReplaceRequest() ? Mode.REPLACE : Mode.UPDATE, code); }); } if (request.getNode() instanceof LwM2mResource || request.getNode() instanceof LwM2mObjectInstance) { - clientContext.update(client); + clientContext.update(lwM2MClient); } + this.updateAttrTelemetry(updateResource, null); } @Override - public void onCreateResponseOk(LwM2mClient client, String path, CreateRequest request) { - if (request.getObjectInstances() != null && request.getObjectInstances().size() > 0) { + public void onCreatebjectInstancesResponseOk(LwM2mClient lwM2MClient, String versionId, CreateRequest request) { + if (request.getObjectInstances() != null && !request.getObjectInstances().isEmpty()) { + ResultUpdateResource updateResource = new ResultUpdateResource(lwM2MClient); request.getObjectInstances().forEach(instance -> - instance.getResources() + instance.getResources().forEach((resId, lwM2mResource) ->{ + this.updateResourcesValue(updateResource, lwM2mResource, versionId + "/" + resId, Mode.REPLACE, 0); + }) ); - clientContext.update(client); + clientContext.update(lwM2MClient); + this.updateAttrTelemetry(updateResource, null); } } @Override - public void onWriteCompositeResponseOk(LwM2mClient client, WriteCompositeRequest request, int code) { + public void onWriteCompositeResponseOk(LwM2mClient lwM2MClient, WriteCompositeRequest request, int code) { log.trace("ReadCompositeResponse: [{}]", request.getNodes()); + ResultUpdateResource updateResource = new ResultUpdateResource(lwM2MClient); request.getNodes().forEach((k, v) -> { if (v instanceof LwM2mSingleResource) { - this.updateResourcesValue(client, (LwM2mResource) v, k.toString(), Mode.REPLACE, code); + this.updateResourcesValue(updateResource, (LwM2mResource) v, k.toString(), Mode.REPLACE, code); } else { LwM2mResourceInstance resourceInstance = (LwM2mResourceInstance) v; LwM2mMultipleResource multipleResource = new LwM2mMultipleResource(((LwM2mResourceInstance) v).getId(), resourceInstance.getType(), resourceInstance); - this.updateResourcesValue(client, multipleResource, k.toString(), Mode.REPLACE, code); + this.updateResourcesValue(updateResource, multipleResource, k.toString(), Mode.REPLACE, code); } }); + this.updateAttrTelemetry(updateResource, null); } //TODO: review and optimize the logic to minimize number of the requests to device. diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java index 64154fc7a4..0c28372bce 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java @@ -48,6 +48,7 @@ public interface LwM2mUplinkMsgHandler { void onUpdateValueAfterReadResponse(Registration registration, String path, ReadResponse response); void onUpdateValueAfterReadCompositeResponse(Registration registration, ReadCompositeResponse response); + void onErrorObservation(Registration registration, String errorMsg); void onUpdateValueWithSendRequest(Registration registration, TimestampedLwM2mNodes data); @@ -66,7 +67,7 @@ public interface LwM2mUplinkMsgHandler { void onWriteResponseOk(LwM2mClient client, String path, WriteRequest request, int code); - void onCreateResponseOk(LwM2mClient client, String path, CreateRequest request); + void onCreatebjectInstancesResponseOk(LwM2mClient client, String path, CreateRequest request); void onWriteCompositeResponseOk(LwM2mClient client, WriteCompositeRequest request, int code); From dd76f2d8231cc754e9254bce68b63d13e17a2674 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 7 May 2025 11:44:54 +0300 Subject: [PATCH 39/63] UI: Fixed rule node config style --- .../gps-geo-action-config.component.html | 2 +- .../math-function-config.component.html | 2 +- ...save-to-custom-table-config.component.html | 2 +- .../arguments-map-config.component.html | 2 +- ...vice-relations-query-config.component.html | 2 +- .../message-types-config.component.html | 2 +- .../common/select-attributes.component.html | 2 +- .../entity-details-config.component.html | 2 +- .../external/lambda-config.component.html | 4 +- .../check-message-config.component.html | 4 +- .../originator-type-config.component.html | 2 +- .../copy-keys-config.component.html | 2 +- .../deduplication-config.component.html | 6 +-- .../delete-keys-config.component.html | 2 +- .../rulechain/rule-node-config.component.html | 2 +- .../rulechain/rule-node-config.component.scss | 53 ++++++++++++++++++- .../rulechain/rule-node-config.component.ts | 10 ++-- 17 files changed, 77 insertions(+), 24 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/gps-geo-action-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/gps-geo-action-config.component.html index 37e93157d3..2d3f230643 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/gps-geo-action-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/gps-geo-action-config.component.html @@ -116,7 +116,7 @@ rule-node-config.polygon-definition - help diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.html index 3147162f06..4d4940404e 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/math-function-config.component.html @@ -77,7 +77,7 @@ rule-node-config.key-field-input - help diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/save-to-custom-table-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/save-to-custom-table-config.component.html index c1d6dd3215..c9685f2c3f 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/save-to-custom-table-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/save-to-custom-table-config.component.html @@ -19,7 +19,7 @@ rule-node-config.custom-table-name - rule-node-config.argument-key-field-input - help diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/device-relations-query-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/device-relations-query-config.component.html index ce99d6f7e6..0da5cae858 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/device-relations-query-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/device-relations-query-config.component.html @@ -60,7 +60,7 @@ [emptyInputPlaceholder]="'rule-node-config.add-device-profile' | translate" [filledInputPlaceholder]="'rule-node-config.add-device-profile' | translate" formControlName="deviceTypes"> - help
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/message-types-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/message-types-config.component.html index ab436088bd..dcc968dee3 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/message-types-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/message-types-config.component.html @@ -62,7 +62,7 @@ - help diff --git a/ui-ngx/src/app/modules/home/components/rule-node/common/select-attributes.component.html b/ui-ngx/src/app/modules/home/components/rule-node/common/select-attributes.component.html index 88c4cc1bdb..49669c147f 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/common/select-attributes.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/common/select-attributes.component.html @@ -53,7 +53,7 @@ - help diff --git a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/entity-details-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/entity-details-config.component.html index 11f0323346..5d80cfb276 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/enrichment/entity-details-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/enrichment/entity-details-config.component.html @@ -22,7 +22,7 @@ [placeholder]="'rule-node-config.add-detail' | translate" [requiredText]="'rule-node-config.entity-details-list-empty' | translate" formControlName="detailsList"> - help diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/lambda-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/lambda-config.component.html index 67f0aaca7f..7de1a46a9d 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/lambda-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/lambda-config.component.html @@ -85,7 +85,7 @@ {{ 'rule-node-config.connection-timeout-min' | translate }} - help @@ -98,7 +98,7 @@ {{ 'rule-node-config.request-timeout-min' | translate }} - help diff --git a/ui-ngx/src/app/modules/home/components/rule-node/filter/check-message-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/filter/check-message-config.component.html index 7dd89d4927..0bdba52c1e 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/filter/check-message-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/filter/check-message-config.component.html @@ -26,14 +26,14 @@ [label]="'rule-node-config.data-keys' | translate" [placeholder]="'rule-node-config.add-message-field' | translate" formControlName="messageNames"> - help - help
- help diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/copy-keys-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/transformation/copy-keys-config.component.html index b1e810bcb2..3d524b8402 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/copy-keys-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/copy-keys-config.component.html @@ -27,7 +27,7 @@ [placeholder]="'rule-node-config.add-key' | translate" [requiredText]="'rule-node-config.key-val.at-least-one-key-error' | translate" formControlName="keys"> - help diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.html index 3df9c1d7d3..aa6ff0bab7 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/deduplication-config.component.html @@ -25,7 +25,7 @@ {{'rule-node-config.interval-min-error' | translate}} - help @@ -73,7 +73,7 @@ {{'rule-node-config.max-pending-msgs-min-error' | translate}} - help @@ -89,7 +89,7 @@ {{'rule-node-config.max-retries-min-error' | translate}} - help diff --git a/ui-ngx/src/app/modules/home/components/rule-node/transformation/delete-keys-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/transformation/delete-keys-config.component.html index 75f9c243e9..4385341686 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/transformation/delete-keys-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/transformation/delete-keys-config.component.html @@ -26,7 +26,7 @@ [placeholder]="'rule-node-config.add-key' | translate" [requiredText]="'rule-node-config.key-val.at-least-one-key-error' | translate" formControlName="keys"> - help diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html index 3522c584f4..0228b8d973 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
{{definedDirectiveError}}
* { + flex: 1; + } + + .flex-2 { + flex: 2; + } + + .third-width { + max-width: 32%; + @media #{$mat-xs} { + max-width: 100%; + } + } + } } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts index e3e5d939df..d27e4d8abb 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-config.component.ts @@ -19,11 +19,13 @@ import { ComponentRef, EventEmitter, forwardRef, + HostBinding, Input, OnDestroy, Output, ViewChild, - ViewContainerRef + ViewContainerRef, + ViewEncapsulation } from '@angular/core'; import { ControlValueAccessor, @@ -52,14 +54,16 @@ import { RuleChainType } from '@shared/models/rule-chain.models'; provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RuleNodeConfigComponent), multi: true - }] + }], + encapsulation: ViewEncapsulation.None }) export class RuleNodeConfigComponent implements ControlValueAccessor, OnDestroy { @ViewChild('definedConfigContent', {read: ViewContainerRef, static: true}) definedConfigContainer: ViewContainerRef; - @ViewChild('jsonObjectEditComponent') jsonObjectEditComponent: JsonObjectEditComponent; + @HostBinding('style.display') readonly styleDisplay = 'block'; + private requiredValue: boolean; get required(): boolean { return this.requiredValue; From 381e976d87c252722e488dfd19e0416721823794 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 7 May 2025 12:17:24 +0300 Subject: [PATCH 40/63] Refactoring for input node relations --- .../update/DefaultDataUpdateService.java | 69 ++++----- .../server/dao/rule/BaseRuleChainService.java | 39 +++-- .../dao/service/RuleChainServiceTest.java | 134 ++++++++++-------- 3 files changed, 117 insertions(+), 125 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index 4c90258330..d8ddd7cd9f 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -24,21 +24,18 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageDataIterable; -import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.query.DynamicValue; import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rule.RuleChainService; -import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.RuleNodeClassInfo; import org.thingsboard.server.service.install.DbUpgradeExecutorService; @@ -46,6 +43,7 @@ import org.thingsboard.server.utils.TbNodeUpgradeUtils; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -65,9 +63,6 @@ public class DefaultDataUpdateService implements DataUpdateService { @Autowired private RelationService relationService; - @Autowired - private TenantService tenantService; - @Autowired private ComponentDiscoveryService componentDiscoveryService; @@ -78,52 +73,36 @@ public class DefaultDataUpdateService implements DataUpdateService { public void updateData() throws Exception { log.info("Updating data ..."); //TODO: should be cleaned after each release - inputNodesUpdater.updateEntities(); + updateInputNodes(); log.info("Data updated."); } - //TODO: should be removed after release - private final PaginatedUpdater inputNodesUpdater = new PaginatedUpdater<>() { - @Override - protected String getName() { - return "Input nodes updater"; - } - - @Override - protected PageData findEntities(String type, PageLink pageLink) { - return tenantService.findTenants(pageLink); - } - - @Override - protected void updateEntity(Tenant tenant) { - TenantId tenantId = tenant.getId(); + private void updateInputNodes() { + log.info("Creating relations for input nodes..."); + int n = 0; + var inputNodes = new PageDataIterable<>(pageLink -> ruleChainService.findAllRuleNodesByType(TB_RULE_CHAIN_INPUT_NODE, pageLink), 1024); + for (RuleNode inputNode : inputNodes) { try { - var inputNodes = ruleChainService.findRuleNodesByTenantIdAndType(tenantId, TB_RULE_CHAIN_INPUT_NODE); - var resultFutures = inputNodes.stream().map(ruleNode -> { - try { - JsonNode id = ruleNode.getConfiguration().get("ruleChainId"); - if (id != null) { - RuleChainId toRuleChainId = new RuleChainId(UUID.fromString(id.asText())); - RuleChainId fromRuleChainId = ruleNode.getRuleChainId(); - EntityRelation relation = new EntityRelation(); - relation.setFrom(fromRuleChainId); - relation.setTo(toRuleChainId); - relation.setType(EntityRelation.USES_TYPE); - relation.setTypeGroup(RelationTypeGroup.COMMON); - return relationService.saveRelationAsync(tenantId, relation); - } - } catch (Exception e) { - log.error("[{}] Failed to save relation for input node: [{}]", tenantId, ruleNode, e); - } - return Futures.immediateFuture(null); - }).toList(); + RuleChainId targetRuleChainId = Optional.ofNullable(inputNode.getConfiguration().get("ruleChainId")) + .filter(JsonNode::isTextual).map(JsonNode::asText).map(id -> new RuleChainId(UUID.fromString(id))) + .orElse(null); + if (targetRuleChainId == null) { + continue; + } - Futures.allAsList(resultFutures).get(); + EntityRelation relation = new EntityRelation(); + relation.setFrom(inputNode.getRuleChainId()); + relation.setTo(targetRuleChainId); + relation.setType(EntityRelation.USES_TYPE); + relation.setTypeGroup(RelationTypeGroup.COMMON); + relationService.saveRelation(TenantId.SYS_TENANT_ID, relation); + n++; } catch (Exception e) { - log.error("[{}] Unable to update Tenant input nodes", tenantId, e); + log.error("Failed to save relation for input node: {}", inputNode, e); } } - }; + log.info("Created {} relations for input nodes", n); + } @Override public void upgradeRuleNodes() { diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 1ce9bd555b..8538bd9492 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -211,9 +211,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC for (RuleNode existingNode : existingRuleNodes) { relationService.deleteEntityRelations(tenantId, existingNode.getId()); if (existingNode.getType().equals(TB_RULE_CHAIN_INPUT_NODE)) { - if (existingNode.getConfiguration().has("ruleChainId")) { - RuleChainId targetRuleChainId = extractRuleChainIdFromInputNode(existingNode); - var relation = createRuleChainInputRelation(ruleChainId, targetRuleChainId); + EntityRelation relation = getRuleChainInputRelation(ruleChainId, existingNode); + if (relation != null) { relationService.deleteRelation(tenantId, relation); } } @@ -242,9 +241,8 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC relations.add(new EntityRelation(ruleChainMetaData.getRuleChainId(), savedNode.getId(), EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); if (node.getType().equals(TB_RULE_CHAIN_INPUT_NODE)) { - if (node.getConfiguration().has("ruleChainId")) { - RuleChainId targetRuleChainId = extractRuleChainIdFromInputNode(node); - var relation = createRuleChainInputRelation(ruleChainId, targetRuleChainId); + EntityRelation relation = getRuleChainInputRelation(ruleChainId, node); + if (relation != null) { relations.add(relation); } } @@ -262,7 +260,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC firstRuleNodeId = nodes.get(ruleChainMetaData.getFirstNodeIndex()).getId(); } if ((ruleChain.getFirstRuleNodeId() != null && !ruleChain.getFirstRuleNodeId().equals(firstRuleNodeId)) - || (ruleChain.getFirstRuleNodeId() == null && firstRuleNodeId != null)) { + || (ruleChain.getFirstRuleNodeId() == null && firstRuleNodeId != null)) { ruleChain.setFirstRuleNodeId(firstRuleNodeId); } if (ruleChainMetaData.getConnections() != null) { @@ -317,19 +315,20 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC return RuleChainUpdateResult.successful(updatedRuleNodes); } - private EntityRelation createRuleChainInputRelation(RuleChainId ruleChainId, RuleChainId targetRuleChainId) { - EntityRelation relation = new EntityRelation(); - relation.setFrom(ruleChainId); - relation.setTo(targetRuleChainId); - relation.setType(EntityRelation.USES_TYPE); - relation.setTypeGroup(RelationTypeGroup.COMMON); - return relation; - } - - private RuleChainId extractRuleChainIdFromInputNode(RuleNode node) { - JsonNode configuration = node.getConfiguration(); - UUID targetUuid = UUID.fromString(configuration.get("ruleChainId").asText()); - return new RuleChainId(targetUuid); + private EntityRelation getRuleChainInputRelation(RuleChainId ruleChainId, RuleNode inputNode) { + RuleChainId targetRuleChainId = Optional.ofNullable(inputNode.getConfiguration().get("ruleChainId")) + .filter(JsonNode::isTextual).map(JsonNode::asText).map(id -> new RuleChainId(UUID.fromString(id))) + .orElse(null); + if (targetRuleChainId != null) { + EntityRelation relation = new EntityRelation(); + relation.setFrom(ruleChainId); + relation.setTo(targetRuleChainId); + relation.setType(EntityRelation.USES_TYPE); + relation.setTypeGroup(RelationTypeGroup.COMMON); + return relation; + } else { + return null; + } } @Override diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java index a2ca663619..11fa38dc19 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.dao.service; import com.datastax.oss.driver.api.core.uuid.Uuids; -import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Assert; import org.junit.Test; import org.junit.jupiter.api.Assertions; @@ -46,6 +45,7 @@ import java.util.List; import java.util.UUID; import java.util.function.Function; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.thingsboard.server.common.data.relation.EntityRelation.USES_TYPE; import static org.thingsboard.server.dao.rule.BaseRuleChainService.TB_RULE_CHAIN_INPUT_NODE; @@ -277,7 +277,7 @@ public class RuleChainServiceTest extends AbstractServiceTest { List ruleNodes = savedRuleChainMetaData.getNodes(); int name3Index = -1; - for (int i=0;i ruleNodes = new ArrayList<>(); - ruleNodes.add(ruleNode); + ruleNodes.add(toRuleChain1Node); + ruleNodes.add(toRuleChain1Node2); ruleChainMetaData.setFirstNodeIndex(0); ruleChainMetaData.setNodes(ruleNodes); ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData, Function.identity()); - List relations = relationService.findByFromAndType(tenantId, savedFromRuleChain.getId(), USES_TYPE, RelationTypeGroup.COMMON); - Assert.assertEquals(1, relations.size()); - EntityRelation usesRelation = relations.get(0); - Assert.assertEquals(savedFromRuleChain.getId(), usesRelation.getFrom()); - Assert.assertEquals(savedToRuleChain.getId(), usesRelation.getTo()); + List relations = relationService.findByFromAndType(tenantId, fromRuleChainId, USES_TYPE, RelationTypeGroup.COMMON); + assertThat(relations).singleElement().satisfies(relationToRuleChain1 -> { + assertThat(relationToRuleChain1.getFrom()).isEqualTo(fromRuleChainId); + assertThat(relationToRuleChain1.getTo()).isEqualTo(toRuleChain1Id); + }); - RuleChain newToRuleChain = new RuleChain(); - newToRuleChain.setName("New To Rule Chain"); - newToRuleChain.setTenantId(tenantId); - RuleChain savedNewToRuleChain = ruleChainService.saveRuleChain(newToRuleChain); + RuleChain toRuleChain2 = new RuleChain(); + toRuleChain2.setName("To Rule Chain 2"); + toRuleChain2.setTenantId(tenantId); + toRuleChain2 = ruleChainService.saveRuleChain(toRuleChain2); + RuleChainId toRuleChain2Id = toRuleChain2.getId(); - RuleNode newRuleNode = new RuleNode(); - newRuleNode.setName("Input node"); - newRuleNode.setType(TB_RULE_CHAIN_INPUT_NODE); - ObjectNode newConfiguration = JacksonUtil.newObjectNode(); - configuration.put("ruleChainId", savedNewToRuleChain.getId().toString()); - newRuleNode.setConfiguration(newConfiguration); + RuleNode toRuleChain2Node = new RuleNode(); + toRuleChain2Node.setName("To Rule Chain 2"); + toRuleChain2Node.setType(TB_RULE_CHAIN_INPUT_NODE); + toRuleChain2Node.setConfiguration(JacksonUtil.newObjectNode() + .put("ruleChainId", toRuleChain2Id.toString())); List newRuleNodes = new ArrayList<>(); - newRuleNodes.add(newRuleNode); - RuleChainMetaData foundRuleChainMetaData = ruleChainService.loadRuleChainMetaData(tenantId, ruleChainMetaData.getRuleChainId()); - foundRuleChainMetaData.setNodes(newRuleNodes); + newRuleNodes.add(toRuleChain2Node); + newRuleNodes.add(toRuleChain1Node); + ruleChainMetaData = ruleChainService.loadRuleChainMetaData(tenantId, ruleChainMetaData.getRuleChainId()); + ruleChainMetaData.setNodes(newRuleNodes); ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData, Function.identity()); - List newRelations = relationService.findByFromAndType(tenantId, savedFromRuleChain.getId(), USES_TYPE, RelationTypeGroup.COMMON); - Assert.assertEquals(1, relations.size()); - EntityRelation newUsesRelation = newRelations.get(0); - Assert.assertEquals(savedFromRuleChain.getId(), newUsesRelation.getFrom()); - Assert.assertEquals(savedNewToRuleChain.getId(), newUsesRelation.getTo()); + List newRelations = relationService.findByFromAndType(tenantId, fromRuleChainId, USES_TYPE, RelationTypeGroup.COMMON); + assertThat(newRelations).hasSize(2); + assertThat(newRelations).anySatisfy(relationToRuleChain1 -> { + assertThat(relationToRuleChain1.getFrom()).isEqualTo(fromRuleChainId); + assertThat(relationToRuleChain1.getTo()).isEqualTo(toRuleChain1Id); + }); + assertThat(newRelations).anySatisfy(relationToRuleChain2 -> { + assertThat(relationToRuleChain2.getFrom()).isEqualTo(fromRuleChainId); + assertThat(relationToRuleChain2.getTo()).isEqualTo(toRuleChain2Id); + }); } private RuleChainId saveRuleChainAndSetAutoAssignToEdge(String name) { @@ -462,9 +476,9 @@ public class RuleChainServiceTest extends AbstractServiceTest { ruleChainMetaData.setFirstNodeIndex(0); ruleChainMetaData.setNodes(ruleNodes); - ruleChainMetaData.addConnectionInfo(0,1,"success"); - ruleChainMetaData.addConnectionInfo(0,2,"fail"); - ruleChainMetaData.addConnectionInfo(1,2,"success"); + ruleChainMetaData.addConnectionInfo(0, 1, "success"); + ruleChainMetaData.addConnectionInfo(0, 2, "fail"); + ruleChainMetaData.addConnectionInfo(1, 2, "success"); Assert.assertTrue(ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData, Function.identity()).isSuccess()); return ruleChainService.loadRuleChainMetaData(tenantId, ruleChainMetaData.getRuleChainId()); @@ -501,10 +515,10 @@ public class RuleChainServiceTest extends AbstractServiceTest { ruleChainMetaData.setFirstNodeIndex(0); ruleChainMetaData.setNodes(ruleNodes); - ruleChainMetaData.addConnectionInfo(0,1,"success"); - ruleChainMetaData.addConnectionInfo(0,2,"fail"); - ruleChainMetaData.addConnectionInfo(1,2,"success"); - ruleChainMetaData.addConnectionInfo(2,2,"success"); + ruleChainMetaData.addConnectionInfo(0, 1, "success"); + ruleChainMetaData.addConnectionInfo(0, 2, "fail"); + ruleChainMetaData.addConnectionInfo(1, 2, "success"); + ruleChainMetaData.addConnectionInfo(2, 2, "success"); return ruleChainMetaData; } @@ -540,10 +554,10 @@ public class RuleChainServiceTest extends AbstractServiceTest { ruleChainMetaData.setFirstNodeIndex(0); ruleChainMetaData.setNodes(ruleNodes); - ruleChainMetaData.addConnectionInfo(0,1,"success"); - ruleChainMetaData.addConnectionInfo(0,2,"fail"); - ruleChainMetaData.addConnectionInfo(1,2,"success"); - ruleChainMetaData.addConnectionInfo(2,0,"success"); + ruleChainMetaData.addConnectionInfo(0, 1, "success"); + ruleChainMetaData.addConnectionInfo(0, 2, "fail"); + ruleChainMetaData.addConnectionInfo(1, 2, "success"); + ruleChainMetaData.addConnectionInfo(2, 0, "success"); return ruleChainMetaData; } @@ -649,16 +663,16 @@ public class RuleChainServiceTest extends AbstractServiceTest { private RuleChain getRuleChain() { String ruleChainStr = "{\n" + - " \"name\": \"Root Rule Chain\",\n" + - " \"type\": \"CORE\",\n" + - " \"firstRuleNodeId\": {\n" + - " \"entityType\": \"RULE_NODE\",\n" + - " \"id\": \"91ad0b00-e779-11ee-9cf0-15d8b6079fdb\"\n" + - " },\n" + - " \"debugMode\": false,\n" + - " \"configuration\": null,\n" + - " \"additionalInfo\": null\n" + - "}"; + " \"name\": \"Root Rule Chain\",\n" + + " \"type\": \"CORE\",\n" + + " \"firstRuleNodeId\": {\n" + + " \"entityType\": \"RULE_NODE\",\n" + + " \"id\": \"91ad0b00-e779-11ee-9cf0-15d8b6079fdb\"\n" + + " },\n" + + " \"debugMode\": false,\n" + + " \"configuration\": null,\n" + + " \"additionalInfo\": null\n" + + "}"; return JacksonUtil.fromString(ruleChainStr, RuleChain.class); } } From 1d83a2c9d387e4b629adbc1b8156f62f0dc6b171 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 7 May 2025 13:01:09 +0300 Subject: [PATCH 41/63] Refactoring for TbMsgProto usage --- ...riginatorIdTbRuleEngineSubmitStrategy.java | 8 +--- ...TbRuleEngineProcessingStrategyFactory.java | 8 ++-- .../TbRuleEngineQueueConsumerManager.java | 15 +++---- .../thingsboard/server/common/msg/TbMsg.java | 45 +++++++++---------- common/message/src/main/proto/tbmsg.proto | 4 +- .../server/common/util/ProtoUtils.java | 22 ++++++++- common/proto/src/main/proto/queue.proto | 2 +- 7 files changed, 55 insertions(+), 49 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByOriginatorIdTbRuleEngineSubmitStrategy.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByOriginatorIdTbRuleEngineSubmitStrategy.java index aec9252356..cec2aa19da 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByOriginatorIdTbRuleEngineSubmitStrategy.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/SequentialByOriginatorIdTbRuleEngineSubmitStrategy.java @@ -20,6 +20,7 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.msg.gen.MsgProtos; +import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.gen.transport.TransportProtos; import java.util.UUID; @@ -34,12 +35,7 @@ public class SequentialByOriginatorIdTbRuleEngineSubmitStrategy extends Sequenti @Override protected EntityId getEntityId(TransportProtos.ToRuleEngineMsg msg) { try { - MsgProtos.TbMsgProto proto; - if (msg.getTbMsg().isEmpty()) { - proto = msg.getTbMsgProto(); - } else { - proto = MsgProtos.TbMsgProto.parseFrom(msg.getTbMsg()); - } + MsgProtos.TbMsgProto proto = ProtoUtils.getTbMsgProto(msg); return EntityIdFactory.getByTypeAndUuid(proto.getEntityType(), new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB())); } catch (InvalidProtocolBufferException e) { log.warn("[{}] Failed to parse TbMsg: {}", queueName, msg); diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java index 2adcd9b199..023b4d4cc7 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java @@ -19,8 +19,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import org.thingsboard.server.common.data.queue.ProcessingStrategy; -import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.queue.TbMsgCallback; +import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -125,7 +125,7 @@ public class TbRuleEngineProcessingStrategyFactory { } log.debug("[{}] Going to reprocess {} messages", queueName, toReprocess.size()); if (log.isTraceEnabled()) { - toReprocess.forEach((id, msg) -> log.trace("Going to reprocess [{}]: {}", id, TbMsg.fromProto(result.getQueueName(), msg.getValue().getTbMsgProto(), msg.getValue().getTbMsg(), TbMsgCallback.EMPTY))); + toReprocess.forEach((id, msg) -> log.trace("Going to reprocess [{}]: {}", id, ProtoUtils.fromTbMsgProto(result.getQueueName(), msg.getValue(), TbMsgCallback.EMPTY))); } if (pauseBetweenRetries > 0) { try { @@ -164,10 +164,10 @@ public class TbRuleEngineProcessingStrategyFactory { log.debug("[{}] Reprocessing skipped for {} failed and {} timeout messages", queueName, result.getFailedMap().size(), result.getPendingMap().size()); } if (log.isTraceEnabled()) { - result.getFailedMap().forEach((id, msg) -> log.trace("Failed messages [{}]: {}", id, TbMsg.fromProto(result.getQueueName(), msg.getValue().getTbMsgProto(), msg.getValue().getTbMsg(), TbMsgCallback.EMPTY))); + result.getFailedMap().forEach((id, msg) -> log.trace("Failed messages [{}]: {}", id, ProtoUtils.fromTbMsgProto(result.getQueueName(), msg.getValue(), TbMsgCallback.EMPTY))); } if (log.isTraceEnabled()) { - result.getPendingMap().forEach((id, msg) -> log.trace("Timeout messages [{}]: {}", id, TbMsg.fromProto(result.getQueueName(), msg.getValue().getTbMsgProto(), msg.getValue().getTbMsg(), TbMsgCallback.EMPTY))); + result.getPendingMap().forEach((id, msg) -> log.trace("Timeout messages [{}]: {}", id, ProtoUtils.fromTbMsgProto(result.getQueueName(), msg.getValue(), TbMsgCallback.EMPTY))); } return new TbRuleEngineProcessingDecision(true, null); } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java index 01368f780e..3aa0d2eabd 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/ruleengine/TbRuleEngineQueueConsumerManager.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.msg.queue.RuleNodeInfo; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbMsgCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; import org.thingsboard.server.queue.TbQueueConsumer; import org.thingsboard.server.queue.common.TbProtoQueueMsg; @@ -178,7 +179,7 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager relationTypes; @@ -207,7 +208,7 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager> pending : map.entrySet()) { ToRuleEngineMsg tmp = pending.getValue().getValue(); - TbMsg tmpMsg = TbMsg.fromProto(config.getName(), tmp.getTbMsgProto(), tmp.getTbMsg(), TbMsgCallback.EMPTY); + TbMsg tmpMsg = ProtoUtils.fromTbMsgProto(config.getName(), tmp, TbMsgCallback.EMPTY); RuleNodeInfo ruleNodeInfo = ctx.getLastVisitedRuleNode(pending.getKey()); if (printAll) { log.trace("[{}][{}] {} to process message: {}, Last Rule Node: {}", queueKey, TenantId.fromUUID(new UUID(tmp.getTenantIdMSB(), tmp.getTenantIdLSB())), prefix, tmpMsg, ruleNodeInfo); @@ -236,13 +237,7 @@ public class TbRuleEngineQueueConsumerManager extends MainQueueConsumerManager msg : msgs) { try { - MsgProtos.TbMsgProto tbMsgProto; - if (msg.getValue().getTbMsg().isEmpty()) { - tbMsgProto = msg.getValue().getTbMsgProto(); - } else { - tbMsgProto = MsgProtos.TbMsgProto.parseFrom(msg.getValue().getTbMsg()); - } - + MsgProtos.TbMsgProto tbMsgProto = ProtoUtils.getTbMsgProto(msg.getValue()); EntityId originator = EntityIdFactory.getByTypeAndUuid(tbMsgProto.getEntityType(), new UUID(tbMsgProto.getEntityIdMSB(), tbMsgProto.getEntityIdLSB())); TopicPartitionInfo tpi = ctx.getPartitionService().resolve(ServiceType.TB_RULE_ENGINE, config.getName(), TenantId.SYS_TENANT_ID, originator); diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java index b7447b14ba..1d8d9497a9 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.msg.gen.MsgProtos; +import org.thingsboard.server.common.msg.gen.MsgProtos.TbMsgProto; import org.thingsboard.server.common.msg.queue.TbMsgCallback; import java.io.Serializable; @@ -151,8 +152,8 @@ public final class TbMsg implements Serializable { this.callback = Objects.requireNonNullElse(callback, TbMsgCallback.EMPTY); } - public static MsgProtos.TbMsgProto toProto(TbMsg msg) { - MsgProtos.TbMsgProto.Builder builder = MsgProtos.TbMsgProto.newBuilder(); + public static TbMsgProto toProto(TbMsg msg) { + TbMsgProto.Builder builder = TbMsgProto.newBuilder(); builder.setId(msg.getId().toString()); builder.setTs(msg.getTs()); builder.setType(msg.getType()); @@ -204,12 +205,11 @@ public final class TbMsg implements Serializable { return builder.build(); } - //TODO: added for processing old messages from queue, should be removed after release - @Deprecated(forRemoval = true) - public static TbMsg fromProto(String queueName, MsgProtos.TbMsgProto proto, ByteString data, TbMsgCallback callback) { + @Deprecated(forRemoval = true, since = "4.1") // to be removed in 4.2 + public static TbMsg fromProto(String queueName, TbMsgProto proto, ByteString data, TbMsgCallback callback) { try { if (!data.isEmpty()) { - proto = MsgProtos.TbMsgProto.parseFrom(data); + proto = TbMsgProto.parseFrom(data); } } catch (InvalidProtocolBufferException e) { throw new IllegalStateException("Could not parse protobuf for TbMsg", e); @@ -217,7 +217,7 @@ public final class TbMsg implements Serializable { return fromProto(queueName, proto, callback); } - public static TbMsg fromProto(String queueName, MsgProtos.TbMsgProto proto, TbMsgCallback callback) { + public static TbMsg fromProto(String queueName, TbMsgProto proto, TbMsgCallback callback) { TbMsgMetaData metaData = new TbMsgMetaData(proto.getMetaData().getDataMap()); EntityId entityId = EntityIdFactory.getByTypeAndUuid(proto.getEntityType(), new UUID(proto.getEntityIdMSB(), proto.getEntityIdLSB())); CustomerId customerId = null; @@ -225,7 +225,8 @@ public final class TbMsg implements Serializable { RuleNodeId ruleNodeId = null; UUID correlationId = null; Integer partition = null; - List calculatedFieldIds = new CopyOnWriteArrayList<>();if (proto.getCustomerIdMSB() != 0L && proto.getCustomerIdLSB() != 0L) { + List calculatedFieldIds = new CopyOnWriteArrayList<>(); + if (proto.getCustomerIdMSB() != 0L && proto.getCustomerIdLSB() != 0L) { customerId = new CustomerId(new UUID(proto.getCustomerIdMSB(), proto.getCustomerIdLSB())); } if (proto.getRuleChainIdMSB() != 0L && proto.getRuleChainIdLSB() != 0L) { @@ -240,19 +241,13 @@ public final class TbMsg implements Serializable { } for (MsgProtos.CalculatedFieldIdProto cfIdProto : proto.getCalculatedFieldsList()) { - CalculatedFieldId calculatedFieldId = new CalculatedFieldId(new UUID( - cfIdProto.getCalculatedFieldIdMSB(), - cfIdProto.getCalculatedFieldIdLSB() - )); - calculatedFieldIds.add(calculatedFieldId); - }TbMsgProcessingCtx ctx; - if (proto.hasCtx()) { - ctx = TbMsgProcessingCtx.fromProto(proto.getCtx()); - } else { - // Backward compatibility with unprocessed messages fetched from queue after update. - ctx = new TbMsgProcessingCtx(proto.getRuleNodeExecCounter()); + CalculatedFieldId calculatedFieldId = new CalculatedFieldId(new UUID( + cfIdProto.getCalculatedFieldIdMSB(), + cfIdProto.getCalculatedFieldIdLSB() + )); + calculatedFieldIds.add(calculatedFieldId); } - + TbMsgProcessingCtx ctx = TbMsgProcessingCtx.fromProto(proto.getCtx()); TbMsgDataType dataType = TbMsgDataType.values()[proto.getDataType()]; return new TbMsg(queueName, UUID.fromString(proto.getId()), proto.getTs(), null, proto.getType(), entityId, customerId, metaData, dataType, proto.getData(), ruleChainId, ruleNodeId, correlationId, partition, calculatedFieldIds, ctx, callback); @@ -505,11 +500,11 @@ public final class TbMsg implements Serializable { public String toString() { return "TbMsg.TbMsgBuilder(queueName=" + this.queueName + ", id=" + this.id + ", ts=" + this.ts + - ", type=" + this.type + ", internalType=" + this.internalType + ", originator=" + this.originator + - ", customerId=" + this.customerId + ", metaData=" + this.metaData + ", dataType=" + this.dataType + - ", data=" + this.data + ", ruleChainId=" + this.ruleChainId + ", ruleNodeId=" + this.ruleNodeId + - ", correlationId=" + this.correlationId + ", partition=" + this.partition + ", previousCalculatedFields=" + this.previousCalculatedFieldIds + - ", ctx=" + this.ctx + ", callback=" + this.callback + ")"; + ", type=" + this.type + ", internalType=" + this.internalType + ", originator=" + this.originator + + ", customerId=" + this.customerId + ", metaData=" + this.metaData + ", dataType=" + this.dataType + + ", data=" + this.data + ", ruleChainId=" + this.ruleChainId + ", ruleNodeId=" + this.ruleNodeId + + ", correlationId=" + this.correlationId + ", partition=" + this.partition + ", previousCalculatedFields=" + this.previousCalculatedFieldIds + + ", ctx=" + this.ctx + ", callback=" + this.callback + ")"; } } diff --git a/common/message/src/main/proto/tbmsg.proto b/common/message/src/main/proto/tbmsg.proto index 65a967e9e4..e70104d503 100644 --- a/common/message/src/main/proto/tbmsg.proto +++ b/common/message/src/main/proto/tbmsg.proto @@ -59,8 +59,8 @@ message TbMsgProto { string data = 14; int64 ts = 15; - // Will be removed in 3.4. Moved to processing context - int32 ruleNodeExecCounter = 16; + + // ruleNodeExecCounter (16) was removed in 4.1 int64 customerIdMSB = 17; int64 customerIdLSB = 18; diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index 1ebd753f3c..502884eb31 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -18,6 +18,8 @@ package org.thingsboard.server.common.util; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.ApiUsageRecordKey; @@ -76,12 +78,15 @@ import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.sync.vc.RepositoryAuthMethod; import org.thingsboard.server.common.data.sync.vc.RepositorySettings; +import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.common.msg.edge.EdgeHighPriorityMsg; import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse; import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; +import org.thingsboard.server.common.msg.gen.MsgProtos; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; +import org.thingsboard.server.common.msg.queue.TbMsgCallback; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponseActorMsg; import org.thingsboard.server.common.msg.rpc.RemoveRpcActorMsg; @@ -93,8 +98,8 @@ import org.thingsboard.server.common.msg.rule.engine.DeviceDeleteMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceEdgeUpdateMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceNameOrTypeUpdateMsg; import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.gen.transport.TransportProtos.KeyValueProto; import org.thingsboard.server.gen.transport.TransportProtos.ApiUsageRecordKeyProto; +import org.thingsboard.server.gen.transport.TransportProtos.KeyValueProto; import java.util.ArrayList; import java.util.Arrays; @@ -1344,6 +1349,21 @@ public class ProtoUtils { return builder.build(); } + @Deprecated(forRemoval = true, since = "4.1") + public static MsgProtos.TbMsgProto getTbMsgProto(TransportProtos.ToRuleEngineMsg ruleEngineMsg) throws InvalidProtocolBufferException { + if (ruleEngineMsg.getTbMsg().isEmpty()) { + return ruleEngineMsg.getTbMsgProto(); + } else { + return MsgProtos.TbMsgProto.parseFrom(ruleEngineMsg.getTbMsg()); + } + } + + @SneakyThrows + @Deprecated(forRemoval = true, since = "4.1") // inline to TbMsg.fromProto(queueName, ruleEngineMsg.getTbMsgProto(), callback) + public static TbMsg fromTbMsgProto(String queueName, TransportProtos.ToRuleEngineMsg ruleEngineMsg, TbMsgCallback callback) { + return TbMsg.fromProto(queueName, getTbMsgProto(ruleEngineMsg), callback); + } + private static boolean isNotNull(Object obj) { return obj != null; } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 449f662a93..2a97fd35d0 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -1703,7 +1703,7 @@ message ToCalculatedFieldNotificationMsg { message ToRuleEngineMsg { int64 tenantIdMSB = 1; int64 tenantIdLSB = 2; - bytes tbMsg = 3 [deprecated = true]; + bytes tbMsg = 3 [deprecated = true]; // for removal in 4.2 repeated string relationTypes = 4; string failureMessage = 5; msgqueue.TbMsgProto tbMsgProto = 6; From 3ea08293fa4982ce24660a07d8878af4fd777c1d Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Wed, 7 May 2025 16:32:25 +0300 Subject: [PATCH 42/63] removed resource CF --- .../org/thingsboard/server/controller/BaseController.java | 8 ++++++-- .../server/controller/CalculatedFieldController.java | 6 ++---- .../server/service/security/permission/Resource.java | 3 +-- .../security/permission/TenantAdminPermissions.java | 1 - 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 73e278389a..d29d398dca 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -964,8 +964,12 @@ public abstract class BaseController { } } - protected CalculatedField checkCalculatedFieldId(CalculatedFieldId calculatedFieldId, Operation operation) throws ThingsboardException { - return checkEntityId(calculatedFieldId, calculatedFieldService::findById, operation); + private void checkCalculatedFieldId(CalculatedFieldId calculatedFieldId, Operation operation) throws ThingsboardException { + validateId(calculatedFieldId, "Invalid entity id"); + SecurityUser user = getCurrentUser(); + CalculatedField cf = calculatedFieldService.findById(user.getTenantId(), calculatedFieldId); + checkNotNull(cf, calculatedFieldId.getEntityType().getNormalName() + " with id [" + calculatedFieldId + "] is not found"); + checkEntityId(cf.getEntityId(), operation); } protected HomeDashboardInfo getHomeDashboardInfo(SecurityUser securityUser, JsonNode additionalInfo) { diff --git a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java index f899d0f480..1c00988f6a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java @@ -59,7 +59,6 @@ import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldTbelScriptEngi import org.thingsboard.server.service.entitiy.cf.TbCalculatedFieldService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; -import org.thingsboard.server.service.security.permission.Resource; import java.util.ArrayList; import java.util.Collections; @@ -136,7 +135,6 @@ public class CalculatedFieldController extends BaseController { public CalculatedField saveCalculatedField(@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "A JSON value representing the calculated field.") @RequestBody CalculatedField calculatedField) throws Exception { calculatedField.setTenantId(getTenantId()); - checkEntity(calculatedField.getId(), calculatedField, Resource.CALCULATED_FIELD); checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD); checkReferencedEntities(calculatedField.getConfiguration(), getCurrentUser()); return tbCalculatedFieldService.save(calculatedField, getCurrentUser()); @@ -186,7 +184,7 @@ public class CalculatedFieldController extends BaseController { public void deleteCalculatedField(@PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws Exception { checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId); CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId)); - CalculatedField calculatedField = checkCalculatedFieldId(calculatedFieldId, Operation.DELETE); + CalculatedField calculatedField = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser()); checkEntityId(calculatedField.getEntityId(), Operation.WRITE_CALCULATED_FIELD); tbCalculatedFieldService.delete(calculatedField, getCurrentUser()); } @@ -200,7 +198,7 @@ public class CalculatedFieldController extends BaseController { public JsonNode getLatestCalculatedFieldDebugEvent(@Parameter @PathVariable(CALCULATED_FIELD_ID) String strCalculatedFieldId) throws ThingsboardException { checkParameter(CALCULATED_FIELD_ID, strCalculatedFieldId); CalculatedFieldId calculatedFieldId = new CalculatedFieldId(toUUID(strCalculatedFieldId)); - CalculatedField calculatedField = checkCalculatedFieldId(calculatedFieldId, Operation.READ); + CalculatedField calculatedField = tbCalculatedFieldService.findById(calculatedFieldId, getCurrentUser()); checkEntityId(calculatedField.getEntityId(), Operation.READ_CALCULATED_FIELD); TenantId tenantId = getCurrentUser().getTenantId(); return Optional.ofNullable(eventService.findLatestEvents(tenantId, calculatedFieldId, EventType.DEBUG_CALCULATED_FIELD, 1)) diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index 9d7590f786..4cb281a719 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -50,8 +50,7 @@ public enum Resource { VERSION_CONTROL, NOTIFICATION(EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_REQUEST, EntityType.NOTIFICATION_RULE), - MOBILE_APP_SETTINGS, - CALCULATED_FIELD(EntityType.CALCULATED_FIELD); + MOBILE_APP_SETTINGS; private final Set entityTypes; diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index a072cf2738..7a67d6739e 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -55,7 +55,6 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.OAUTH2_CONFIGURATION_TEMPLATE, new PermissionChecker.GenericPermissionChecker(Operation.READ)); put(Resource.MOBILE_APP, tenantEntityPermissionChecker); put(Resource.MOBILE_APP_BUNDLE, tenantEntityPermissionChecker); - put(Resource.CALCULATED_FIELD, tenantEntityPermissionChecker); } public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { From 67048b936dc1a79c89941866d613e8775e2ce2a0 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 9 May 2025 10:08:58 +0300 Subject: [PATCH 43/63] UI: Remove sticky option for argument table header row --- .../calculated-field-arguments-table.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html index 53d8a9d7b6..b4b5a939e1 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/arguments-table/calculated-field-arguments-table.component.html @@ -116,7 +116,7 @@ + *matHeaderRowDef="['name', 'entityType', 'target', 'type', 'key', 'actions']"> From ecc82d8c048ed7f0d699ff54dc1d53c0ac623d5d Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 9 May 2025 12:23:51 +0300 Subject: [PATCH 44/63] UI: Event table hide clear event button as input --- .../app/modules/home/components/event/event-table-config.ts | 4 ++-- .../modules/home/components/event/event-table.component.ts | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) 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 0335c43f29..03fc641735 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 @@ -59,7 +59,6 @@ import { AppState } from '@core/core.state'; export class EventTableConfig extends EntityTableConfig { eventTypeValue: EventType | DebugEventType; - hideClearEventAction = false; private filterParams: FilterEventBody = {}; private filterColumns: FilterEntityColumn[] = []; @@ -95,7 +94,8 @@ export class EventTableConfig extends EntityTableConfig { private cd: ChangeDetectorRef, private store: Store, public testButtonLabel?: string, - private debugEventSelected?: EventEmitter) { + private debugEventSelected?: EventEmitter, + public hideClearEventAction = false) { super(); this.loadDataOnInit = false; this.tableTitle = ''; diff --git a/ui-ngx/src/app/modules/home/components/event/event-table.component.ts b/ui-ngx/src/app/modules/home/components/event/event-table.component.ts index d12791680b..f51cf66d8a 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table.component.ts @@ -58,6 +58,9 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy { @Input() debugEventTypes: Array; + @Input() + hideClearEventAction: boolean = false; + activeValue = false; dirtyValue = false; entityIdValue: EntityId; @@ -147,7 +150,8 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy { this.cd, this.store, this.functionTestButtonLabel, - this.debugEventSelected + this.debugEventSelected, + this.hideClearEventAction ); } From 590805b6a3ec92e868e6a9b6427cc32f4670a998 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Fri, 9 May 2025 13:25:31 +0300 Subject: [PATCH 45/63] Handle uncaught auth exceptions --- .../ThingsboardSecurityConfiguration.java | 7 ++- .../server/controller/BaseController.java | 9 +--- .../ThingsboardErrorResponseHandler.java | 26 ++++++++++- .../security/auth/AuthExceptionHandler.java | 46 +++++++++++++++++++ .../data/exception/ThingsboardErrorCode.java | 3 +- 5 files changed, 80 insertions(+), 11 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/security/auth/AuthExceptionHandler.java diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index 871798ccd1..93cf9da8e0 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -47,6 +47,7 @@ import org.springframework.web.filter.ShallowEtagHeaderFilter; import org.thingsboard.server.dao.oauth2.OAuth2Configuration; import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.auth.AuthExceptionHandler; import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider; import org.thingsboard.server.service.security.auth.jwt.JwtTokenAuthenticationProcessingFilter; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenAuthenticationProvider; @@ -129,6 +130,9 @@ public class ThingsboardSecurityConfiguration { @Autowired private RateLimitProcessingFilter rateLimitProcessingFilter; + @Autowired + private AuthExceptionHandler authExceptionHandler; + @Bean protected PayloadSizeFilter payloadSizeFilter() { return new PayloadSizeFilter(maxPayloadSizeConfig); @@ -235,7 +239,8 @@ public class ThingsboardSecurityConfiguration { .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(payloadSizeFilter(), UsernamePasswordAuthenticationFilter.class) - .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class); + .addFilterAfter(rateLimitProcessingFilter, UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(authExceptionHandler, buildRestLoginProcessingFilter().getClass()); if (oauth2Configuration != null) { http.oauth2Login(login -> login .authorizationEndpoint(config -> config diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index d29d398dca..93bf5b35fe 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -437,14 +437,7 @@ public abstract class BaseController { } else if (exception instanceof AsyncRequestTimeoutException) { return new ThingsboardException("Request timeout", ThingsboardErrorCode.GENERAL); } else if (exception instanceof DataAccessException) { - if (!logControllerErrorStackTrace) { // not to log the error twice - log.warn("Database error: {} - {}", exception.getClass().getSimpleName(), ExceptionUtils.getRootCauseMessage(exception)); - } - if (cause instanceof ConstraintViolationException) { - return new ThingsboardException(ExceptionUtils.getRootCause(exception).getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS); - } else { - return new ThingsboardException("Database error", ThingsboardErrorCode.GENERAL); - } + return new ThingsboardException(exception, ThingsboardErrorCode.DATABASE); } else if (exception instanceof EntityVersionMismatchException) { return new ThingsboardException(exception.getMessage(), exception, ThingsboardErrorCode.VERSION_CONFLICT); } diff --git a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java index 9a08441936..7be11e5748 100644 --- a/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java +++ b/application/src/main/java/org/thingsboard/server/exception/ThingsboardErrorResponseHandler.java @@ -21,7 +21,9 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; +import org.hibernate.exception.ConstraintViolationException; import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.dao.DataAccessException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; @@ -139,6 +141,8 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand ThingsboardException thingsboardException = (ThingsboardException) exception; if (thingsboardException.getErrorCode() == ThingsboardErrorCode.SUBSCRIPTION_VIOLATION) { handleSubscriptionException((ThingsboardException) exception, response); + } else if (thingsboardException.getErrorCode() == ThingsboardErrorCode.DATABASE) { + handleDatabaseException(thingsboardException.getCause(), response); } else { handleThingsboardException((ThingsboardException) exception, response); } @@ -148,8 +152,10 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand handleAccessDeniedException(response); } else if (exception instanceof AuthenticationException) { handleAuthenticationException((AuthenticationException) exception, response); - } else if (exception instanceof MaxPayloadSizeExceededException) { + } else if (exception instanceof MaxPayloadSizeExceededException) { handleMaxPayloadSizeExceededException(response, (MaxPayloadSizeExceededException) exception); + } else if (exception instanceof DataAccessException e) { + handleDatabaseException(e, response); } else { response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); JacksonUtil.writeValue(response.getWriter(), ThingsboardErrorResponse.of(exception.getMessage(), @@ -201,6 +207,17 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand JacksonUtil.fromBytes(((HttpClientErrorException) subscriptionException.getCause()).getResponseBodyAsByteArray(), Object.class)); } + private void handleDatabaseException(Throwable databaseException, HttpServletResponse response) throws IOException { + ThingsboardErrorResponse errorResponse; + if (databaseException instanceof ConstraintViolationException) { + errorResponse = ThingsboardErrorResponse.of(ExceptionUtils.getRootCause(databaseException).getMessage(), ThingsboardErrorCode.BAD_REQUEST_PARAMS, HttpStatus.BAD_REQUEST); + } else { + log.warn("Database error: {} - {}", databaseException.getClass().getSimpleName(), ExceptionUtils.getRootCauseMessage(databaseException)); + errorResponse = ThingsboardErrorResponse.of("Database error", ThingsboardErrorCode.DATABASE, HttpStatus.INTERNAL_SERVER_ERROR); + } + writeResponse(errorResponse, response); + } + private void handleAccessDeniedException(HttpServletResponse response) throws IOException { response.setStatus(HttpStatus.FORBIDDEN.value()); JacksonUtil.writeValue(response.getWriter(), @@ -233,4 +250,11 @@ public class ThingsboardErrorResponseHandler extends ResponseEntityExceptionHand } } + // TODO: refactor this class to use this method instead of boilerplate JacksonUtil.writeValue(response.getWriter(), ... + private void writeResponse(ThingsboardErrorResponse errorResponse, HttpServletResponse response) throws IOException { + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(errorResponse.getStatus()); + JacksonUtil.writeValue(response.getWriter(), errorResponse); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/AuthExceptionHandler.java b/application/src/main/java/org/thingsboard/server/service/security/auth/AuthExceptionHandler.java new file mode 100644 index 0000000000..27ed2996d1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/AuthExceptionHandler.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.security.auth; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.AuthenticationException; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.thingsboard.server.exception.ThingsboardErrorResponseHandler; + +@Component +@RequiredArgsConstructor +@Slf4j +public class AuthExceptionHandler extends OncePerRequestFilter { + + private final ThingsboardErrorResponseHandler errorResponseHandler; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { + try { + filterChain.doFilter(request, response); + } catch (AuthenticationException e) { + throw e; + } catch (Exception e) { + errorResponseHandler.handle(e, response); + } + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java index 13c65fe6e6..deae86a9de 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/exception/ThingsboardErrorCode.java @@ -31,7 +31,8 @@ public enum ThingsboardErrorCode { TOO_MANY_UPDATES(34), VERSION_CONFLICT(35), SUBSCRIPTION_VIOLATION(40), - PASSWORD_VIOLATION(45); + PASSWORD_VIOLATION(45), + DATABASE(46); private int errorCode; From ed5868d8e5d825d033d638a4da297ca1a397f285 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Fri, 9 May 2025 14:16:53 +0300 Subject: [PATCH 46/63] removed permission check for tenant --- .../server/controller/CalculatedFieldController.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java index 1c00988f6a..0dcf22e433 100644 --- a/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java +++ b/application/src/main/java/org/thingsboard/server/controller/CalculatedFieldController.java @@ -270,7 +270,10 @@ public class CalculatedFieldController extends BaseController { for (EntityId referencedEntityId : referencedEntityIds) { EntityType entityType = referencedEntityId.getEntityType(); switch (entityType) { - case TENANT, CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ); + case TENANT -> { + return; + } + case CUSTOMER, ASSET, DEVICE -> checkEntityId(referencedEntityId, Operation.READ); default -> throw new IllegalArgumentException("Calculated fields do not support '" + entityType + "' for referenced entities."); } From 9bb50ae355265dae626432855e87f4788fb3ddb8 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 9 May 2025 14:55:28 +0300 Subject: [PATCH 47/63] UI: Refacoring hide clear event --- .../app/modules/home/components/event/event-table.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/event/event-table.component.ts b/ui-ngx/src/app/modules/home/components/event/event-table.component.ts index f51cf66d8a..53cb8e1b96 100644 --- a/ui-ngx/src/app/modules/home/components/event/event-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/event/event-table.component.ts @@ -156,7 +156,7 @@ export class EventTableComponent implements OnInit, AfterViewInit, OnDestroy { } ngAfterViewInit() { - this.isEmptyData$ = this.entitiesTable.dataSource.isEmpty().subscribe(value => this.eventTableConfig.hideClearEventAction = value); + this.isEmptyData$ = this.entitiesTable.dataSource.isEmpty().subscribe(value => this.eventTableConfig.hideClearEventAction = value || this.hideClearEventAction); } ngOnDestroy() { From 68eff16e676c05c2d9c099a0856dd3ae53df1084 Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Fri, 9 May 2025 19:12:40 +0300 Subject: [PATCH 48/63] lwm2m: create strategy Composite_By_Object and tests --- .../lwm2m/AbstractLwM2MIntegrationTest.java | 186 ++++++++++++--- .../lwm2m/client/SimpleLwM2MDevice.java | 4 +- .../ota/AbstractOtaLwM2MIntegrationTest.java | 8 +- .../AbstractSecurityLwM2MIntegrationTest.java | 2 - .../sql/NoSecLwM2MIntegrationTest.java | 2 +- ...rveStrategyTransportConfigurationTest.java | 2 +- ...StrategyWithNoSecQueueModeConnectTest.java | 27 ++- .../lwm2m/TelemetryObserveStrategy.java | 2 +- .../server/client/LwM2mClientContext.java | 3 - .../server/client/LwM2mClientContextImpl.java | 15 +- .../server/client/ResultUpdateResource.java | 15 ++ .../lwm2m/server/model/LwM2MModelConfig.java | 65 +++++- .../model/LwM2MModelConfigServiceImpl.java | 118 ++++++++-- .../ParametersAnalyzeResult.java | 2 +- .../model/ParametersObserveAnalyzeResult.java | 44 ++++ .../model/ParametersUpdateAnalyzeResult.java | 33 +++ .../ota/DefaultLwM2MOtaUpdateService.java | 10 +- .../uplink/DefaultLwM2mUplinkMsgHandler.java | 212 +++++++++++------- .../lwm2m/utils/LwM2MTransportUtil.java | 66 +++++- 19 files changed, 644 insertions(+), 172 deletions(-) rename common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/{client => model}/ParametersAnalyzeResult.java (94%) create mode 100644 common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/ParametersObserveAnalyzeResult.java create mode 100644 common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/ParametersUpdateAnalyzeResult.java diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index 5fec5107bd..587f9ade1c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -34,7 +34,6 @@ import org.springframework.http.HttpStatus; import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; -import org.thingsboard.script.api.tbel.TbDate; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileProvisionType; @@ -53,6 +52,7 @@ import org.thingsboard.server.common.data.device.profile.DisabledDeviceProfilePr import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.lwm2m.OtherConfiguration; import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryMappingConfiguration; +import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy; import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.AbstractLwM2MBootstrapServerCredential; import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.LwM2MBootstrapServerCredential; import org.thingsboard.server.common.data.device.profile.lwm2m.bootstrap.NoSecLwM2MBootstrapServerCredential; @@ -90,10 +90,15 @@ import static org.awaitility.Awaitility.await; import static org.eclipse.leshan.client.object.Security.noSec; import static org.hamcrest.core.IsInstanceOf.instanceOf; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.COMPOSITE_ALL; +import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.COMPOSITE_BY_OBJECT; +import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.SINGLE; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_STARTED; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_BOOTSTRAP_SUCCESS; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MClientState.ON_INIT; @@ -180,43 +185,97 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte " }"; public static String TELEMETRY_WITH_MANY_OBSERVE = + " {\n" + + " \"keyName\": {\n" + + " \"/3_1.2/0/9\": \"batteryLevel\",\n" + + " \"/3_1.2/0/20\": \"batteryStatus\"\n" + + " },\n" + + " \"observe\": [\n" + + " \"/3_1.2/0/9\",\n" + + " \"/3_1.2/0/20\"\n" + + " ],\n" + + " \"attribute\": [],\n" + + " \"telemetry\": [\n" + + " \"/3_1.2/0/9\",\n" + + " \"/3_1.2/0/20\"\n" + + " ],\n" + + " \"attributeLwm2m\": {},\n" + + " \"observeStrategy\": 0\n" + + " }"; + + public static String TELEMETRY_WITH_SINGLE_PARAMS_OBJECT_ID_5_ID_3 = + " {\n" + + " \"keyName\": {\n" + + " \"/3_1.2/0/9\": \"batteryLevel\",\n" + + " \"/5_1.2/0/3\": \"state\",\n" + + " \"/5_1.2/0/5\": \"updateResult\",\n" + + " \"/5_1.2/0/6\": \"pkgname\",\n" + + " \"/5_1.2/0/7\": \"pkgversion\",\n" + + " \"/5_1.2/0/9\": \"firmwareUpdateDeliveryMethod\"\n" + + " },\n" + + " \"observe\": [\n" + + " \"/3_1.2/0/9\",\n" + + " \"/5_1.2/0/3\",\n" + + " \"/5_1.2/0/5\",\n" + + " \"/5_1.2/0/6\",\n" + + " \"/5_1.2/0/7\",\n" + + " \"/5_1.2/0/9\"\n" + + " ],\n" + + " \"attribute\": [],\n" + + " \"telemetry\": [\n" + + " \"/3_1.2/0/9\",\n" + + " \"/5_1.2/0/3\",\n" + + " \"/5_1.2/0/5\",\n" + + " \"/5_1.2/0/6\",\n" + + " \"/5_1.2/0/7\",\n" + + " \"/5_1.2/0/9\"\n" + + " ],\n" + + " \"attributeLwm2m\": {},\n" + + " \"observeStrategy\": 0\n" + + " }"; + + public static String TELEMETRY_WITH_COMPOSITE_ALL_OBSERVE_ID_3_ID_19 = " {\n" + " \"keyName\": {\n" + " \"/3_1.2/0/9\": \"batteryLevel\",\n" + - " \"/3_1.2/0/20\": \"batteryStatus\"\n" + + " \"/3_1.2/0/20\": \"batteryStatus\",\n" + + " \"/19_1.1/0/2\": \"dataCreationTime\"\n" + " },\n" + " \"observe\": [\n" + " \"/3_1.2/0/9\",\n" + - " \"/3_1.2/0/20\"\n" + + " \"/3_1.2/0/20\",\n" + + " \"/19_1.1/0/2\"\n" + " ],\n" + " \"attribute\": [],\n" + " \"telemetry\": [\n" + " \"/3_1.2/0/9\",\n" + - " \"/3_1.2/0/20\"\n" + + " \"/3_1.2/0/20\",\n" + + " \"/19_1.1/0/2\"\n" + " ],\n" + - " \"attributeLwm2m\": {}\n" + + " \"attributeLwm2m\": {},\n" + + " \"observeStrategy\": 1\n" + " }"; - public static String TELEMETRY_WITH_COMPOSITE_OBSERVE = + public static String TELEMETRY_WITH_COMPOSITE_BY_OBJECT_OBSERVE_ID_3_ID_5_ID_19 = " {\n" + " \"keyName\": {\n" + - " \"/3_1.2/0/9\": \"batteryLevel\",\n" + " \"/3_1.2/0/20\": \"batteryStatus\",\n" + + " \"/5_1.2/0/6\": \"pkgname\",\n" + " \"/19_1.1/0/2\": \"dataCreationTime\"\n" + " },\n" + " \"observe\": [\n" + - " \"/3_1.2/0/9\",\n" + " \"/3_1.2/0/20\",\n" + + " \"/5_1.2/0/6\",\n" + " \"/19_1.1/0/2\"\n" + " ],\n" + " \"attribute\": [],\n" + " \"telemetry\": [\n" + - " \"/3_1.2/0/9\",\n" + " \"/3_1.2/0/20\",\n" + + " \"/5_1.2/0/6\",\n" + " \"/19_1.1/0/2\"\n" + " ],\n" + " \"attributeLwm2m\": {},\n" + - " \"observeStrategy\": 1\n" + + " \"observeStrategy\": 2\n" + " }"; public static final String CLIENT_LWM2M_SETTINGS = @@ -274,8 +333,9 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte public void basicTestConnectionObserveSingleTelemetry(Security security, LwM2MDeviceCredentials deviceCredentials, String endpoint, - boolean queueMode) throws Exception { - Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITHOUT_OBSERVE, getBootstrapServerCredentialsNoSec(NONE)); + boolean queueMode, + boolean isUpdateProfile) throws Exception { + Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITH_ONE_OBSERVE, getBootstrapServerCredentialsNoSec(NONE)); DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + endpoint, transportConfiguration); Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId()); @@ -292,7 +352,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte getWsClient().registerWaitForUpdate(); this.createNewClient(security, null, false, endpoint, null, queueMode, device.getId().getId().toString()); - awaitObserveReadAll(0, lwM2MTestClient.getDeviceIdStr()); + awaitObserveReadAll(1, lwM2MTestClient.getDeviceIdStr()); String msg = getWsClient().waitForUpdate(); EntityDataUpdate update = JacksonUtil.fromString(msg, EntityDataUpdate.class); @@ -303,18 +363,45 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES)); var tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("batteryLevel"); - Assert.assertThat(Long.parseLong(tsValue.getValue()), instanceOf(Long.class)); + assertThat(Long.parseLong(tsValue.getValue()), instanceOf(Long.class)); int expectedMax = 50; int expectedMin = 5; Assert.assertTrue(expectedMax >= Long.parseLong(tsValue.getValue())); Assert.assertTrue(expectedMin <= Long.parseLong(tsValue.getValue())); + if (isUpdateProfile) { + String actualResultReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null); + String expectedReadAll = "[\"SingleObservation:/3/0/9\"]"; + assertEquals(expectedReadAll, actualResultReadAll); + + updateProfile(deviceProfile, TELEMETRY_WITH_SINGLE_PARAMS_OBJECT_ID_5_ID_3, SINGLE); + awaitObserveReadAll(6, lwM2MTestClient.getDeviceIdStr()); + String expectedReadAll_3_9 = "\"SingleObservation:/3/0/9\""; + String expectedReadAll_5_5 = "\"SingleObservation:/5/0/5\""; + String expectedReadAll_5_6 = "\"SingleObservation:/5/0/6\""; + String expectedReadAll_5_7 = "\"SingleObservation:/5/0/7\""; + String expectedReadAll_5_9 = "\"SingleObservation:/5/0/9\""; + actualResultReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null); + assertTrue(expectedReadAll, actualResultReadAll.contains(expectedReadAll_3_9)); + assertTrue(expectedReadAll, actualResultReadAll.contains(expectedReadAll_5_5)); + assertTrue(expectedReadAll, actualResultReadAll.contains(expectedReadAll_5_6)); + assertTrue(expectedReadAll, actualResultReadAll.contains(expectedReadAll_5_7)); + assertTrue(expectedReadAll, actualResultReadAll.contains(expectedReadAll_5_9)); + + updateProfile(deviceProfile, TELEMETRY_WITH_MANY_OBSERVE, SINGLE); + awaitObserveReadAll(2, lwM2MTestClient.getDeviceIdStr()); + String expectedReadAll_3_20 = "\"SingleObservation:/3/0/20\""; + actualResultReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null); + assertTrue(expectedReadAll, actualResultReadAll.contains(expectedReadAll_3_9)); + assertTrue(expectedReadAll, actualResultReadAll.contains(expectedReadAll_3_20)); + } } public void basicTestConnectionObserveCompositeTelemetry(Security security, LwM2MDeviceCredentials deviceCredentials, String endpoint, Lwm2mDeviceProfileTransportConfiguration transportConfiguration, - int cntObserve) throws Exception { + int cntObserve, + int varTest) throws Exception { DeviceProfile deviceProfile = createLwm2mDeviceProfile("profileFor" + endpoint, transportConfiguration); Device device = createLwm2mDevice(deviceCredentials, endpoint, deviceProfile.getId()); @@ -322,14 +409,15 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte SingleEntityFilter sef = new SingleEntityFilter(); sef.setSingleEntity(device.getId()); LatestValueCmd latestCmd = new LatestValueCmd(); - String key1 = "batteryLevel"; - String key2 = "dataCreationTime"; + String key1 = "pkgname"; + String key2 = "pkgversion"; + String key3 = "batteryLevel"; latestCmd.setKeys(Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, key1))); latestCmd.setKeys(Collections.singletonList(new EntityKey(EntityKeyType.TIME_SERIES, key2))); EntityDataQuery edq = new EntityDataQuery(sef, new EntityDataPageLink(1, 0, null, null), Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); - EntityDataCmd cmd = new EntityDataCmd(2, edq, null, latestCmd, null); + EntityDataCmd cmd = new EntityDataCmd(3, edq, null, latestCmd, null); getWsClient().send(cmd); getWsClient().waitForReply(); @@ -339,7 +427,7 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte String msg = getWsClient().waitForUpdate(); EntityDataUpdate update = JacksonUtil.fromString(msg, EntityDataUpdate.class); - Assert.assertEquals(2, update.getCmdId()); + Assert.assertEquals(3, update.getCmdId()); List eData = update.getUpdate(); Assert.assertNotNull(eData); Assert.assertEquals(1, eData.size()); @@ -347,16 +435,54 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES)); var tsValue1 = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get(key1); var tsValue2 = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get(key2); - if (tsValue1 != null) { - Assert.assertThat(Long.parseLong(tsValue1.getValue()), instanceOf(Long.class)); + var tsValue3 = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get(key3); + var value = tsValue1 != null ? tsValue1.getValue() : tsValue2.getValue(); + if (tsValue3 != null) { + assertThat(Long.parseLong(tsValue3.getValue()), instanceOf(Long.class)); int expectedMax = 50; int expectedMin = 5; - Assert.assertTrue(expectedMax >= Long.parseLong(tsValue1.getValue())); - Assert.assertTrue(expectedMin <= Long.parseLong(tsValue1.getValue())); + Assert.assertTrue(expectedMax >= Long.parseLong(tsValue3.getValue())); + Assert.assertTrue(expectedMin <= Long.parseLong(tsValue3.getValue())); } else { - String pattern = "MMM d, yyyy HH:mm a"; - TbDate d = new TbDate(tsValue2.getValue(), pattern, "en-US"); - Assert.assertNotNull(d); + assertNotNull(value); + } + + String expectedReadAll; + String actualResultReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null); + if (varTest == 0) { + expectedReadAll = "[\"CompositeObservation: [/5/0/9, /3/0/9, /5/0/5, /5/0/6, /5/0/7, /5/0/3]\"]"; + assertEquals(expectedReadAll, actualResultReadAll); + updateProfile(deviceProfile, TELEMETRY_WITH_COMPOSITE_ALL_OBSERVE_ID_3_ID_19, COMPOSITE_ALL); + awaitObserveReadAll(1, lwM2MTestClient.getDeviceIdStr()); + expectedReadAll = "[\"CompositeObservation: [/19/0/2, /3/0/20, /3/0/9]\"]"; + actualResultReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null); + assertEquals(expectedReadAll, actualResultReadAll); + } else if (varTest == 1) { + String expectedReadAll3 = "\"CompositeObservation: [/3/0/9]\""; + String expectedReadAll5 = "\"CompositeObservation: [/5/0/9, /5/0/5, /5/0/6, /5/0/7, /5/0/3]\""; + assertTrue(actualResultReadAll.contains(expectedReadAll3)); + assertTrue(actualResultReadAll.contains(expectedReadAll5)); + updateProfile(deviceProfile, TELEMETRY_WITH_COMPOSITE_BY_OBJECT_OBSERVE_ID_3_ID_5_ID_19, COMPOSITE_BY_OBJECT); + awaitObserveReadAll(3, lwM2MTestClient.getDeviceIdStr()); + String expectedReadAll_3 = "\"CompositeObservation: [/3/0/20]\""; + String expectedReadAll_5 = "\"CompositeObservation: [/3/0/20]\""; + String expectedReadAll_19 = "\"CompositeObservation: [/19/0/2]\""; + actualResultReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null); + assertTrue(actualResultReadAll.contains(expectedReadAll_3)); + assertTrue(actualResultReadAll.contains(expectedReadAll_5)); + assertTrue(actualResultReadAll.contains(expectedReadAll_19)); + } else if (varTest == 2) { + String expectedReadAll3 = "\"CompositeObservation: [/3/0/9]\""; + String expectedReadAll5 = "\"CompositeObservation: [/5/0/9, /5/0/5, /5/0/6, /5/0/7, /5/0/3]\""; + assertTrue(actualResultReadAll.contains(expectedReadAll3)); + assertTrue(actualResultReadAll.contains(expectedReadAll5)); + updateProfile(deviceProfile, TELEMETRY_WITH_MANY_OBSERVE, SINGLE); + awaitObserveReadAll(2, lwM2MTestClient.getDeviceIdStr()); + String expectedReadAll_3_9 = "\"SingleObservation:/3/0/9\""; + String expectedReadAll_3_20 = "\"SingleObservation:/3/0/20\""; + actualResultReadAll = sendRpcObserveOkWithResultValue("ObserveReadAll", null); + assertTrue(actualResultReadAll.contains(expectedReadAll_3_9)); + assertTrue(actualResultReadAll.contains(expectedReadAll_3_20)); } } @@ -380,6 +506,14 @@ public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportInte return lwm2mDeviceProfile; } + protected void updateProfile(DeviceProfile deviceProfile, String telemetryObserve, TelemetryObserveStrategy telemetryObserveStrategy) throws Exception { + Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(telemetryObserve, getBootstrapServerCredentialsNoSec(NONE)); + transportConfiguration.getObserveAttr().setObserveStrategy(telemetryObserveStrategy); + DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/" + deviceProfile.getId().getId().toString(), DeviceProfile.class); + foundDeviceProfile.getProfileData().setTransportConfiguration(transportConfiguration); + doPost("/api/deviceProfile", foundDeviceProfile, DeviceProfile.class); + } + protected Device createLwm2mDevice(LwM2MDeviceCredentials credentials, String endpoint, DeviceProfileId deviceProfileId) throws Exception { Device device = new Device(); device.setName(endpoint); diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java index 2720c7784e..447cc051fc 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/SimpleLwM2MDevice.java @@ -257,7 +257,9 @@ public class SimpleLwM2MDevice extends BaseInstanceEnabler implements Destroyabl } private int getBatteryStatus() { - return RANDOM.nextInt(7); + int status = RANDOM.nextInt(7); + log.error("getBatteryStatus: [{}]", status); + return status; } private long getMemoryTotal() { diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java index e580827d51..a67f3cf00f 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/ota/AbstractOtaLwM2MIntegrationTest.java @@ -40,13 +40,15 @@ import java.util.UUID; import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.rest.client.utils.RestJsonConverter.toTimeseries; import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE; import static org.thingsboard.server.common.data.ota.OtaPackageType.SOFTWARE; -import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.RESOURCE_ID_11; -import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.*; +import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.OTA_INFO_19_FILE_CHECKSUM256; +import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.OTA_INFO_19_FILE_NAME; +import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.OTA_INFO_19_FILE_SIZE; +import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.OTA_INFO_19_TITLE; +import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaUpdateService.OTA_INFO_19_VERSION; @Slf4j @DaoSqlTest diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java index 2367670d5f..6dda2de9fe 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/AbstractSecurityLwM2MIntegrationTest.java @@ -473,8 +473,6 @@ public abstract class AbstractSecurityLwM2MIntegrationTest extends AbstractLwM2M protected String sendRPCSecurityExecuteById(String path, String deviceId, String endpoint) throws Exception { log.info("endpoint1: [{}]", endpoint); - - String setRpcRequest = "{\"method\": \"Execute\", \"params\": {\"id\": \"" + path + "\"}}"; return doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setRpcRequest, String.class, status().isOk()); } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java index 192dc6d1d0..d4a464b9c6 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/NoSecLwM2MIntegrationTest.java @@ -32,7 +32,7 @@ public class NoSecLwM2MIntegrationTest extends AbstractSecurityLwM2MIntegrationT public void testWithNoSecConnectLwm2mSuccessAndObserveTelemetry() throws Exception { String clientEndpoint = CLIENT_ENDPOINT_NO_SEC; LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); - super.basicTestConnectionObserveSingleTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, false); + super.basicTestConnectionObserveSingleTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, false, false); } // Bootstrap + Lwm2m diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/ObserveStrategyTransportConfigurationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/ObserveStrategyTransportConfigurationTest.java index c6a6bdad6d..18614bbe7c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/ObserveStrategyTransportConfigurationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/ObserveStrategyTransportConfigurationTest.java @@ -35,7 +35,7 @@ public class ObserveStrategyTransportConfigurationTest extends AbstractSecurityL @Test public void testTransportConfigurationObserveStrategyBeforeParseNotNullAfterParseNotNull_STRATEGY_COMPOSITE_ALL() throws Exception { - Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITH_COMPOSITE_OBSERVE, getBootstrapServerCredentialsNoSec(NONE)); + Lwm2mDeviceProfileTransportConfiguration transportConfiguration = getTransportConfiguration(TELEMETRY_WITH_COMPOSITE_ALL_OBSERVE_ID_3_ID_19, getBootstrapServerCredentialsNoSec(NONE)); Assert.assertNotNull(transportConfiguration.getObserveAttr().getObserveStrategy()); Assert.assertEquals(COMPOSITE_ALL, transportConfiguration.getObserveAttr().getObserveStrategy()); } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/ObserveStrategyWithNoSecQueueModeConnectTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/ObserveStrategyWithNoSecQueueModeConnectTest.java index 85b17a0f41..46e10ce81b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/ObserveStrategyWithNoSecQueueModeConnectTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/transportConfiguration/ObserveStrategyWithNoSecQueueModeConnectTest.java @@ -20,33 +20,44 @@ import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCr import org.thingsboard.server.common.data.device.profile.Lwm2mDeviceProfileTransportConfiguration; import org.thingsboard.server.transport.lwm2m.security.AbstractSecurityLwM2MIntegrationTest; +import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.COMPOSITE_ALL; import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.COMPOSITE_BY_OBJECT; import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfileBootstrapConfigType.NONE; public class ObserveStrategyWithNoSecQueueModeConnectTest extends AbstractSecurityLwM2MIntegrationTest { @Test - public void testWithNoSecQueueModeConnectLwm2mSuccessAndObserveSingleTelemetry() throws Exception { + public void testWithNoSecQueueModeConnectLwm2mSuccessAndObserveSingleTelemetryUpdateProfileAfterConnected() throws Exception { String clientEndpoint = CLIENT_ENDPOINT_NO_SEC + "_ObserveSingle"; LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); - super.basicTestConnectionObserveSingleTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, true); + super.basicTestConnectionObserveSingleTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, true, true); } @Test - public void testWithNoSecQueueModeConnectLwm2mSuccessAndObserveCompositeAllTelemetry() throws Exception { + public void testWithNoSecQueueModeConnectLwm2mSuccessAndObserveCompositeAllTelemetry_Both_UpdateProfileAfterConnected() throws Exception { String clientEndpoint = CLIENT_ENDPOINT_NO_SEC + "_ObserveCompositeAll"; LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); - Lwm2mDeviceProfileTransportConfiguration transportConfiguration = super.getTransportConfiguration(TELEMETRY_WITH_COMPOSITE_OBSERVE, getBootstrapServerCredentialsNoSec(NONE)); - super.basicTestConnectionObserveCompositeTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, transportConfiguration, 1); + Lwm2mDeviceProfileTransportConfiguration transportConfiguration = super.getTransportConfiguration(TELEMETRY_WITH_SINGLE_PARAMS_OBJECT_ID_5_ID_3, getBootstrapServerCredentialsNoSec(NONE)); + transportConfiguration.getObserveAttr().setObserveStrategy(COMPOSITE_ALL); + super.basicTestConnectionObserveCompositeTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, transportConfiguration, 1, 0); } @Test - public void testWithNoSecQueueModeConnectLwm2mSuccessAndObserveCompositeByObjectTelemetry() throws Exception { + public void testWithNoSecQueueModeConnectLwm2mSuccessAndObserveCompositeByObjectTelemetry_Both_UpdateProfileAfterConnected() throws Exception { String clientEndpoint = CLIENT_ENDPOINT_NO_SEC + "_ObserveCompositeByObject"; LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); - Lwm2mDeviceProfileTransportConfiguration transportConfiguration = super.getTransportConfiguration(TELEMETRY_WITH_COMPOSITE_OBSERVE, getBootstrapServerCredentialsNoSec(NONE)); + Lwm2mDeviceProfileTransportConfiguration transportConfiguration = super.getTransportConfiguration(TELEMETRY_WITH_SINGLE_PARAMS_OBJECT_ID_5_ID_3, getBootstrapServerCredentialsNoSec(NONE)); transportConfiguration.getObserveAttr().setObserveStrategy(COMPOSITE_BY_OBJECT); - super.basicTestConnectionObserveCompositeTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, transportConfiguration, 2); + super.basicTestConnectionObserveCompositeTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, transportConfiguration, 2, 1); + } + + @Test + public void testWithNoSecQueueModeConnectLwm2mSuccessAndObserveCompositeByObjectTelemetry_Single_UpdateProfileAfterConnected() throws Exception { + String clientEndpoint = CLIENT_ENDPOINT_NO_SEC + "_ObserveCompositeByObject_Single"; + LwM2MDeviceCredentials clientCredentials = getDeviceCredentialsNoSec(createNoSecClientCredentials(clientEndpoint)); + Lwm2mDeviceProfileTransportConfiguration transportConfiguration = super.getTransportConfiguration(TELEMETRY_WITH_SINGLE_PARAMS_OBJECT_ID_5_ID_3, getBootstrapServerCredentialsNoSec(NONE)); + transportConfiguration.getObserveAttr().setObserveStrategy(COMPOSITE_BY_OBJECT); + super.basicTestConnectionObserveCompositeTelemetry(SECURITY_NO_SEC, clientCredentials, clientEndpoint, transportConfiguration, 2, 2); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryObserveStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryObserveStrategy.java index 56d85c4b6f..264d883353 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryObserveStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryObserveStrategy.java @@ -49,7 +49,7 @@ public enum TelemetryObserveStrategy { return strategy; } } - return null; + throw new IllegalArgumentException("Unknown TelemetryObserveStrategy id: " + id); } @Override diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java index d2bf887932..98150c7bd4 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContext.java @@ -40,9 +40,6 @@ public interface LwM2mClientContext { Collection getLwM2mClients(); - //TODO: replace UUID with DeviceProfileId - Lwm2mDeviceProfileTransportConfiguration getProfile(UUID profileUuId); - Lwm2mDeviceProfileTransportConfiguration getProfile(Registration registration); Lwm2mDeviceProfileTransportConfiguration profileUpdate(DeviceProfile deviceProfile); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java index 6a4bc645d4..4cf8d825b9 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java @@ -288,7 +288,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { @Override public String getObjectIdByKeyNameFromProfile(LwM2mClient client, String keyName) { - Lwm2mDeviceProfileTransportConfiguration profile = getProfile(client.getProfileId()); + Lwm2mDeviceProfileTransportConfiguration profile = getProfile(client.getRegistration()); for (Map.Entry entry : profile.getObserveAttr().getKeyName().entrySet()) { String k = entry.getKey(); String v = entry.getValue(); @@ -346,7 +346,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { private PowerMode getPowerMode(LwM2mClient lwM2MClient) { PowerMode powerMode = lwM2MClient.getPowerMode(); if (powerMode == null) { - Lwm2mDeviceProfileTransportConfiguration deviceProfile = getProfile(lwM2MClient.getProfileId()); + Lwm2mDeviceProfileTransportConfiguration deviceProfile = getProfile(lwM2MClient.getRegistration()); powerMode = deviceProfile.getClientLwM2mSettings().getPowerMode(); } return powerMode; @@ -357,11 +357,6 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { return lwM2mClientsByEndpoint.values(); } - @Override - public Lwm2mDeviceProfileTransportConfiguration getProfile(UUID profileId) { - return doGetAndCache(profileId); - } - @Override public Lwm2mDeviceProfileTransportConfiguration getProfile(Registration registration) { UUID profileId = getClientByEndpoint(registration.getEndpoint()).getProfileId(); @@ -412,7 +407,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { PowerMode powerMode = client.getPowerMode(); OtherConfiguration profileSettings = null; if (powerMode == null && client.getProfileId() != null) { - var clientProfile = getProfile(client.getProfileId()); + var clientProfile = getProfile(client.getRegistration()); profileSettings = clientProfile.getClientLwM2mSettings(); powerMode = profileSettings.getPowerMode(); } @@ -458,7 +453,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { PowerMode powerMode = client.getPowerMode(); OtherConfiguration profileSettings = null; if (powerMode == null && client.getProfileId() != null) { - var clientProfile = getProfile(client.getProfileId()); + var clientProfile = getProfile(client.getRegistration()); profileSettings = clientProfile.getClientLwM2mSettings(); powerMode = profileSettings.getPowerMode(); } @@ -514,7 +509,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { if (PowerMode.E_DRX.equals(client.getPowerMode()) && client.getEdrxCycle() != null) { timeout = client.getEdrxCycle(); } else { - var clientProfile = getProfile(client.getProfileId()); + var clientProfile = getProfile(client.getRegistration()); OtherConfiguration clientLwM2mSettings = clientProfile.getClientLwM2mSettings(); if (PowerMode.E_DRX.equals(clientLwM2mSettings.getPowerMode())) { timeout = clientLwM2mSettings.getEdrxCycle(); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultUpdateResource.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultUpdateResource.java index 9a6c179eea..ffbf4595fb 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultUpdateResource.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultUpdateResource.java @@ -1,3 +1,18 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.thingsboard.server.transport.lwm2m.server.client; import lombok.AllArgsConstructor; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfig.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfig.java index 29fb5c86a9..ab1a47312b 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfig.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfig.java @@ -21,13 +21,17 @@ import lombok.NoArgsConstructor; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.device.profile.lwm2m.ObjectAttributes; +import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy; -import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.COMPOSITE_BY_OBJECT; +import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.SINGLE; import static org.thingsboard.server.common.data.util.CollectionsUtil.diffSets; +import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.areArraysStringEqual; +import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.deepCopyConcurrentMap; @Data @NoArgsConstructor @@ -39,18 +43,42 @@ public class LwM2MModelConfig { private Set attributesToRemove; private Set toObserve; private Set toCancelObserve; + private Map toObserveByObject; + private Map toObserveByObjectToCancel; private Set toRead; + private TelemetryObserveStrategy observeStrategyOld; + private TelemetryObserveStrategy observeStrategyNew; @JsonIgnore private Set toCancelRead; + public LwM2MModelConfig(String endpoint, Map attributesToAdd, Set attributesToRemove, Set toObserve, + Set toCancelObserve, Map toObserveByObject, Map toObserveByObjectToCancel, + Set toRead, TelemetryObserveStrategy observeStrategyOld, TelemetryObserveStrategy observeStrategyNew) { + this.endpoint = endpoint; + this.attributesToAdd = attributesToAdd; + this.attributesToRemove = attributesToRemove; + this.toObserve = toObserve; + this.toCancelObserve = toCancelObserve; + this.toObserveByObject = toObserveByObject; + this.toObserveByObjectToCancel = toObserveByObjectToCancel; + this.toRead = toRead; + this.toCancelRead = ConcurrentHashMap.newKeySet(); + this.observeStrategyOld = observeStrategyOld; + this.observeStrategyNew = observeStrategyNew; + } + public LwM2MModelConfig(String endpoint) { this.endpoint = endpoint; this.attributesToAdd = new ConcurrentHashMap<>(); this.attributesToRemove = ConcurrentHashMap.newKeySet(); this.toObserve = ConcurrentHashMap.newKeySet(); this.toCancelObserve = ConcurrentHashMap.newKeySet(); + this.toObserveByObject = new ConcurrentHashMap<>(); + this.toObserveByObjectToCancel = new ConcurrentHashMap<>(); this.toRead = ConcurrentHashMap.newKeySet(); - this.toCancelRead = new HashSet<>(); + this.toCancelRead = ConcurrentHashMap.newKeySet(); + this.observeStrategyOld = SINGLE; + this.observeStrategyNew = SINGLE; } public void merge(LwM2MModelConfig modelConfig) { @@ -83,10 +111,41 @@ public class LwM2MModelConfig { this.toRead.removeAll(modelConfig.getToObserve()); this.toRead.removeAll(modelConfig.getToCancelRead()); this.toRead.addAll(modelConfig.getToRead()); + + if (COMPOSITE_BY_OBJECT.equals(this.observeStrategyNew) + && COMPOSITE_BY_OBJECT.equals(modelConfig.getObserveStrategyNew()) + && COMPOSITE_BY_OBJECT.equals(modelConfig.getObserveStrategyOld())) { + + modelConfig.getToObserveByObjectToCancel().forEach((key, value) -> + this.toObserveByObjectToCancel.putIfAbsent(key, value.clone()) + ); + Map toObserveByObjectOld = deepCopyConcurrentMap(this.toObserveByObject); + this.toObserveByObject = new ConcurrentHashMap<>(); + for (Map.Entry entry : modelConfig.getToObserveByObject().entrySet()) { + Integer key = entry.getKey(); + String[] newValue = entry.getValue(); + if (toObserveByObjectOld.containsKey(key)) { + String[] oldValue = toObserveByObjectOld.get(key); + if (!areArraysStringEqual(oldValue, newValue)) { + this.toObserveByObjectToCancel.putIfAbsent(key, oldValue.clone()); + this.toObserveByObject.put(key, newValue); + } + } else { + this.toObserveByObject.put(key, newValue); + } + } + } else { + this.toObserveByObject = new ConcurrentHashMap<>(); + this.toObserveByObjectToCancel = new ConcurrentHashMap<>(); + } } @JsonIgnore public boolean isEmpty() { - return attributesToAdd.isEmpty() && toObserve.isEmpty() && toCancelObserve.isEmpty() && toRead.isEmpty(); + return attributesToAdd.isEmpty() && toObserve.isEmpty() && toCancelObserve.isEmpty() && toRead.isEmpty() && this.isEmptyByObject(); + } + @JsonIgnore + private boolean isEmptyByObject() { + return !this.observeStrategyOld.equals(this.observeStrategyNew) || (COMPOSITE_BY_OBJECT.equals(this.observeStrategyOld) && toObserveByObject.isEmpty() && toObserveByObjectToCancel.isEmpty()); } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImpl.java index 8204ba4715..ab8db42e6d 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/LwM2MModelConfigServiceImpl.java @@ -27,6 +27,8 @@ import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; import org.thingsboard.server.transport.lwm2m.server.downlink.DownlinkRequestCallback; import org.thingsboard.server.transport.lwm2m.server.downlink.LwM2mDownlinkMsgHandler; +import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MCancelAllObserveCallback; +import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MCancelAllRequest; import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MCancelObserveCallback; import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MCancelObserveRequest; import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MObserveCallback; @@ -35,6 +37,10 @@ import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MReadCallbac import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MReadRequest; import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteAttributesCallback; import org.thingsboard.server.transport.lwm2m.server.downlink.TbLwM2MWriteAttributesRequest; +import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MCancelObserveCompositeCallback; +import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MCancelObserveCompositeRequest; +import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MObserveCompositeCallback; +import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MObserveCompositeRequest; import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService; import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MModelConfigStore; import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; @@ -42,10 +48,15 @@ import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandle import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; +import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.COMPOSITE_BY_OBJECT; +import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.SINGLE; +import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.deepCopyConcurrentMap; + @Slf4j @Service @TbLwM2mTransportComponent @@ -92,6 +103,7 @@ public class LwM2MModelConfigServiceImpl implements LwM2MModelConfigService { LwM2MModelConfig modelConfig = currentModelConfigs.get(endpoint); if (modelConfig == null || modelConfig.isEmpty()) { modelConfig = newModelConfig; + log.warn("sendUpdates: [{}]", modelConfig); currentModelConfigs.put(endpoint, modelConfig); } else { modelConfig.merge(newModelConfig); @@ -153,33 +165,104 @@ public class LwM2MModelConfigServiceImpl implements LwM2MModelConfigService { ); }); - Set toObserve = modelConfig.getToObserve(); - toObserve.forEach(id -> { - TbLwM2MObserveRequest request = TbLwM2MObserveRequest.builder().versionedId(id) + // update observe + if (!modelConfig.getObserveStrategyOld().equals(modelConfig.getObserveStrategyNew()) + || !modelConfig.getToCancelObserve().isEmpty() || !modelConfig.getToObserve().isEmpty()) { + this.doSendCancelObserve(lwM2mClient, modelConfig); + } + } + + private void doSendCancelObserve(LwM2mClient lwM2mClient, LwM2MModelConfig modelConfig) { + String endpoint = lwM2mClient.getEndpoint(); + if (SINGLE.equals(modelConfig.getObserveStrategyOld()) && SINGLE.equals(modelConfig.getObserveStrategyNew())) { + Set toCancelObserveClone = ConcurrentHashMap.newKeySet(); + toCancelObserveClone.addAll(modelConfig.getToCancelObserve()); + toCancelObserveClone.forEach(id -> { + TbLwM2MCancelObserveRequest request = TbLwM2MCancelObserveRequest.builder().versionedId(id) + .timeout(clientContext.getRequestTimeout(lwM2mClient)).build(); + downlinkMsgHandler.sendCancelObserveRequest(lwM2mClient, request, + createDownlinkProxyCallback(() -> { + modelConfig.getToCancelObserve().remove(id); + if (modelConfig.isEmpty()) { + modelStore.remove(endpoint); + } + }, new TbLwM2MCancelObserveCallback(logService, lwM2mClient, id)) + ); + }); + this.doSendObserve(lwM2mClient, modelConfig); + } else if (COMPOSITE_BY_OBJECT.equals(modelConfig.getObserveStrategyOld()) && COMPOSITE_BY_OBJECT.equals(modelConfig.getObserveStrategyNew())) { + Map toObserveByObjectToCancelClone = deepCopyConcurrentMap(modelConfig.getToObserveByObjectToCancel()); + toObserveByObjectToCancelClone.forEach((key, ersionedIds) -> { + TbLwM2MCancelObserveCompositeRequest request = TbLwM2MCancelObserveCompositeRequest.builder().versionedIds(ersionedIds) + .timeout(clientContext.getRequestTimeout(lwM2mClient)).build(); + downlinkMsgHandler.sendCancelObserveCompositeRequest(lwM2mClient, request, + createDownlinkProxyCallback(() -> { + modelConfig.getToObserveByObjectToCancel().remove(key); + if (modelConfig.isEmpty()) { + modelStore.remove(endpoint); + } + }, new TbLwM2MCancelObserveCompositeCallback(logService, lwM2mClient, ersionedIds)) + ); + }); + this.doSendObserve(lwM2mClient, modelConfig); + } else { + // cancelAll - response - ok + TbLwM2MCancelAllRequest request = TbLwM2MCancelAllRequest.builder() .timeout(clientContext.getRequestTimeout(lwM2mClient)).build(); - downlinkMsgHandler.sendObserveRequest(lwM2mClient, request, + downlinkMsgHandler.sendCancelObserveAllRequest(lwM2mClient, request, createDownlinkProxyCallback(() -> { - toObserve.remove(id); - if (modelConfig.isEmpty()) { - modelStore.remove(endpoint); - } - }, new TbLwM2MObserveCallback(uplinkMsgHandler, logService, lwM2mClient, id)) + modelConfig.getToCancelObserve().clear(); + modelStore.remove(endpoint); + this.doSendObserve(lwM2mClient, modelConfig); + }, new TbLwM2MCancelAllObserveCallback(logService, lwM2mClient)) ); - }); + } + } - Set toCancelObserve = modelConfig.getToCancelObserve(); - toCancelObserve.forEach(id -> { - TbLwM2MCancelObserveRequest request = TbLwM2MCancelObserveRequest.builder().versionedId(id) + private void doSendObserve (LwM2mClient lwM2mClient, LwM2MModelConfig modelConfig) { + String endpoint = lwM2mClient.getEndpoint(); + if (SINGLE.equals(modelConfig.getObserveStrategyNew())) { + Set toObserveClone = ConcurrentHashMap.newKeySet(); + toObserveClone.addAll(modelConfig.getToObserve()); + toObserveClone.forEach(id -> { + TbLwM2MObserveRequest request = TbLwM2MObserveRequest.builder().versionedId(id) + .timeout(clientContext.getRequestTimeout(lwM2mClient)).build(); + downlinkMsgHandler.sendObserveRequest(lwM2mClient, request, + createDownlinkProxyCallback(() -> { + modelConfig.getToObserve().remove(id); + if (modelConfig.isEmpty()) { + modelStore.remove(endpoint); + } + }, new TbLwM2MObserveCallback(uplinkMsgHandler, logService, lwM2mClient, id)) + ); + }); + } else if (COMPOSITE_BY_OBJECT.equals(modelConfig.getObserveStrategyNew())){ + Map toObserveByObjectClone = deepCopyConcurrentMap(modelConfig.getToObserveByObject()); + toObserveByObjectClone.forEach((key, ersionedIds) -> { + TbLwM2MObserveCompositeRequest request = TbLwM2MObserveCompositeRequest.builder().versionedIds(ersionedIds) + .timeout(clientContext.getRequestTimeout(lwM2mClient)).build(); + downlinkMsgHandler.sendObserveCompositeRequest(lwM2mClient, request, + createDownlinkProxyCallback(() -> { + modelConfig.getToObserveByObject().remove(key); + if (modelConfig.isEmpty()) { + modelStore.remove(endpoint); + } + }, new TbLwM2MObserveCompositeCallback(uplinkMsgHandler, logService, lwM2mClient, ersionedIds)) + ); + }); + } else { // COMPOSITE_ALL + String [] versionedIds = modelConfig.getToObserve().toArray(new String[0]); + TbLwM2MObserveCompositeRequest request = TbLwM2MObserveCompositeRequest.builder().versionedIds(versionedIds) .timeout(clientContext.getRequestTimeout(lwM2mClient)).build(); - downlinkMsgHandler.sendCancelObserveRequest(lwM2mClient, request, + downlinkMsgHandler.sendObserveCompositeRequest(lwM2mClient, request, createDownlinkProxyCallback(() -> { - toCancelObserve.remove(id); + modelConfig.getToObserve().clear(); if (modelConfig.isEmpty()) { modelStore.remove(endpoint); } - }, new TbLwM2MCancelObserveCallback(logService, lwM2mClient, id)) + }, new TbLwM2MObserveCompositeCallback(uplinkMsgHandler, logService, lwM2mClient, versionedIds)) ); - }); + } } private DownlinkRequestCallback createDownlinkProxyCallback(Runnable processRemove, DownlinkRequestCallback callback) { @@ -222,6 +305,7 @@ public class LwM2MModelConfigServiceImpl implements LwM2MModelConfigService { @Override public void removeUpdates(String endpoint) { currentModelConfigs.remove(endpoint); + modelStore.remove(endpoint); } @PreDestroy diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ParametersAnalyzeResult.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/ParametersAnalyzeResult.java similarity index 94% rename from common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ParametersAnalyzeResult.java rename to common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/ParametersAnalyzeResult.java index 50f0e69d91..6131cdba98 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ParametersAnalyzeResult.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/ParametersAnalyzeResult.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.transport.lwm2m.server.client; +package org.thingsboard.server.transport.lwm2m.server.model; import lombok.Data; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/ParametersObserveAnalyzeResult.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/ParametersObserveAnalyzeResult.java new file mode 100644 index 0000000000..a8373ac378 --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/ParametersObserveAnalyzeResult.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server.model; + +import lombok.Data; +import org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.SINGLE; + +@Data +public class ParametersObserveAnalyzeResult { + Set observeSingleToCancel = ConcurrentHashMap.newKeySet(); + Set observeSingleToNew = ConcurrentHashMap.newKeySet(); + Map observeByObjectToNew = new ConcurrentHashMap<>();; + Map observeByObjectToCancel = new ConcurrentHashMap<>();; + TelemetryObserveStrategy observeStrategyOld = SINGLE; + TelemetryObserveStrategy observeStrategyNew = SINGLE; + + public ParametersObserveAnalyzeResult(Set observeSingleToCancel, Set observeSingleToNew, TelemetryObserveStrategy observeStrategyOld, TelemetryObserveStrategy observeStrategyNew){ + this.observeSingleToCancel = observeSingleToCancel; + this.observeSingleToNew = observeSingleToNew; + this.observeStrategyOld = observeStrategyOld; + this.observeStrategyNew = observeStrategyNew; + } + + public ParametersObserveAnalyzeResult(){} +} diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/ParametersUpdateAnalyzeResult.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/ParametersUpdateAnalyzeResult.java new file mode 100644 index 0000000000..b53029a52b --- /dev/null +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/model/ParametersUpdateAnalyzeResult.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.lwm2m.server.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import org.thingsboard.server.common.data.device.profile.lwm2m.ObjectAttributes; + +import java.util.Map; +import java.util.Set; + +@Data +@AllArgsConstructor +public class ParametersUpdateAnalyzeResult { + ParametersAnalyzeResult analyzerParameters; + Set newObjectsToRead; + Set newObjectsToCancelRead; + Map attributeLwm2mNew; +} + diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java index ea8c2acb14..79f077a71e 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java @@ -200,7 +200,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl attributesToFetch.add(SOFTWARE_URL); } - var clientSettings = clientContext.getProfile(client.getProfileId()).getClientLwM2mSettings(); + var clientSettings = clientContext.getProfile(client.getRegistration()).getClientLwM2mSettings(); initFwStrategy(client, clientSettings); initSwStrategy(client, clientSettings); @@ -528,7 +528,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl } else { strategy = info.getDeliveryMethod() == FirmwareDeliveryMethod.PULL.code ? LwM2MFirmwareUpdateStrategy.OBJ_5_TEMP_URL : LwM2MFirmwareUpdateStrategy.OBJ_5_BINARY; } - Boolean useObject19ForOtaInfo = clientContext.getProfile(client.getProfileId()).getClientLwM2mSettings().getUseObject19ForOtaInfo(); + Boolean useObject19ForOtaInfo = clientContext.getProfile(client.getRegistration()).getClientLwM2mSettings().getUseObject19ForOtaInfo(); if (useObject19ForOtaInfo != null && useObject19ForOtaInfo){ sendInfoToObject19ForOta(client, FW_INFO_19_INSTANCE_ID, response, otaPackageId); } @@ -554,7 +554,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl if (TransportProtos.ResponseStatus.SUCCESS.equals(response.getResponseStatus())) { UUID otaPackageId = new UUID(response.getOtaPackageIdMSB(), response.getOtaPackageIdLSB()); LwM2MSoftwareUpdateStrategy strategy = info.getStrategy(); - Boolean useObject19ForOtaInfo = clientContext.getProfile(client.getProfileId()).getClientLwM2mSettings().getUseObject19ForOtaInfo(); + Boolean useObject19ForOtaInfo = clientContext.getProfile(client.getRegistration()).getClientLwM2mSettings().getUseObject19ForOtaInfo(); if (useObject19ForOtaInfo != null && useObject19ForOtaInfo){ sendInfoToObject19ForOta(client, SW_INFO_19_INSTANCE_ID, response, otaPackageId); } @@ -629,7 +629,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl return this.fwStates.computeIfAbsent(client.getEndpoint(), endpoint -> { LwM2MClientFwOtaInfo info = otaInfoStore.getFw(endpoint); if (info == null) { - var profile = clientContext.getProfile(client.getProfileId()); + var profile = clientContext.getProfile(client.getRegistration()); info = new LwM2MClientFwOtaInfo(endpoint, profile.getClientLwM2mSettings().getFwUpdateResource(), LwM2MFirmwareUpdateStrategy.fromStrategyFwByCode(profile.getClientLwM2mSettings().getFwUpdateStrategy())); update(info); @@ -642,7 +642,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl return this.swStates.computeIfAbsent(client.getEndpoint(), endpoint -> { LwM2MClientSwOtaInfo info = otaInfoStore.getSw(endpoint); if (info == null) { - var profile = clientContext.getProfile(client.getProfileId()); + var profile = clientContext.getProfile(client.getRegistration()); info = new LwM2MClientSwOtaInfo(endpoint, profile.getClientLwM2mSettings().getSwUpdateResource(), LwM2MSoftwareUpdateStrategy.fromStrategySwByCode(profile.getClientLwM2mSettings().getSwUpdateStrategy())); update(info); diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java index 8ac8296c38..5897533539 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java @@ -78,7 +78,6 @@ import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientState; import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientStateException; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; -import org.thingsboard.server.transport.lwm2m.server.client.ParametersAnalyzeResult; import org.thingsboard.server.transport.lwm2m.server.client.ResultUpdateResource; import org.thingsboard.server.transport.lwm2m.server.client.ResultsAddKeyValueProto; import org.thingsboard.server.transport.lwm2m.server.common.LwM2MExecutorAwareService; @@ -98,6 +97,9 @@ import org.thingsboard.server.transport.lwm2m.server.downlink.composite.TbLwM2MO import org.thingsboard.server.transport.lwm2m.server.log.LwM2MTelemetryLogService; import org.thingsboard.server.transport.lwm2m.server.model.LwM2MModelConfig; import org.thingsboard.server.transport.lwm2m.server.model.LwM2MModelConfigService; +import org.thingsboard.server.transport.lwm2m.server.model.ParametersAnalyzeResult; +import org.thingsboard.server.transport.lwm2m.server.model.ParametersObserveAnalyzeResult; +import org.thingsboard.server.transport.lwm2m.server.model.ParametersUpdateAnalyzeResult; import org.thingsboard.server.transport.lwm2m.server.ota.LwM2MOtaUpdateService; import org.thingsboard.server.transport.lwm2m.server.session.LwM2MSessionManager; import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MDtlsSessionStore; @@ -117,6 +119,7 @@ import java.util.Optional; import java.util.Random; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -140,9 +143,11 @@ import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaU import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.LOG_LWM2M_ERROR; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.LOG_LWM2M_INFO; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.LOG_LWM2M_WARN; +import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.areArraysStringEqual; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertObjectIdToVersionedId; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.convertOtaUpdateValueToString; import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fromVersionedIdToObjectId; +import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.groupByObjectIdVersionedIds; @Slf4j @@ -403,14 +408,15 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl @Override public void onDeviceProfileUpdate(SessionInfoProto sessionInfo, DeviceProfile deviceProfile) { try { + List clients = clientContext.getLwM2mClients() .stream().filter(e -> e.getProfileId() != null) .filter(e -> e.getProfileId().equals(deviceProfile.getUuidId())).collect(Collectors.toList()); clients.forEach(client -> { client.onDeviceProfileUpdate(deviceProfile); }); - if (clients.size() > 0) { - var oldProfile = clientContext.getProfile(deviceProfile.getUuidId()); + if (!clients.isEmpty()) { + var oldProfile = clientContext.getProfile(clients.get(0).getRegistration()); this.onDeviceProfileUpdate(clients, oldProfile, deviceProfile); } } catch (Exception e) { @@ -476,7 +482,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl * @param lwM2MClient - object with All parameters off client */ private void initClientTelemetry(LwM2mClient lwM2MClient) { - Lwm2mDeviceProfileTransportConfiguration profile = clientContext.getProfile(lwM2MClient.getProfileId()); + Lwm2mDeviceProfileTransportConfiguration profile = clientContext.getProfile(lwM2MClient.getRegistration()); Set supportedObjects = clientContext.getSupportedIdVerInClient(lwM2MClient); if (supportedObjects != null && supportedObjects.size() > 0) { this.sendReadRequests(lwM2MClient, profile, supportedObjects); @@ -690,7 +696,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl } private void onDeviceUpdate(LwM2mClient lwM2MClient, Device device, Optional deviceProfileOpt) { - var oldProfile = clientContext.getProfile(lwM2MClient.getProfileId()); + var oldProfile = clientContext.getProfile(lwM2MClient.getRegistration()); deviceProfileOpt.ifPresent(deviceProfile -> this.onDeviceProfileUpdate(Collections.singletonList(lwM2MClient), oldProfile, deviceProfile)); lwM2MClient.onDeviceUpdate(device, deviceProfileOpt); } @@ -774,7 +780,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl private TransportProtos.KeyValueProto getKvToThingsBoard(String pathIdVer, Registration registration) { LwM2mClient lwM2MClient = this.clientContext.getClientByEndpoint(registration.getEndpoint()); - Map names = clientContext.getProfile(lwM2MClient.getProfileId()).getObserveAttr().getKeyName(); + Map names = clientContext.getProfile(lwM2MClient.getRegistration()).getObserveAttr().getKeyName(); if (names != null && names.containsKey(pathIdVer)) { String resourceName = names.get(pathIdVer); if (resourceName != null && !resourceName.isEmpty()) { @@ -861,80 +867,49 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl this.updateAttrTelemetry(updateResource, null); } - //TODO: review and optimize the logic to minimize number of the requests to device. - private void onDeviceProfileUpdate(List clients, Lwm2mDeviceProfileTransportConfiguration oldProfile, DeviceProfile deviceProfile) { + private void onDeviceProfileUpdate(List clients, Lwm2mDeviceProfileTransportConfiguration oldProfileTransportConfiguration, DeviceProfile deviceProfile) { if (clientContext.profileUpdate(deviceProfile) != null) { - TelemetryMappingConfiguration oldTelemetryParams = oldProfile.getObserveAttr(); - Set attributeSetOld = oldTelemetryParams.getAttribute(); - Set telemetrySetOld = oldTelemetryParams.getTelemetry(); - Set observeOld = oldTelemetryParams.getObserve(); - Map keyNameOld = oldTelemetryParams.getKeyName(); - Map attributeLwm2mOld = oldTelemetryParams.getAttributeLwm2m(); - - var newProfile = clientContext.getProfile(deviceProfile.getUuidId()); - TelemetryMappingConfiguration newTelemetryParams = newProfile.getObserveAttr(); - Set attributeSetNew = newTelemetryParams.getAttribute(); - Set telemetrySetNew = newTelemetryParams.getTelemetry(); - Set observeNew = newTelemetryParams.getObserve(); - Map keyNameNew = newTelemetryParams.getKeyName(); - Map attributeLwm2mNew = newTelemetryParams.getAttributeLwm2m(); - - Set observeToAdd = diffSets(observeOld, observeNew); - Set observeToRemove = diffSets(observeNew, observeOld); - - Set newObjectsToRead = new HashSet<>(); - Set newObjectsToCancelRead = new HashSet<>(); + var newProfileTransportConfiguration = clientContext.getProfile(clients.get(0).getRegistration()); + ParametersUpdateAnalyzeResult parametersUpdate = getParametersUpdate(oldProfileTransportConfiguration, newProfileTransportConfiguration); + ParametersObserveAnalyzeResult parametersObserve = getParametersObserve(oldProfileTransportConfiguration.getObserveAttr(), newProfileTransportConfiguration.getObserveAttr(), deviceProfile.getId().getId()); + compareAndSetWriteAttributesObservations(clients, parametersUpdate, parametersObserve); + updateValueOta(clients, newProfileTransportConfiguration, oldProfileTransportConfiguration); + } + } - if (!attributeSetOld.equals(attributeSetNew)) { - newObjectsToRead.addAll(diffSets(attributeSetOld, attributeSetNew)); - newObjectsToCancelRead.addAll(diffSets(attributeSetNew, attributeSetOld)); + private ParametersUpdateAnalyzeResult getParametersUpdate(Lwm2mDeviceProfileTransportConfiguration oldProfile, Lwm2mDeviceProfileTransportConfiguration newProfile){ + TelemetryMappingConfiguration newTelemetryParams = newProfile.getObserveAttr(); + Map keyNameNew = newTelemetryParams.getKeyName(); + Map attributeLwm2mNew = newTelemetryParams.getAttributeLwm2m(); + Set attributeSetNew = newTelemetryParams.getAttribute(); + Set telemetrySetNew = newTelemetryParams.getTelemetry(); - } - if (!telemetrySetOld.equals(telemetrySetNew)) { - newObjectsToRead.addAll(diffSets(telemetrySetOld, telemetrySetNew)); - newObjectsToCancelRead.addAll(diffSets(telemetrySetNew, telemetrySetOld)); - } - if (!keyNameOld.equals(keyNameNew)) { - ParametersAnalyzeResult keyNameChange = this.getAnalyzerKeyName(keyNameOld, keyNameNew); - newObjectsToRead.addAll(keyNameChange.getPathPostParametersAdd()); - } + TelemetryMappingConfiguration oldTelemetryParams = oldProfile.getObserveAttr(); + Map keyNameOld = oldTelemetryParams.getKeyName(); + Map attributeLwm2mOld = oldTelemetryParams.getAttributeLwm2m(); + ParametersAnalyzeResult analyzerParameters = getAttributesAnalyzer(attributeLwm2mOld, attributeLwm2mNew); - ParametersAnalyzeResult analyzerParameters = getAttributesAnalyzer(attributeLwm2mOld, attributeLwm2mNew); + // analyze Read + Set newObjectsToRead = new HashSet<>(); + Set newObjectsToCancelRead = new HashSet<>(); - clients.forEach(client -> { - LwM2MModelConfig modelConfig = new LwM2MModelConfig(client.getEndpoint()); - modelConfig.getToRead().addAll(diffSets(observeToAdd, newObjectsToRead)); - modelConfig.getToCancelRead().addAll(newObjectsToCancelRead); - modelConfig.getToCancelObserve().addAll(observeToRemove); - modelConfig.getToObserve().addAll(observeToAdd); - - Set clientObjects = clientContext.getSupportedIdVerInClient(client); - Set pathToAdd = analyzerParameters.getPathPostParametersAdd().stream().filter(target -> clientObjects.contains("/" + target.split(LWM2M_SEPARATOR_PATH)[1])) - .collect(Collectors.toUnmodifiableSet()); - modelConfig.getAttributesToAdd().putAll(pathToAdd.stream().collect(Collectors.toMap(t -> t, attributeLwm2mNew::get))); - - Set pathToRemove = analyzerParameters.getPathPostParametersDel().stream().filter(target -> clientObjects.contains("/" + target.split(LWM2M_SEPARATOR_PATH)[1])) - .collect(Collectors.toUnmodifiableSet()); - modelConfig.getAttributesToRemove().addAll(pathToRemove); - - modelConfigService.sendUpdates(client, modelConfig); - }); + Set attributeSetOld = oldTelemetryParams.getAttribute(); + Set telemetrySetOld = oldTelemetryParams.getTelemetry(); - // update value in fwInfo - OtherConfiguration newLwM2mSettings = newProfile.getClientLwM2mSettings(); - OtherConfiguration oldLwM2mSettings = oldProfile.getClientLwM2mSettings(); - if (!newLwM2mSettings.getFwUpdateStrategy().equals(oldLwM2mSettings.getFwUpdateStrategy()) - || (StringUtils.isNotEmpty(newLwM2mSettings.getFwUpdateResource()) && - !newLwM2mSettings.getFwUpdateResource().equals(oldLwM2mSettings.getFwUpdateResource()))) { - clients.forEach(lwM2MClient -> otaService.onFirmwareStrategyUpdate(lwM2MClient, newLwM2mSettings)); - } + if (!attributeSetOld.equals(attributeSetNew)) { + newObjectsToRead.addAll(diffSets(attributeSetOld, attributeSetNew)); + newObjectsToCancelRead.addAll(diffSets(attributeSetNew, attributeSetOld)); - if (!newLwM2mSettings.getSwUpdateStrategy().equals(oldLwM2mSettings.getSwUpdateStrategy()) - || (StringUtils.isNotEmpty(newLwM2mSettings.getSwUpdateResource()) && - !newLwM2mSettings.getSwUpdateResource().equals(oldLwM2mSettings.getSwUpdateResource()))) { - clients.forEach(lwM2MClient -> otaService.onCurrentSoftwareStrategyUpdate(lwM2MClient, newLwM2mSettings)); - } } + if (!telemetrySetOld.equals(telemetrySetNew)) { + newObjectsToRead.addAll(diffSets(telemetrySetOld, telemetrySetNew)); + newObjectsToCancelRead.addAll(diffSets(telemetrySetNew, telemetrySetOld)); + } + if (!keyNameOld.equals(keyNameNew)) { + ParametersAnalyzeResult keyNameChange = this.getAnalyzerKeyName(keyNameOld, keyNameNew); + newObjectsToRead.addAll(keyNameChange.getPathPostParametersAdd()); + } + return new ParametersUpdateAnalyzeResult(analyzerParameters, newObjectsToRead, newObjectsToCancelRead, attributeLwm2mNew); } private ParametersAnalyzeResult getAnalyzerKeyName(Map keyNameOld, Map keyNameNew) { @@ -947,6 +922,55 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl return analyzerParameters; } + private ParametersObserveAnalyzeResult getParametersObserve(TelemetryMappingConfiguration oldTelemetryParams, TelemetryMappingConfiguration newTelemetryParams, UUID profileId){ + try { + TelemetryObserveStrategy observeStrategyOld = oldTelemetryParams.getObserveStrategy(); + TelemetryObserveStrategy observeStrategyNew = newTelemetryParams.getObserveStrategy(); + Set observeOld = oldTelemetryParams.getObserve(); + Set observeNew = newTelemetryParams.getObserve(); + Set observeSingleToNew = diffSets(observeOld, observeNew); + Set observeSingleToCancel = diffSets(observeNew, observeOld); + if (!observeSingleToNew.isEmpty() || !observeSingleToCancel.isEmpty()) { + ParametersObserveAnalyzeResult observeAnalyzeResult = new ParametersObserveAnalyzeResult(observeSingleToCancel, + observeSingleToNew, observeStrategyOld, observeStrategyNew); + if (SINGLE.equals(observeStrategyOld) && SINGLE.equals(observeStrategyNew)) { + return observeAnalyzeResult; + } else if (COMPOSITE_BY_OBJECT.equals(observeStrategyOld) && COMPOSITE_BY_OBJECT.equals(observeStrategyNew)) { + Map observeByObjectToCancel = new ConcurrentHashMap<>(); + Map observeByObjectToNew = new ConcurrentHashMap<>(); + Map observeByObjectOld = groupByObjectIdVersionedIds(observeOld); + Map observeByObjectNew = groupByObjectIdVersionedIds(observeNew); + for (Map.Entry entry : observeByObjectNew.entrySet()) { + Integer key = entry.getKey(); + String[] newValue = entry.getValue(); + if (observeByObjectOld.containsKey(key)) { + String[] oldValue = observeByObjectOld.get(key); + if (!areArraysStringEqual(oldValue, newValue)) { + observeByObjectToCancel.put(key, oldValue); + observeByObjectToNew.put(key, newValue); + } + } else { + observeByObjectToNew.put(key, newValue); + } + } + observeAnalyzeResult.setObserveByObjectToCancel(observeByObjectToCancel); + observeAnalyzeResult.setObserveByObjectToNew(observeByObjectToNew); + return observeAnalyzeResult; + } else { + // Observe Cancel All + observeAnalyzeResult.setObserveSingleToCancel(observeOld); + // Observe All new + observeAnalyzeResult.setObserveSingleToNew(observeNew); + return observeAnalyzeResult; + } + } + return new ParametersObserveAnalyzeResult(); + } catch (IllegalArgumentException e) { + log.error("Error lwm2m on Profile Update id: [{}]. Failed observe Strategy: [{}]", profileId, e.getMessage()); + return new ParametersObserveAnalyzeResult(); + } + } + private ParametersAnalyzeResult getAttributesAnalyzer(Map attributeLwm2mOld, Map attributeLwm2mNew) { ParametersAnalyzeResult analyzerParameters = new ParametersAnalyzeResult(); Set pathOld = attributeLwm2mOld.keySet(); @@ -963,8 +987,37 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl return analyzerParameters; } - private void compareAndSetWriteAttributes(LwM2mClient client, ParametersAnalyzeResult analyzerParameters, Map lwm2mAttributesNew, LwM2MModelConfig modelConfig) { + private void compareAndSetWriteAttributesObservations(List clients, ParametersUpdateAnalyzeResult parametersUpdate, ParametersObserveAnalyzeResult parametersObserve) { + clients.forEach(client -> { + Set clientObjects = clientContext.getSupportedIdVerInClient(client); + Set pathToAdd = parametersUpdate.getAnalyzerParameters().getPathPostParametersAdd().stream().filter(target -> clientObjects.contains("/" + target.split(LWM2M_SEPARATOR_PATH)[1])) + .collect(Collectors.toUnmodifiableSet()); + Map attributesToAdd = pathToAdd.stream().collect(Collectors.toMap(t -> t, parametersUpdate.getAttributeLwm2mNew()::get)); + Set attributesToRemove = parametersUpdate.getAnalyzerParameters().getPathPostParametersDel().stream().filter(target -> clientObjects.contains("/" + target.split(LWM2M_SEPARATOR_PATH)[1])) + .collect(Collectors.toUnmodifiableSet()); + Set toRead = diffSets(parametersObserve.getObserveSingleToNew(), parametersUpdate.getNewObjectsToRead()); + LwM2MModelConfig modelConfig = new LwM2MModelConfig(client.getEndpoint(), attributesToAdd, attributesToRemove, parametersObserve.getObserveSingleToNew(), + parametersObserve.getObserveSingleToCancel(), parametersObserve.getObserveByObjectToNew(), parametersObserve.getObserveByObjectToCancel(), + toRead, parametersObserve.getObserveStrategyOld(), parametersObserve.getObserveStrategyNew()); + modelConfig.getToCancelRead().addAll(parametersUpdate.getNewObjectsToCancelRead()); + modelConfigService.sendUpdates(client, modelConfig); + }); + } + private void updateValueOta(List clients, Lwm2mDeviceProfileTransportConfiguration oldProfile, Lwm2mDeviceProfileTransportConfiguration newProfile) { + OtherConfiguration newLwM2mSettings = newProfile.getClientLwM2mSettings(); + OtherConfiguration oldLwM2mSettings = oldProfile.getClientLwM2mSettings(); + if (!newLwM2mSettings.getFwUpdateStrategy().equals(oldLwM2mSettings.getFwUpdateStrategy()) + || (StringUtils.isNotEmpty(newLwM2mSettings.getFwUpdateResource()) && + !newLwM2mSettings.getFwUpdateResource().equals(oldLwM2mSettings.getFwUpdateResource()))) { + clients.forEach(lwM2MClient -> otaService.onFirmwareStrategyUpdate(lwM2MClient, newLwM2mSettings)); + } + + if (!newLwM2mSettings.getSwUpdateStrategy().equals(oldLwM2mSettings.getSwUpdateStrategy()) + || (StringUtils.isNotEmpty(newLwM2mSettings.getSwUpdateResource()) && + !newLwM2mSettings.getSwUpdateResource().equals(oldLwM2mSettings.getSwUpdateResource()))) { + clients.forEach(lwM2MClient -> otaService.onCurrentSoftwareStrategyUpdate(lwM2MClient, newLwM2mSettings)); + } } /** @@ -1041,7 +1094,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl } private Map getNamesFromProfileForSharedAttributes(LwM2mClient lwM2MClient) { - Lwm2mDeviceProfileTransportConfiguration profile = clientContext.getProfile(lwM2MClient.getProfileId()); + Lwm2mDeviceProfileTransportConfiguration profile = clientContext.getProfile(lwM2MClient.getRegistration()); return profile.getObserveAttr().getKeyName(); } @@ -1077,15 +1130,4 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl clientContext.update(lwM2MClient); } } - - private Map groupByObjectIdVersionedIds(Set targetIds){ - return targetIds.stream() - .collect(Collectors.groupingBy( - id -> new LwM2mPath(fromVersionedIdToObjectId(id)).getObjectId(), - Collectors.collectingAndThen( - Collectors.toList(), - list -> list.toArray(new String[0]) - ) - )); - } } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java index 3ee83766fe..160ca3d905 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java @@ -45,10 +45,14 @@ import org.thingsboard.server.transport.lwm2m.server.ota.firmware.FirmwareUpdate import org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateResult; import org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateState; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_LENGTH; import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CONNECTION_ID_NODE_ID; @@ -202,15 +206,15 @@ public class LwM2MTransportUtil { } public static Object getJsonPrimitiveValue(JsonPrimitive value) { - if(value.isString()) { + if (value.isString()) { return value.getAsString(); - } else if (value.isNumber()){ + } else if (value.isNumber()) { try { return Integer.valueOf(value.toString()); } catch (NumberFormatException i) { try { return Long.valueOf(value.toString()); - } catch (NumberFormatException l){ + } catch (NumberFormatException l) { if (value.getAsFloat() >= Float.MIN_VALUE && value.getAsFloat() <= Float.MAX_VALUE) { return value.getAsFloat(); } else { @@ -218,7 +222,7 @@ public class LwM2MTransportUtil { } } } - } else if (value.isBoolean()){ + } else if (value.isBoolean()) { return value.getAsBoolean(); } else { return null; @@ -241,7 +245,7 @@ public class LwM2MTransportUtil { if (value instanceof JsonElement) { return convertMultiResourceValuesFromJson((JsonElement) value, type, versionedId); } else if (value instanceof Map) { - JsonElement valueConvert = convertToJsonObject((Map) value); + JsonElement valueConvert = convertToJsonObject((Map) value); return convertMultiResourceValuesFromJson(valueConvert, type, versionedId); } else { return null; @@ -400,4 +404,56 @@ public class LwM2MTransportUtil { } return (int) (Math.log(size / 16) / Math.log(2)); } + + public static ConcurrentHashMap groupByObjectIdVersionedIds(Set targetIds) { + return targetIds.stream() + .collect(Collectors.groupingBy( + id -> new LwM2mPath(fromVersionedIdToObjectId(id)).getObjectId(), + ConcurrentHashMap::new, + Collectors.collectingAndThen( + Collectors.toList(), + list -> list.toArray(new String[0]) + ) + )); + } + + public static boolean areArraysStringEqual(String[] oldValue, String[] newValue) { + if (oldValue == null || newValue == null) return false; + if (oldValue.length != newValue.length) return false; + String[] sorted1 = oldValue.clone(); + String[] sorted2 = newValue.clone(); + Arrays.sort(sorted1); + Arrays.sort(sorted2); + return Arrays.equals(sorted1, sorted2); + } + + public static ConcurrentHashMap deepCopyConcurrentMap(Map original) { + return original.isEmpty() ? new ConcurrentHashMap<>() : original.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> entry.getValue() != null ? entry.getValue().clone() : null, + (v1, v2) -> v1, // merge function in case of duplicate keys + ConcurrentHashMap::new + )); + } + + public static boolean areMapsEqual(Map m1, Map m2) { + if (m1.size() != m2.size()) return false; + for (Integer key : m1.keySet()) { + if (!m2.containsKey(key)) return false; + + String[] arr1 = m1.get(key); + String[] arr2 = m2.get(key); + + if (arr1 == null || arr2 == null) { + if (arr1 != arr2) return false; + String[] sorted1 = arr1.clone(); + String[] sorted2 = arr2.clone(); + Arrays.sort(sorted1); + Arrays.sort(sorted2); + if (!Arrays.equals(sorted1, sorted2)) return false; + } + } + return true; + } } From 8b00da12c4ae5434d0b6c0033caf3b6ffb53e6b8 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Sat, 10 May 2025 12:16:25 +0200 Subject: [PATCH 49/63] tools cassandra-all bumped to v5 --- pom.xml | 7 +------ .../thingsboard/client/tools/migrator/WriterBuilder.java | 6 +++--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/pom.xml b/pom.xml index f95e9df9b1..e5d6c0e165 100755 --- a/pom.xml +++ b/pom.xml @@ -57,7 +57,7 @@ 0.10 4.17.0 4.2.25 - 3.11.17 + 5.0.4 33.1.0-jre 3.1.8 3.14.0 @@ -1827,11 +1827,6 @@ cassandra-all ${cassandra-all.version} - - org.apache.cassandra - cassandra-thrift - ${cassandra-all.version} - org.junit.vintage junit-vintage-engine diff --git a/tools/src/main/java/org/thingsboard/client/tools/migrator/WriterBuilder.java b/tools/src/main/java/org/thingsboard/client/tools/migrator/WriterBuilder.java index b7f8524031..80fdad3c1c 100644 --- a/tools/src/main/java/org/thingsboard/client/tools/migrator/WriterBuilder.java +++ b/tools/src/main/java/org/thingsboard/client/tools/migrator/WriterBuilder.java @@ -59,7 +59,7 @@ public class WriterBuilder { public static CQLSSTableWriter getTsWriter(File dir) { return CQLSSTableWriter.builder() - .inDirectory(dir) + .inDirectory(dir.getAbsolutePath()) .forTable(tsSchema) .using("INSERT INTO thingsboard.ts_kv_cf (entity_type, entity_id, key, partition, ts, bool_v, str_v, long_v, dbl_v, json_v) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") @@ -68,7 +68,7 @@ public class WriterBuilder { public static CQLSSTableWriter getLatestWriter(File dir) { return CQLSSTableWriter.builder() - .inDirectory(dir) + .inDirectory(dir.getAbsolutePath()) .forTable(latestSchema) .using("INSERT INTO thingsboard.ts_kv_latest_cf (entity_type, entity_id, key, ts, bool_v, str_v, long_v, dbl_v, json_v) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)") @@ -77,7 +77,7 @@ public class WriterBuilder { public static CQLSSTableWriter getPartitionWriter(File dir) { return CQLSSTableWriter.builder() - .inDirectory(dir) + .inDirectory(dir.getAbsolutePath()) .forTable(partitionSchema) .using("INSERT INTO thingsboard.ts_kv_partitions_cf (entity_type, entity_id, key, partition) " + "VALUES (?, ?, ?, ?)") From 5111a07a1154e92263e11138bab8078acd54a8c9 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Sat, 10 May 2025 12:23:05 +0200 Subject: [PATCH 50/63] addressing vulnerabilities on high and critical --- pom.xml | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index e5d6c0e165..cd2078e70a 100755 --- a/pom.xml +++ b/pom.xml @@ -42,13 +42,14 @@ 4.0.2 2.4.0-b180830.0359 4.0.5 - 10.1.39 + 10.1.40 + 2.5.2 3.2.12 3.2.12 3.2.12 6.1.15 6.2.11 - 6.2.8 + 6.3.8 5.1.5 0.12.5 2.0.13 @@ -102,7 +103,7 @@ 1.19.0 1.78.1 2.0.1 - 42.7.3 + 42.7.5 org/thingsboard/server/gen/**/*, org/thingsboard/server/extensions/core/plugin/telemetry/gen/**/* @@ -112,7 +113,7 @@ - 3.7.1 + 3.7.2 8.10.1 3.5.3 2.2 @@ -1163,6 +1164,13 @@ tomcat-embed-websocket ${tomcat.version} + + + net.minidev + json-smart + ${net.minidev.json-smart} + + org.springframework.boot spring-boot-starter @@ -1183,6 +1191,18 @@ spring-security-oauth2-jose ${spring-security.version} + + + org.springframework.security + spring-security-config + ${spring-security.version} + + + org.springframework.security + spring-security-web + ${spring-security.version} + + org.springframework spring-core From 9d72f5f4346d621ab4d8752b331e0990cb14eee2 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Sat, 10 May 2025 12:24:24 +0200 Subject: [PATCH 51/63] bump californium version from 2.6.1 to 2.7.4 to fix vulnerability and not break the monitoring module --- monitoring/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/pom.xml b/monitoring/pom.xml index 4f59cbc975..4ef6c775bf 100644 --- a/monitoring/pom.xml +++ b/monitoring/pom.xml @@ -42,7 +42,7 @@ ThingsBoard Monitoring Service org.thingsboard.monitoring.ThingsboardMonitoringApplication - 2.6.1 + 2.7.4 2.0.0-M4 From dec61b8ac28be647563f33ad2565bde0feef5039 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Sat, 10 May 2025 12:25:53 +0200 Subject: [PATCH 52/63] cassandra thrift removed after cassandra-all bump for module tools --- tools/pom.xml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tools/pom.xml b/tools/pom.xml index 0286c135e4..7e2c44ddd6 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -55,10 +55,6 @@ org.apache.cassandra cassandra-all - - org.apache.cassandra - cassandra-thrift - commons-io commons-io From a3c7084d7fc98492386be0217a5d7ff4c3204493 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Sat, 10 May 2025 14:24:04 +0200 Subject: [PATCH 53/63] refactored obsolete com.github.java-json-tools:json-schema-validator with actively updated com.networknt:json-schema-validator --- common/cluster-api/pom.xml | 4 - common/dao-api/pom.xml | 4 - dao/pom.xml | 4 + .../BaseComponentDescriptorService.java | 20 ++-- .../BaseComponentDescriptorServiceTest.java | 98 +++++++++++++++++++ pom.xml | 10 +- 6 files changed, 116 insertions(+), 24 deletions(-) create mode 100644 dao/src/test/java/org/thingsboard/server/dao/component/BaseComponentDescriptorServiceTest.java diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index ee9c1eae75..75048197d3 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -60,10 +60,6 @@ jakarta.annotation jakarta.annotation-api - - com.github.java-json-tools - json-schema-validator - org.slf4j slf4j-api diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index d7142e723d..d3027356c3 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -56,10 +56,6 @@ jakarta.annotation jakarta.annotation-api - - com.github.java-json-tools - json-schema-validator - org.slf4j slf4j-api diff --git a/dao/pom.xml b/dao/pom.xml index f203343c89..9618c38386 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -59,6 +59,10 @@ org.thingsboard.common util + + com.networknt + json-schema-validator + org.slf4j slf4j-api diff --git a/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java b/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java index 5acf1b0f97..2c5d6fb3c5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/component/BaseComponentDescriptorService.java @@ -16,10 +16,10 @@ package org.thingsboard.server.dao.component; import com.fasterxml.jackson.databind.JsonNode; -import com.github.fge.jsonschema.core.exceptions.ProcessingException; -import com.github.fge.jsonschema.core.report.ProcessingReport; -import com.github.fge.jsonschema.main.JsonSchemaFactory; -import com.github.fge.jsonschema.main.JsonValidator; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SpecVersion; +import com.networknt.schema.ValidationMessage; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -36,6 +36,7 @@ import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.Validator; import java.util.Optional; +import java.util.Set; /** * @author Andrew Shvayka @@ -89,15 +90,18 @@ public class BaseComponentDescriptorService implements ComponentDescriptorServic @Override public boolean validate(TenantId tenantId, ComponentDescriptor component, JsonNode configuration) { - JsonValidator validator = JsonSchemaFactory.byDefault().getValidator(); try { if (!component.getConfigurationDescriptor().has("schema")) { throw new DataValidationException("Configuration descriptor doesn't contain schema property!"); } JsonNode configurationSchema = component.getConfigurationDescriptor().get("schema"); - ProcessingReport report = validator.validate(configurationSchema, configuration); - return report.isSuccess(); - } catch (ProcessingException e) { + + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); + JsonSchema schema = factory.getSchema(configurationSchema); + + Set validationMessages = schema.validate(configuration); + return validationMessages.isEmpty(); + } catch (Exception e) { throw new IncorrectParameterException(e.getMessage(), e); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/component/BaseComponentDescriptorServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/component/BaseComponentDescriptorServiceTest.java new file mode 100644 index 0000000000..d76c11f708 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/component/BaseComponentDescriptorServiceTest.java @@ -0,0 +1,98 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.component; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.plugin.ComponentClusteringMode; +import org.thingsboard.server.common.data.plugin.ComponentDescriptor; +import org.thingsboard.server.common.data.plugin.ComponentScope; +import org.thingsboard.server.common.data.plugin.ComponentType; +import org.thingsboard.server.dao.exception.IncorrectParameterException; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class BaseComponentDescriptorServiceTest { + + private BaseComponentDescriptorService service; + private ComponentDescriptor componentDescriptor; + private TenantId tenantId; + + @BeforeEach + void setUp() { + service = Mockito.spy(BaseComponentDescriptorService.class); + tenantId = TenantId.SYS_TENANT_ID; + + // Create a simple component descriptor + componentDescriptor = new ComponentDescriptor(); + componentDescriptor.setType(ComponentType.ACTION); + componentDescriptor.setScope(ComponentScope.TENANT); + componentDescriptor.setClusteringMode(ComponentClusteringMode.ENABLED); + componentDescriptor.setName("Test Component"); + componentDescriptor.setClazz("org.thingsboard.test.TestComponent"); + + // Create configuration descriptor with schema from JSON string + String configDescriptorJson = """ + { + "schema": { + "type": "object", + "properties": { + "testField": { + "type": "string" + } + }, + "required": ["testField"] + } + }"""; + + componentDescriptor.setConfigurationDescriptor(JacksonUtil.toJsonNode(configDescriptorJson)); + } + + @Test + void testValidate() { + // Create valid configuration from JSON string + String validConfigJson = "{\"testField\": \"test value\"}"; + JsonNode validConfig = JacksonUtil.toJsonNode(validConfigJson); + + // Create invalid configuration (missing required field) from JSON string + String invalidConfigJson = "{}"; + JsonNode invalidConfig = JacksonUtil.toJsonNode(invalidConfigJson); + + // Test valid configuration + boolean validResult = service.validate(tenantId, componentDescriptor, validConfig); + assertTrue(validResult, "Valid configuration should pass validation"); + + // Test invalid configuration + boolean invalidResult = service.validate(tenantId, componentDescriptor, invalidConfig); + assertFalse(invalidResult, "Invalid configuration should fail validation"); + + // Test with component descriptor without schema + ComponentDescriptor noSchemaDescriptor = new ComponentDescriptor(componentDescriptor); + noSchemaDescriptor.setConfigurationDescriptor(JacksonUtil.toJsonNode("{}")); + + // Should throw exception when schema is missing + assertThrows(IncorrectParameterException.class, () -> { + service.validate(tenantId, noSchemaDescriptor, validConfig); + }, "Should throw exception when schema is missing"); + } + +} diff --git a/pom.xml b/pom.xml index cd2078e70a..0260dc09be 100755 --- a/pom.xml +++ b/pom.xml @@ -75,7 +75,7 @@ 2.17.2 1.7.0 4.4.0 - 2.2.14 + 1.5.6 0.6.12 3.12.1 2.0.0-M15 @@ -1620,15 +1620,9 @@ ${auth0-jwt.version} - com.github.java-json-tools + com.networknt json-schema-validator ${json-schema-validator.version} - - - com.sun.mail - mailapi - - org.eclipse.leshan From d608b87f945337908b4c731a10116eab35b080b0 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Sat, 10 May 2025 15:25:59 +0200 Subject: [PATCH 54/63] test scope mockserver with no dependency to not affect transitive dependencies --- pom.xml | 4 ++-- rule-engine/rule-engine-components/pom.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 0260dc09be..608a46584a 100755 --- a/pom.xml +++ b/pom.xml @@ -2233,7 +2233,7 @@ org.mock-server - mockserver-netty + mockserver-netty-no-dependencies ${mock-server.version} test @@ -2245,7 +2245,7 @@ org.mock-server - mockserver-client-java + mockserver-client-java-no-dependencies ${mock-server.version} test diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 008c422dbf..b975f7c624 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -141,12 +141,12 @@ org.mock-server - mockserver-netty + mockserver-netty-no-dependencies test org.mock-server - mockserver-client-java + mockserver-client-java-no-dependencies test From 99d2d1e03311cbba6eadf6e2959d122ec4258e67 Mon Sep 17 00:00:00 2001 From: Sergey Matvienko Date: Sat, 10 May 2025 15:30:08 +0200 Subject: [PATCH 55/63] Monitoring COAP Leshan Dependency Upgrade --- monitoring/pom.xml | 2 - .../monitoring/client/Lwm2mClient.java | 133 +++++++++--------- 2 files changed, 69 insertions(+), 66 deletions(-) diff --git a/monitoring/pom.xml b/monitoring/pom.xml index 4f59cbc975..e17b87454e 100644 --- a/monitoring/pom.xml +++ b/monitoring/pom.xml @@ -42,8 +42,6 @@ ThingsBoard Monitoring Service org.thingsboard.monitoring.ThingsboardMonitoringApplication - 2.6.1 - 2.0.0-M4 diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java b/monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java index f658bd1362..599fe29525 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/client/Lwm2mClient.java @@ -20,13 +20,16 @@ import lombok.Setter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.eclipse.californium.core.network.CoapEndpoint; -import org.eclipse.californium.core.network.config.NetworkConfig; -import org.eclipse.californium.core.observe.ObservationStore; -import org.eclipse.californium.scandium.DTLSConnector; -import org.eclipse.californium.scandium.config.DtlsConnectorConfig; -import org.eclipse.leshan.client.californium.LeshanClient; -import org.eclipse.leshan.client.californium.LeshanClientBuilder; +import org.eclipse.californium.core.config.CoapConfig; +import org.eclipse.californium.elements.config.Configuration; +import org.eclipse.californium.scandium.config.DtlsConfig; +import org.eclipse.leshan.client.LeshanClient; +import org.eclipse.leshan.client.LeshanClientBuilder; +import org.eclipse.leshan.client.californium.endpoint.CaliforniumClientEndpointsProvider; +import org.eclipse.leshan.client.californium.endpoint.ClientProtocolProvider; +import org.eclipse.leshan.client.californium.endpoint.coap.CoapOscoreProtocolProvider; +import org.eclipse.leshan.client.californium.endpoint.coaps.CoapsClientProtocolProvider; +import org.eclipse.leshan.client.endpoint.LwM2mClientEndpointsProvider; import org.eclipse.leshan.client.engine.DefaultRegistrationEngineFactory; import org.eclipse.leshan.client.object.Security; import org.eclipse.leshan.client.object.Server; @@ -34,9 +37,8 @@ import org.eclipse.leshan.client.observer.LwM2mClientObserver; import org.eclipse.leshan.client.resource.BaseInstanceEnabler; import org.eclipse.leshan.client.resource.DummyInstanceEnabler; import org.eclipse.leshan.client.resource.ObjectsInitializer; -import org.eclipse.leshan.client.servers.ServerIdentity; +import org.eclipse.leshan.client.servers.LwM2mServer; import org.eclipse.leshan.core.ResponseCode; -import org.eclipse.leshan.core.californium.EndpointFactory; import org.eclipse.leshan.core.model.InvalidDDFFileException; import org.eclipse.leshan.core.model.LwM2mModel; import org.eclipse.leshan.core.model.ObjectLoader; @@ -53,7 +55,6 @@ import org.thingsboard.monitoring.util.ResourceUtils; import javax.security.auth.Destroyable; import java.io.IOException; -import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -95,9 +96,11 @@ public class Lwm2mClient extends BaseInstanceEnabler implements Destroyable { } Security security = noSec(serverUri, 123); - NetworkConfig coapConfig = new NetworkConfig().setString(NetworkConfig.Keys.COAP_PORT, StringUtils.substringAfterLast(serverUri, ":")); - - LeshanClient leshanClient; + Configuration coapConfig = new Configuration(); + String portStr = StringUtils.substringAfterLast(serverUri, ":"); + if (StringUtils.isNotEmpty(portStr)) { + coapConfig.set(CoapConfig.COAP_PORT, Integer.parseInt(portStr)); + } LwM2mModel model = new StaticModel(models); ObjectsInitializer initializer = new ObjectsInitializer(model); @@ -105,118 +108,121 @@ public class Lwm2mClient extends BaseInstanceEnabler implements Destroyable { initializer.setInstancesForObject(SERVER, new Server(123, TimeUnit.MINUTES.toSeconds(5))); initializer.setInstancesForObject(DEVICE, this); initializer.setClassForObject(ACCESS_CONTROL, DummyInstanceEnabler.class); - DtlsConnectorConfig.Builder dtlsConfig = new DtlsConnectorConfig.Builder(); - dtlsConfig.setRecommendedCipherSuitesOnly(true); - dtlsConfig.setClientOnly(); - DefaultRegistrationEngineFactory engineFactory = new DefaultRegistrationEngineFactory(); - engineFactory.setReconnectOnUpdate(false); - engineFactory.setResumeOnConnect(true); + // Create client endpoints Provider + List protocolProvider = new ArrayList<>(); + protocolProvider.add(new CoapOscoreProtocolProvider()); + protocolProvider.add(new CoapsClientProtocolProvider()); + CaliforniumClientEndpointsProvider.Builder endpointsBuilder = new CaliforniumClientEndpointsProvider.Builder( + protocolProvider.toArray(new ClientProtocolProvider[protocolProvider.size()])); - EndpointFactory endpointFactory = new EndpointFactory() { + // Create Californium Configuration + Configuration clientCoapConfig = endpointsBuilder.createDefaultConfiguration(); - @Override - public CoapEndpoint createUnsecuredEndpoint(InetSocketAddress address, NetworkConfig coapConfig, - ObservationStore store) { - CoapEndpoint.Builder builder = new CoapEndpoint.Builder(); - builder.setInetSocketAddress(address); - builder.setNetworkConfig(coapConfig); - return builder.build(); - } + // Set some DTLS stuff + clientCoapConfig.setTransient(DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY); + clientCoapConfig.set(DtlsConfig.DTLS_RECOMMENDED_CIPHER_SUITES_ONLY, true); - @Override - public CoapEndpoint createSecuredEndpoint(DtlsConnectorConfig dtlsConfig, NetworkConfig coapConfig, - ObservationStore store) { - CoapEndpoint.Builder builder = new CoapEndpoint.Builder(); - DtlsConnectorConfig.Builder dtlsConfigBuilder = new DtlsConnectorConfig.Builder(dtlsConfig); - builder.setConnector(new DTLSConnector(dtlsConfigBuilder.build())); - builder.setNetworkConfig(coapConfig); - return builder.build(); - } - }; + // Set Californium Configuration + endpointsBuilder.setConfiguration(clientCoapConfig); + + // creates EndpointsProvider + List endpointsProvider = new ArrayList<>(); + endpointsProvider.add(endpointsBuilder.build()); + // Configure registration engine + DefaultRegistrationEngineFactory engineFactory = new DefaultRegistrationEngineFactory(); + engineFactory.setReconnectOnUpdate(false); + engineFactory.setResumeOnConnect(true); + + // Build the client LeshanClientBuilder builder = new LeshanClientBuilder(endpoint); builder.setObjects(initializer.createAll()); - builder.setCoapConfig(coapConfig); - builder.setDtlsConfig(dtlsConfig); + builder.setEndpointsProviders(endpointsProvider.toArray(new LwM2mClientEndpointsProvider[endpointsProvider.size()])); builder.setRegistrationEngineFactory(engineFactory); - builder.setEndpointFactory(endpointFactory); builder.setDecoder(new DefaultLwM2mDecoder(false)); builder.setEncoder(new DefaultLwM2mEncoder(false)); leshanClient = builder.build(); + // Add observer LwM2mClientObserver observer = new LwM2mClientObserver() { - @Override - public void onBootstrapStarted(ServerIdentity bsserver, BootstrapRequest request) {} + public void onBootstrapStarted(LwM2mServer bsserver, BootstrapRequest request) { + // No implementation needed + } @Override - public void onBootstrapSuccess(ServerIdentity bsserver, BootstrapRequest request) {} + public void onBootstrapSuccess(LwM2mServer bsserver, BootstrapRequest request) { + // No implementation needed + } @Override - public void onBootstrapFailure(ServerIdentity bsserver, BootstrapRequest request, - ResponseCode responseCode, String errorMessage, Exception cause) {} + public void onBootstrapFailure(LwM2mServer bsserver, BootstrapRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + // No implementation needed + } @Override - public void onBootstrapTimeout(ServerIdentity bsserver, BootstrapRequest request) {} + public void onBootstrapTimeout(LwM2mServer bsserver, BootstrapRequest request) { + // No implementation needed + } @Override - public void onRegistrationStarted(ServerIdentity server, RegisterRequest request) { + public void onRegistrationStarted(LwM2mServer server, RegisterRequest request) { log.debug("onRegistrationStarted [{}]", request.getEndpointName()); } @Override - public void onRegistrationSuccess(ServerIdentity server, RegisterRequest request, String registrationID) { + public void onRegistrationSuccess(LwM2mServer server, RegisterRequest request, String registrationID) { log.debug("onRegistrationSuccess [{}] [{}]", request.getEndpointName(), registrationID); } @Override - public void onRegistrationFailure(ServerIdentity server, RegisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + public void onRegistrationFailure(LwM2mServer server, RegisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { log.debug("onRegistrationFailure [{}] [{}] [{}]", request.getEndpointName(), responseCode, errorMessage); } @Override - public void onRegistrationTimeout(ServerIdentity server, RegisterRequest request) { + public void onRegistrationTimeout(LwM2mServer server, RegisterRequest request) { log.debug("onRegistrationTimeout [{}]", request.getEndpointName()); } @Override - public void onUpdateStarted(ServerIdentity server, UpdateRequest request) { + public void onUpdateStarted(LwM2mServer server, UpdateRequest request) { log.debug("onUpdateStarted [{}]", request.getRegistrationId()); } @Override - public void onUpdateSuccess(ServerIdentity server, UpdateRequest request) { + public void onUpdateSuccess(LwM2mServer server, UpdateRequest request) { log.debug("onUpdateSuccess [{}]", request.getRegistrationId()); } @Override - public void onUpdateFailure(ServerIdentity server, UpdateRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + public void onUpdateFailure(LwM2mServer server, UpdateRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { log.debug("onUpdateFailure [{}]", request.getRegistrationId()); } @Override - public void onUpdateTimeout(ServerIdentity server, UpdateRequest request) { + public void onUpdateTimeout(LwM2mServer server, UpdateRequest request) { log.debug("onUpdateTimeout [{}]", request.getRegistrationId()); } @Override - public void onDeregistrationStarted(ServerIdentity server, DeregisterRequest request) { + public void onDeregistrationStarted(LwM2mServer server, DeregisterRequest request) { log.debug("onDeregistrationStarted [{}]", request.getRegistrationId()); } @Override - public void onDeregistrationSuccess(ServerIdentity server, DeregisterRequest request) { - log.debug("onDeregistrationStarted [{}]", request.getRegistrationId()); + public void onDeregistrationSuccess(LwM2mServer server, DeregisterRequest request) { + log.debug("onDeregistrationSuccess [{}]", request.getRegistrationId()); } @Override - public void onDeregistrationFailure(ServerIdentity server, DeregisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { + public void onDeregistrationFailure(LwM2mServer server, DeregisterRequest request, ResponseCode responseCode, String errorMessage, Exception cause) { log.debug("onDeregistrationFailure [{}] [{}] [{}]", request.getRegistrationId(), responseCode, errorMessage); } @Override - public void onDeregistrationTimeout(ServerIdentity server, DeregisterRequest request) { + public void onDeregistrationTimeout(LwM2mServer server, DeregisterRequest request) { log.debug("onDeregistrationTimeout [{}]", request.getRegistrationId()); } @@ -224,7 +230,6 @@ public class Lwm2mClient extends BaseInstanceEnabler implements Destroyable { public void onUnexpectedError(Throwable unexpectedError) { log.debug("onUnexpectedError [{}]", unexpectedError.toString()); } - }; leshanClient.addObserver(observer); @@ -239,17 +244,17 @@ public class Lwm2mClient extends BaseInstanceEnabler implements Destroyable { } @Override - public ReadResponse read(ServerIdentity identity, int resourceId) { + public ReadResponse read(LwM2mServer server, int resourceId) { if (supportedResources.contains(resourceId)) { return ReadResponse.success(resourceId, data); } - return super.read(identity, resourceId); + return super.read(server, resourceId); } @SneakyThrows public void send(String data, int resource) { this.data = data; - fireResourcesChange(resource); + fireResourceChange(resource); } @Override From 32212b9c5129d5b60a968310635fd98179118fee Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 12 May 2025 12:08:52 +0300 Subject: [PATCH 56/63] Monitoring: automatic rule chain update --- .../ThingsboardMonitoringApplication.java | 6 + .../monitoring/client/TbClient.java | 9 +- .../monitoring/service/BaseHealthChecker.java | 5 +- .../service/BaseMonitoringService.java | 10 +- .../service/MonitoringEntityService.java | 245 +++++++++++++ .../service/MonitoringReporter.java | 14 +- .../transport/TransportHealthChecker.java | 149 +------- .../monitoring/util/ResourceUtils.java | 16 +- .../{root_rule_chain.json => rule_chain.json} | 327 ++++++++---------- 9 files changed, 428 insertions(+), 353 deletions(-) create mode 100644 monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java rename monitoring/src/main/resources/{root_rule_chain.json => rule_chain.json} (76%) diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java b/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java index f5f9e73275..c8e8d7070b 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/ThingsboardMonitoringApplication.java @@ -25,6 +25,7 @@ import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.EnableScheduling; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.monitoring.service.BaseMonitoringService; +import org.thingsboard.monitoring.service.MonitoringEntityService; import java.util.List; import java.util.Map; @@ -38,6 +39,8 @@ public class ThingsboardMonitoringApplication { @Autowired private List> monitoringServices; + @Autowired + private MonitoringEntityService entityService; @Value("${monitoring.monitoring_rate_ms}") private int monitoringRateMs; @@ -50,6 +53,9 @@ public class ThingsboardMonitoringApplication { @EventListener(ApplicationReadyEvent.class) public void startMonitoring() { + entityService.checkEntities(); + monitoringServices.forEach(BaseMonitoringService::init); + ScheduledExecutorService scheduler = ThingsBoardExecutors.newSingleThreadScheduledExecutor("monitoring-executor"); scheduler.scheduleWithFixedDelay(() -> { monitoringServices.forEach(monitoringService -> { diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/client/TbClient.java b/monitoring/src/main/java/org/thingsboard/monitoring/client/TbClient.java index e6a7e1b8af..0884cf7e5c 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/client/TbClient.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/client/TbClient.java @@ -15,17 +15,15 @@ */ package org.thingsboard.monitoring.client; +import jakarta.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Value; -import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import org.thingsboard.rest.client.RestClient; import java.time.Duration; @Component -@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class TbClient extends RestClient { @Value("${monitoring.rest.username}") @@ -41,6 +39,11 @@ public class TbClient extends RestClient { .build(), baseUrl); } + @PostConstruct + private void init() { + logIn(); + } + public String logIn() { login(username, password); return getToken(); diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java index 1e9cdbe191..d19e723332 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java @@ -22,7 +22,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.thingsboard.monitoring.client.TbClient; import org.thingsboard.monitoring.client.WsClient; import org.thingsboard.monitoring.config.MonitoringConfig; import org.thingsboard.monitoring.config.MonitoringTarget; @@ -46,6 +45,8 @@ public abstract class BaseHealthChecker, T ext @Value("${monitoring.calculated_fields.enabled:true}") protected boolean checkCalculatedFields; - @PostConstruct - private void init() { + public void init() { if (configs == null || configs.isEmpty()) { return; } - tbClient.logIn(); + configs.forEach(config -> { config.getTargets().forEach(target -> { BaseHealthChecker healthChecker = initHealthChecker(target, config); @@ -108,7 +106,7 @@ public abstract class BaseMonitoringService, T ext private BaseHealthChecker initHealthChecker(T target, C config) { BaseHealthChecker healthChecker = (BaseHealthChecker) createHealthChecker(config, target); log.info("Initializing {} for {}", healthChecker.getClass().getSimpleName(), target.getBaseUrl()); - healthChecker.initialize(tbClient); + healthChecker.initialize(); devices.add(target.getDeviceId()); return healthChecker; } @@ -140,7 +138,7 @@ public abstract class BaseMonitoringService, T ext reporter.serviceIsOk(MonitoredServiceKey.EDQS); } - reporter.reportLatencies(tbClient); + reporter.reportLatencies(); log.debug("Finished {}", getName()); } catch (ServiceFailureException e) { reporter.serviceFailure(e.getServiceKey(), e); diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java new file mode 100644 index 0000000000..66dc9200e3 --- /dev/null +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java @@ -0,0 +1,245 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.monitoring.service; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.RegexUtils; +import org.thingsboard.monitoring.client.TbClient; +import org.thingsboard.monitoring.config.transport.DeviceConfig; +import org.thingsboard.monitoring.config.transport.TransportMonitoringConfig; +import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget; +import org.thingsboard.monitoring.config.transport.TransportType; +import org.thingsboard.monitoring.util.ResourceUtils; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.DeviceProfileType; +import org.thingsboard.server.common.data.DeviceTransportType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.asset.Asset; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapClientCredentials; +import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; +import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecBootstrapClientCredential; +import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCredential; +import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; +import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; +import org.thingsboard.server.common.data.device.data.DeviceData; +import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; +import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.DeviceProfileData; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.data.security.DeviceCredentialsType; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.thingsboard.monitoring.service.BaseHealthChecker.TEST_CF_TELEMETRY_KEY; +import static org.thingsboard.monitoring.service.BaseHealthChecker.TEST_TELEMETRY_KEY; + +@Service +@Slf4j +@RequiredArgsConstructor +public class MonitoringEntityService { + + private final TbClient tbClient; + + @Value("${monitoring.calculated_fields.enabled:true}") + private boolean calculatedFieldsMonitoringEnabled; + + public void checkEntities() { + RuleChain ruleChain = tbClient.getRuleChains(RuleChainType.CORE, new PageLink(10)).getData().stream() + .filter(RuleChain::isRoot) + .findFirst().orElseThrow(); + RuleChainId ruleChainId = ruleChain.getId(); + + JsonNode ruleChainDescriptor = ResourceUtils.getResource("rule_chain.json"); + List attributeKeys = tbClient.getAttributeKeys(ruleChainId); + Map attributes = tbClient.getAttributeKvEntries(ruleChainId, attributeKeys).stream() + .collect(Collectors.toMap(KvEntry::getKey, KvEntry::getValueAsString)); + + int currentVersion = Integer.parseInt(attributes.getOrDefault("version", "0")); + int newVersion = ruleChainDescriptor.get("version").asInt(); + if (currentVersion == newVersion) { + log.info("Not updating rule chain, version is the same ({})", currentVersion); + return; + } else { + log.info("Updating rule chain '{}' from version {} to {}", ruleChain.getName(), currentVersion, newVersion); + } + + String metadataJson = RegexUtils.replace(ruleChainDescriptor.get("metadata").toString(), + "\\$\\{MONITORING:(.+?)}", matchResult -> { + String key = matchResult.group(1); + String value = attributes.get(key); + if (value == null) { + throw new IllegalArgumentException("No attribute found for key " + key); + } + log.info("Using {}: {}", key, value); + return value; + }); + RuleChainMetaData metaData = JacksonUtil.fromString(metadataJson, RuleChainMetaData.class); + metaData.setRuleChainId(ruleChainId); + tbClient.saveRuleChainMetaData(metaData); + } + + public Asset getOrCreateMonitoringAsset() { + String assetName = "[Monitoring] Latencies"; + return tbClient.findAsset(assetName).orElseGet(() -> { + Asset asset = new Asset(); + asset.setType("Monitoring"); + asset.setName(assetName); + asset = tbClient.saveAsset(asset); + log.info("Created monitoring asset {}", asset.getId()); + return asset; + }); + } + + public void checkEntities(TransportMonitoringConfig config, TransportMonitoringTarget target) { + Device device = getOrCreateDevice(config, target); + DeviceCredentials credentials = tbClient.getDeviceCredentialsByDeviceId(device.getId()) + .orElseThrow(() -> new IllegalArgumentException("No credentials found for device " + device.getId())); + + DeviceConfig deviceConfig = new DeviceConfig(); + deviceConfig.setId(device.getId().toString()); + deviceConfig.setName(device.getName()); + deviceConfig.setCredentials(credentials); + target.setDevice(deviceConfig); + } + + private Device getOrCreateDevice(TransportMonitoringConfig config, TransportMonitoringTarget target) { + TransportType transportType = config.getTransportType(); + String deviceName = String.format("%s %s (%s) - %s", target.getNamePrefix(), transportType.getName(), target.getQueue(), target.getBaseUrl()).trim(); + Device device = tbClient.getTenantDevice(deviceName).orElse(null); + if (device != null) { + if (calculatedFieldsMonitoringEnabled) { + CalculatedField calculatedField = tbClient.getCalculatedFieldsByEntityId(device.getId(), new PageLink(1, 0, TEST_CF_TELEMETRY_KEY)) + .getData().stream().findFirst().orElse(null); + if (calculatedField == null) { + createCalculatedField(device); + } + } + return device; + } + + log.info("Creating new device '{}'", deviceName); + device = new Device(); + device.setName(deviceName); + + DeviceCredentials credentials = new DeviceCredentials(); + credentials.setCredentialsId(RandomStringUtils.randomAlphabetic(20)); + DeviceData deviceData = new DeviceData(); + deviceData.setConfiguration(new DefaultDeviceConfiguration()); + + DeviceProfile deviceProfile = getOrCreateDeviceProfile(config, target); + device.setType(deviceProfile.getName()); + device.setDeviceProfileId(deviceProfile.getId()); + + if (transportType != TransportType.LWM2M) { + deviceData.setTransportConfiguration(new DefaultDeviceTransportConfiguration()); + credentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN); + } else { + deviceData.setTransportConfiguration(new Lwm2mDeviceTransportConfiguration()); + credentials.setCredentialsType(DeviceCredentialsType.LWM2M_CREDENTIALS); + LwM2MDeviceCredentials lwm2mCreds = new LwM2MDeviceCredentials(); + NoSecClientCredential client = new NoSecClientCredential(); + client.setEndpoint(credentials.getCredentialsId()); + lwm2mCreds.setClient(client); + LwM2MBootstrapClientCredentials bootstrap = new LwM2MBootstrapClientCredentials(); + bootstrap.setBootstrapServer(new NoSecBootstrapClientCredential()); + bootstrap.setLwm2mServer(new NoSecBootstrapClientCredential()); + lwm2mCreds.setBootstrap(bootstrap); + credentials.setCredentialsValue(JacksonUtil.toString(lwm2mCreds)); + } + + return tbClient.saveDeviceWithCredentials(device, credentials).get(); + } + + private DeviceProfile getOrCreateDeviceProfile(TransportMonitoringConfig config, TransportMonitoringTarget target) { + TransportType transportType = config.getTransportType(); + String profileName = String.format("%s %s (%s)", target.getNamePrefix(), transportType.getName(), target.getQueue()).trim(); + DeviceProfile deviceProfile = tbClient.getDeviceProfiles(new PageLink(1, 0, profileName)).getData() + .stream().findFirst().orElse(null); + if (deviceProfile != null) { + return deviceProfile; + } + + log.info("Creating new device profile '{}'", profileName); + if (transportType != TransportType.LWM2M) { + deviceProfile = new DeviceProfile(); + deviceProfile.setType(DeviceProfileType.DEFAULT); + deviceProfile.setTransportType(DeviceTransportType.DEFAULT); + DeviceProfileData profileData = new DeviceProfileData(); + profileData.setConfiguration(new DefaultDeviceProfileConfiguration()); + profileData.setTransportConfiguration(new DefaultDeviceProfileTransportConfiguration()); + deviceProfile.setProfileData(profileData); + } else { + tbClient.getResources(new PageLink(1, 0, "LwM2M Monitoring")).getData() + .stream().findFirst() + .orElseGet(() -> { + TbResource newResource = ResourceUtils.getResource("lwm2m/resource.json", TbResource.class); + log.info("Creating LwM2M resource"); + return tbClient.saveResource(newResource); + }); + deviceProfile = ResourceUtils.getResource("lwm2m/device_profile.json", DeviceProfile.class); + } + + deviceProfile.setName(profileName); + deviceProfile.setDefaultQueueName(target.getQueue()); + return tbClient.saveDeviceProfile(deviceProfile); + } + + private void createCalculatedField(Device device) { + log.info("Creating calculated field for device '{}'", device.getName()); + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setName(TEST_CF_TELEMETRY_KEY); + calculatedField.setEntityId(device.getId()); + calculatedField.setType(CalculatedFieldType.SCRIPT); + ScriptCalculatedFieldConfiguration configuration = new ScriptCalculatedFieldConfiguration(); + Argument testDataArgument = new Argument(); + testDataArgument.setRefEntityKey(new ReferencedEntityKey(TEST_TELEMETRY_KEY, ArgumentType.TS_LATEST, null)); + configuration.setArguments(Map.of( + TEST_TELEMETRY_KEY, testDataArgument + )); + configuration.setExpression("return { \"" + TEST_CF_TELEMETRY_KEY + "\": " + TEST_TELEMETRY_KEY + " + \"-cf\" };"); + Output output = new Output(); + output.setType(OutputType.TIME_SERIES); + configuration.setOutput(output); + calculatedField.setConfiguration(configuration); + calculatedField.setDebugMode(true); + tbClient.saveCalculatedField(calculatedField); + } + +} diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java index 62ed0d74aa..99cb9feb3d 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringReporter.java @@ -46,6 +46,8 @@ import java.util.stream.Collectors; public class MonitoringReporter { private final NotificationService notificationService; + private final TbClient tbClient; + private final MonitoringEntityService entityService; private final Map latencies = new ConcurrentHashMap<>(); private final Map failuresCounters = new ConcurrentHashMap<>(); @@ -62,7 +64,7 @@ public class MonitoringReporter { @Value("${monitoring.latency.reporting_asset_id}") private String reportingAssetId; - public void reportLatencies(TbClient tbClient) { + public void reportLatencies() { if (latencies.isEmpty()) { return; } @@ -81,15 +83,7 @@ public class MonitoringReporter { try { if (StringUtils.isBlank(reportingAssetId)) { - String assetName = "[Monitoring] Latencies"; - Asset monitoringAsset = tbClient.findAsset(assetName).orElseGet(() -> { - Asset asset = new Asset(); - asset.setType("Monitoring"); - asset.setName(assetName); - asset = tbClient.saveAsset(asset); - log.info("Created monitoring asset {}", asset.getId()); - return asset; - }); + Asset monitoringAsset = entityService.getOrCreateMonitoringAsset(); reportingAssetId = monitoringAsset.getId().toString(); } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java index ea95740c68..946c3e6d3d 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/transport/TransportHealthChecker.java @@ -17,46 +17,13 @@ package org.thingsboard.monitoring.service.transport; import com.fasterxml.jackson.databind.node.TextNode; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Value; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.monitoring.client.TbClient; -import org.thingsboard.monitoring.config.transport.DeviceConfig; import org.thingsboard.monitoring.config.transport.TransportInfo; import org.thingsboard.monitoring.config.transport.TransportMonitoringConfig; import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget; import org.thingsboard.monitoring.config.transport.TransportType; import org.thingsboard.monitoring.service.BaseHealthChecker; -import org.thingsboard.monitoring.util.ResourceUtils; -import org.thingsboard.server.common.data.Device; -import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.data.DeviceProfileType; -import org.thingsboard.server.common.data.DeviceTransportType; -import org.thingsboard.server.common.data.TbResource; -import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldType; -import org.thingsboard.server.common.data.cf.configuration.Argument; -import org.thingsboard.server.common.data.cf.configuration.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.Output; -import org.thingsboard.server.common.data.cf.configuration.OutputType; -import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; -import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MBootstrapClientCredentials; -import org.thingsboard.server.common.data.device.credentials.lwm2m.LwM2MDeviceCredentials; -import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecBootstrapClientCredential; -import org.thingsboard.server.common.data.device.credentials.lwm2m.NoSecClientCredential; -import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; -import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration; -import org.thingsboard.server.common.data.device.data.DeviceData; -import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfiguration; -import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration; -import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration; -import org.thingsboard.server.common.data.device.profile.DeviceProfileData; -import org.thingsboard.server.common.data.page.PageLink; -import org.thingsboard.server.common.data.security.DeviceCredentials; -import org.thingsboard.server.common.data.security.DeviceCredentialsType; - -import java.util.Map; @Slf4j public abstract class TransportHealthChecker extends BaseHealthChecker { @@ -69,16 +36,8 @@ public abstract class TransportHealthChecker new IllegalArgumentException("No credentials found for device " + device.getId())); - - DeviceConfig deviceConfig = new DeviceConfig(); - deviceConfig.setId(device.getId().toString()); - deviceConfig.setName(device.getName()); - deviceConfig.setCredentials(credentials); - target.setDevice(deviceConfig); + protected void initialize() { + entityService.checkEntities(config, target); } @Override @@ -98,110 +57,6 @@ public abstract class TransportHealthChecker { - TbResource newResource = ResourceUtils.getResource("lwm2m/resource.json", TbResource.class); - log.info("Creating LwM2M resource"); - return tbClient.saveResource(newResource); - }); - deviceProfile = ResourceUtils.getResource("lwm2m/device_profile.json", DeviceProfile.class); - } - - deviceProfile.setName(profileName); - deviceProfile.setDefaultQueueName(target.getQueue()); - return tbClient.saveDeviceProfile(deviceProfile); - } - - private void createCalculatedField(TbClient tbClient, Device device) { - log.info("Creating calculated field for device '{}'", device.getName()); - CalculatedField calculatedField = new CalculatedField(); - calculatedField.setName(TEST_CF_TELEMETRY_KEY); - calculatedField.setEntityId(device.getId()); - calculatedField.setType(CalculatedFieldType.SCRIPT); - ScriptCalculatedFieldConfiguration configuration = new ScriptCalculatedFieldConfiguration(); - Argument testDataArgument = new Argument(); - testDataArgument.setRefEntityKey(new ReferencedEntityKey(TEST_TELEMETRY_KEY, ArgumentType.TS_LATEST, null)); - configuration.setArguments(Map.of( - TEST_TELEMETRY_KEY, testDataArgument - )); - configuration.setExpression("return { \"" + TEST_CF_TELEMETRY_KEY + "\": " + TEST_TELEMETRY_KEY + " + \"-cf\" };"); - Output output = new Output(); - output.setType(OutputType.TIME_SERIES); - configuration.setOutput(output); - calculatedField.setConfiguration(configuration); - calculatedField.setDebugMode(true); - tbClient.saveCalculatedField(calculatedField); - } - @Override protected boolean isCfMonitoringEnabled() { return calculatedFieldsMonitoringEnabled; diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/util/ResourceUtils.java b/monitoring/src/main/java/org/thingsboard/monitoring/util/ResourceUtils.java index 3e25cfbd39..a92033f439 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/util/ResourceUtils.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/util/ResourceUtils.java @@ -15,6 +15,7 @@ */ package org.thingsboard.monitoring.util; +import com.fasterxml.jackson.databind.JsonNode; import lombok.SneakyThrows; import org.thingsboard.common.util.JacksonUtil; @@ -24,14 +25,21 @@ public class ResourceUtils { @SneakyThrows public static T getResource(String path, Class type) { - InputStream resource = ResourceUtils.class.getClassLoader().getResourceAsStream(path); - if (resource == null) { - throw new IllegalArgumentException("Resource not found for path " + path); - } + InputStream resource = getResourceStream(path); return JacksonUtil.OBJECT_MAPPER.readValue(resource, type); } + @SneakyThrows + public static JsonNode getResource(String path) { + InputStream resource = getResourceStream(path); + return JacksonUtil.OBJECT_MAPPER.readTree(resource); + } + public static InputStream getResourceAsStream(String path) { + return getResourceStream(path); + } + + private static InputStream getResourceStream(String path) { InputStream resource = ResourceUtils.class.getClassLoader().getResourceAsStream(path); if (resource == null) { throw new IllegalArgumentException("Resource not found for path " + path); diff --git a/monitoring/src/main/resources/root_rule_chain.json b/monitoring/src/main/resources/rule_chain.json similarity index 76% rename from monitoring/src/main/resources/root_rule_chain.json rename to monitoring/src/main/resources/rule_chain.json index a1c12c8e9d..96d2388636 100644 --- a/monitoring/src/main/resources/root_rule_chain.json +++ b/monitoring/src/main/resources/rule_chain.json @@ -1,257 +1,232 @@ { + "version": 1, "ruleChain": { - "additionalInfo": null, "name": "Root Rule Chain", "type": "CORE", "firstRuleNodeId": null, "root": false, "debugMode": false, "configuration": null, - "externalId": null + "additionalInfo": null }, "metadata": { - "firstNodeIndex": 12, + "firstNodeIndex": 9, "nodes": [ { - "additionalInfo": { - "description": null, - "layoutX": 1202, - "layoutY": 221 - }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "name": "Save Timeseries", + "debugSettings": { + "failuresEnabled": true, + "allEnabled": false, + "allEnabledUntil": 1735310701003 + }, "singletonMode": false, + "queueName": null, "configurationVersion": 1, "configuration": { "defaultTTL": 0, - "useServerTs": false, "processingSettings": { "type": "ON_EVERY_MESSAGE" } }, - "externalId": null + "additionalInfo": { + "description": null, + "layoutX": 1202, + "layoutY": 221 + } }, { - "additionalInfo": { - "layoutX": 1000, - "layoutY": 167 - }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode", "name": "Save Attributes", + "debugSettings": null, "singletonMode": false, + "queueName": null, "configurationVersion": 3, "configuration": { + "scope": "CLIENT_SCOPE", + "notifyDevice": false, "processingSettings": { "type": "ON_EVERY_MESSAGE" }, - "scope": "CLIENT_SCOPE", - "notifyDevice": false, "sendAttributesUpdatedNotification": false, "updateAttributesOnlyOnValueChange": false }, - "externalId": null + "additionalInfo": { + "layoutX": 1000, + "layoutY": 167 + } }, { - "additionalInfo": { - "layoutX": 566, - "layoutY": 302 - }, "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode", "name": "Message Type Switch", + "debugSettings": null, "singletonMode": false, + "queueName": null, "configurationVersion": 0, "configuration": { "version": 0 }, - "externalId": null + "additionalInfo": { + "layoutX": 566, + "layoutY": 302 + } }, { - "additionalInfo": { - "layoutX": 1000, - "layoutY": 381 - }, "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log RPC from Device", + "debugSettings": null, "singletonMode": false, + "queueName": null, "configurationVersion": 0, "configuration": { "scriptLang": "TBEL", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", "tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" }, - "externalId": null - }, - { "additionalInfo": { "layoutX": 1000, - "layoutY": 494 - }, + "layoutY": 381 + } + }, + { "type": "org.thingsboard.rule.engine.action.TbLogNode", "name": "Log Other", + "debugSettings": null, "singletonMode": false, + "queueName": null, "configurationVersion": 0, "configuration": { "scriptLang": "TBEL", "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);", "tbelScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);" }, - "externalId": null - }, - { "additionalInfo": { "layoutX": 1000, - "layoutY": 583 - }, + "layoutY": 494 + } + }, + { "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode", "name": "RPC Call Request", + "debugSettings": null, "singletonMode": false, + "queueName": null, "configurationVersion": 0, "configuration": { "timeoutInSeconds": 60 }, - "externalId": null - }, - { - "additionalInfo": { - "layoutX": 255, - "layoutY": 301 - }, - "type": "org.thingsboard.rule.engine.filter.TbOriginatorTypeFilterNode", - "name": "Is Entity Group", - "singletonMode": false, - "configurationVersion": 0, - "configuration": { - "originatorTypes": [ - "ENTITY_GROUP" - ] - }, - "externalId": null - }, - { "additionalInfo": { - "layoutX": 319, - "layoutY": 109 - }, - "type": "org.thingsboard.rule.engine.filter.TbMsgTypeFilterNode", - "name": "Post attributes or RPC request", - "singletonMode": false, - "configurationVersion": 0, - "configuration": { - "messageTypes": [ - "POST_ATTRIBUTES_REQUEST", - "RPC_CALL_FROM_SERVER_TO_DEVICE" - ] - }, - "externalId": null + "layoutX": 1000, + "layoutY": 583 + } }, { - "additionalInfo": { - "layoutX": 627, - "layoutY": 108 + "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", + "name": "Device Profile Node", + "debugSettings": { + "failuresEnabled": true, + "allEnabled": false, + "allEnabledUntil": 1735310701003 }, - "type": "org.thingsboard.rule.engine.transform.TbDuplicateMsgToGroupNode", - "name": "Duplicate To Group Entities", "singletonMode": false, - "configurationVersion": 0, + "queueName": null, + "configurationVersion": 1, "configuration": { - "entityGroupId": null, - "entityGroupIsMessageOriginator": true + "persistAlarmRulesState": false, + "fetchAlarmRulesStateOnStart": false }, - "externalId": null - }, - { "additionalInfo": { "description": "Process incoming messages from devices with the alarm rules defined in the device profile. Dispatch all incoming messages with \"Success\" relation type.", "layoutX": 45, "layoutY": 359 - }, - "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode", - "name": "Device Profile Node", - "singletonMode": false, - "configurationVersion": 0, - "configuration": { - "persistAlarmRulesState": false, - "fetchAlarmRulesStateOnStart": false - }, - "externalId": null + } }, { - "additionalInfo": { - "description": "", - "layoutX": 160, - "layoutY": 631 - }, "type": "org.thingsboard.rule.engine.filter.TbJsFilterNode", "name": "Test JS script", + "debugSettings": null, "singletonMode": false, + "queueName": null, "configurationVersion": 0, "configuration": { "scriptLang": "JS", - "jsScript": "var test = {\n a: 'a',\n b: 'b'\n};\nreturn test.a === 'a' && test.b === 'b';", + "jsScript": "var test = {\n a: 'a',\n b: 'b'\n};\n\nreturn test.a === 'a' && test.b === 'b';", "tbelScript": "return msg.temperature > 20;" }, - "externalId": null + "additionalInfo": { + "description": "dashboardId: ${MONITORING:dashboardId}", + "layoutX": 251, + "layoutY": 499 + } }, { - "additionalInfo": { - "description": "", - "layoutX": 427, - "layoutY": 541 - }, "type": "org.thingsboard.rule.engine.filter.TbJsFilterNode", "name": "Test TBEL script", + "debugSettings": null, "singletonMode": false, + "queueName": null, "configurationVersion": 0, "configuration": { "scriptLang": "TBEL", "jsScript": "return msg.temperature > 20;", "tbelScript": "var a = \"a\";\nvar b = \"b\";\nreturn a.equals(\"a\") && b.equals(\"b\");" }, - "externalId": null - }, - { "additionalInfo": { "description": "", - "layoutX": 40, - "layoutY": 252 - }, + "layoutX": 317, + "layoutY": 355 + } + }, + { "type": "org.thingsboard.rule.engine.transform.TbTransformMsgNode", "name": "Add arrival timestamp", + "debugSettings": { + "failuresEnabled": true, + "allEnabled": false, + "allEnabledUntil": 1744642101587 + }, "singletonMode": false, + "queueName": null, "configurationVersion": 0, "configuration": { "scriptLang": "TBEL", "jsScript": "return {msg: msg, metadata: metadata, msgType: msgType};", "tbelScript": "metadata.arrivalTs = Date.now();\nreturn {msg: msg, metadata: metadata, msgType: msgType};" }, - "externalId": null - }, - { "additionalInfo": { "description": "", - "layoutX": 1467, - "layoutY": 267 - }, + "layoutX": 40, + "layoutY": 252 + } + }, + { "type": "org.thingsboard.rule.engine.transform.TbTransformMsgNode", "name": "Calculate additional latencies", + "debugSettings": { + "failuresEnabled": true, + "allEnabled": false, + "allEnabledUntil": 1735310701003 + }, "singletonMode": false, + "queueName": null, "configurationVersion": 0, "configuration": { "scriptLang": "TBEL", "jsScript": "return {msg: msg, metadata: metadata, msgType: msgType};", "tbelScript": "var arrivalLatency = metadata.arrivalTs - metadata.ts;\nvar processingTime = Date.now() - metadata.arrivalTs;\nmsg = {\n arrivalLatency: arrivalLatency,\n processingTime: processingTime\n};\nreturn {msg: msg, metadata: metadata, msgType: msgType};" }, - "externalId": null - }, - { "additionalInfo": { "description": "", - "layoutX": 1438, - "layoutY": 403 - }, + "layoutX": 1467, + "layoutY": 267 + } + }, + { "type": "org.thingsboard.rule.engine.transform.TbChangeOriginatorNode", "name": "To latencies asset", + "debugSettings": null, "singletonMode": false, + "queueName": null, "configurationVersion": 0, "configuration": { "originatorSource": "ENTITY", @@ -269,64 +244,79 @@ "fetchLastLevelOnly": false } }, - "externalId": null + "additionalInfo": { + "description": "", + "layoutX": 1438, + "layoutY": 403 + } }, { - "additionalInfo": { - "description": null, - "layoutX": 1458, - "layoutY": 505 - }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "name": "Save Timeseries", + "debugSettings": { + "failuresEnabled": true, + "allEnabled": false, + "allEnabledUntil": 1735310701003 + }, "singletonMode": false, + "queueName": null, "configurationVersion": 1, "configuration": { "defaultTTL": 0, - "useServerTs": false, "processingSettings": { "type": "ON_EVERY_MESSAGE" } }, - "externalId": null + "additionalInfo": { + "description": null, + "layoutX": 1458, + "layoutY": 505 + } }, { - "additionalInfo": { - "description": "", - "layoutX": 928, - "layoutY": 266 - }, "type": "org.thingsboard.rule.engine.filter.TbCheckMessageNode", "name": "Has testData", + "debugSettings": null, "singletonMode": false, + "queueName": null, "configurationVersion": 0, "configuration": { "messageNames": [ - "testData" + "testData", + "testDataCf" ], "metadataNames": [], - "checkAllKeys": true + "checkAllKeys": false }, - "externalId": null + "additionalInfo": { + "description": "", + "layoutX": 928, + "layoutY": 266 + } }, { - "additionalInfo": { - "description": null, - "layoutX": 1203, - "layoutY": 327 - }, "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode", "name": "Save Timeseries with TTL", + "debugSettings": { + "failuresEnabled": true, + "allEnabled": false, + "allEnabledUntil": 1742305233839 + }, "singletonMode": false, + "queueName": null, "configurationVersion": 1, "configuration": { - "defaultTTL": 180, - "useServerTs": false, "processingSettings": { "type": "ON_EVERY_MESSAGE" - } + }, + "defaultTTL": 60, + "useServerTs": null }, - "externalId": null + "additionalInfo": { + "description": "", + "layoutX": 1203, + "layoutY": 327 + } } ], "connections": [ @@ -352,23 +342,13 @@ }, { "fromIndex": 2, - "toIndex": 16, + "toIndex": 13, "type": "Post telemetry" }, - { - "fromIndex": 6, - "toIndex": 2, - "type": "False" - }, { "fromIndex": 6, "toIndex": 7, - "type": "True" - }, - { - "fromIndex": 7, - "toIndex": 2, - "type": "False" + "type": "Success" }, { "fromIndex": 7, @@ -378,54 +358,39 @@ { "fromIndex": 8, "toIndex": 2, - "type": "Success" + "type": "True" }, { "fromIndex": 9, - "toIndex": 10, + "toIndex": 6, "type": "Success" }, { "fromIndex": 10, "toIndex": 11, - "type": "True" + "type": "Success" }, { "fromIndex": 11, - "toIndex": 6, - "type": "True" - }, - { - "fromIndex": 12, - "toIndex": 9, + "toIndex": 12, "type": "Success" }, { "fromIndex": 13, - "toIndex": 14, - "type": "Success" - }, - { - "fromIndex": 14, - "toIndex": 15, - "type": "Success" - }, - { - "fromIndex": 16, "toIndex": 0, "type": "False" }, { - "fromIndex": 16, - "toIndex": 17, + "fromIndex": 13, + "toIndex": 14, "type": "True" }, { - "fromIndex": 17, - "toIndex": 13, + "fromIndex": 14, + "toIndex": 10, "type": "Success" } ], "ruleChainConnections": null } -} +} \ No newline at end of file From 602d60281c5537fba1bfc194135a275b97b66fa4 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 12 May 2025 12:50:03 +0300 Subject: [PATCH 57/63] Fixes for monitoring --- .../config/transport/TransportMonitoringTarget.java | 5 +++++ .../monitoring/service/MonitoringEntityService.java | 9 ++++++++- monitoring/src/main/resources/rule_chain.json | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java index e8a9ab03fa..5558f39c05 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/config/transport/TransportMonitoringTarget.java @@ -15,6 +15,7 @@ */ package org.thingsboard.monitoring.config.transport; +import com.google.common.base.Strings; import lombok.Data; import org.apache.commons.lang3.StringUtils; import org.thingsboard.monitoring.config.MonitoringTarget; @@ -39,4 +40,8 @@ public class TransportMonitoringTarget implements MonitoringTarget { return StringUtils.defaultIfEmpty(queue, "Main"); } + public String getNamePrefix() { + return Strings.nullToEmpty(namePrefix); + } + } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java index 66dc9200e3..062104ecd5 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/MonitoringEntityService.java @@ -29,6 +29,7 @@ import org.thingsboard.monitoring.config.transport.TransportMonitoringConfig; import org.thingsboard.monitoring.config.transport.TransportMonitoringTarget; import org.thingsboard.monitoring.config.transport.TransportType; import org.thingsboard.monitoring.util.ResourceUtils; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileType; @@ -113,6 +114,8 @@ public class MonitoringEntityService { RuleChainMetaData metaData = JacksonUtil.fromString(metadataJson, RuleChainMetaData.class); metaData.setRuleChainId(ruleChainId); tbClient.saveRuleChainMetaData(metaData); + tbClient.saveEntityAttributesV2(ruleChainId, DataConstants.SERVER_SCOPE, JacksonUtil.newObjectNode() + .put("version", newVersion)); } public Asset getOrCreateMonitoringAsset() { @@ -184,7 +187,11 @@ public class MonitoringEntityService { credentials.setCredentialsValue(JacksonUtil.toString(lwm2mCreds)); } - return tbClient.saveDeviceWithCredentials(device, credentials).get(); + device = tbClient.saveDeviceWithCredentials(device, credentials).get(); + if (calculatedFieldsMonitoringEnabled) { + createCalculatedField(device); + } + return device; } private DeviceProfile getOrCreateDeviceProfile(TransportMonitoringConfig config, TransportMonitoringTarget target) { diff --git a/monitoring/src/main/resources/rule_chain.json b/monitoring/src/main/resources/rule_chain.json index 96d2388636..2bc0e89a8a 100644 --- a/monitoring/src/main/resources/rule_chain.json +++ b/monitoring/src/main/resources/rule_chain.json @@ -154,7 +154,7 @@ "tbelScript": "return msg.temperature > 20;" }, "additionalInfo": { - "description": "dashboardId: ${MONITORING:dashboardId}", + "description": "", "layoutX": 251, "layoutY": 499 } From fd66c5f1773251fde5d016f7351843ea1caef8b3 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Mon, 12 May 2025 12:58:33 +0300 Subject: [PATCH 58/63] CF monitoring fixes --- .../thingsboard/monitoring/data/MonitoredServiceKey.java | 1 - .../thingsboard/monitoring/service/BaseHealthChecker.java | 6 ++---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java index 7579d75231..9c3ee5b786 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/MonitoredServiceKey.java @@ -19,6 +19,5 @@ public class MonitoredServiceKey { public static final String GENERAL = "Monitoring"; public static final String EDQS = "*EDQS*"; - public static final String CF = "*CF*"; } diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java index d19e723332..072310fb67 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseHealthChecker.java @@ -117,11 +117,9 @@ public abstract class BaseHealthChecker Date: Mon, 12 May 2025 12:50:47 +0200 Subject: [PATCH 59/63] test fix for givenSslIsTrueAndCredentials_whenGetSslContext_thenVerifySslContext --- .../java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java index bf650af8bb..2438e59106 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mqtt/TbMqttNodeTest.java @@ -181,7 +181,8 @@ public class TbMqttNodeTest extends AbstractRuleNodeUpgradeTest { SslContext actualSslContext = mqttClientConfig.getValue().getSslContext(); assertThat(actualSslContext) .usingRecursiveComparison() - .ignoringFields("ctx", "ctxLock", "sessionContext.context.ctx", "sessionContext.context.ctxLock") + .ignoringFields("ctx", "ctxLock", "sessionContext.context.ctx", "sessionContext.context.ctxLock", + "sslContext") .isEqualTo(SslContextBuilder.forClient().build()); } From be8149a4bc46c2b60ed949aaa5dba176ebe8b171 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Mon, 12 May 2025 14:25:24 +0300 Subject: [PATCH 60/63] UI: Fixed help icon color in rule node --- .../rule-node/action/gps-geo-action-config.component.html | 3 ++- .../action/save-to-custom-table-config.component.html | 1 + .../rule-node/action/timeseries-config.component.html | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/gps-geo-action-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/gps-geo-action-config.component.html index 2d3f230643..8849d60c77 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/gps-geo-action-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/gps-geo-action-config.component.html @@ -116,11 +116,12 @@ rule-node-config.polygon-definition - help + {{ 'rule-node-config.polygon-definition-required' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/save-to-custom-table-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/save-to-custom-table-config.component.html index c9685f2c3f..aee7b9cd83 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/save-to-custom-table-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/save-to-custom-table-config.component.html @@ -22,6 +22,7 @@ help diff --git a/ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.html index 9a6d06255d..f41613c6fb 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/action/timeseries-config.component.html @@ -76,9 +76,10 @@ requiredText="{{ 'rule-node-config.default-ttl-required' | translate }}" minErrorText="{{ 'rule-node-config.min-default-ttl-message' | translate }}" formControlName="defaultTTL"> - help From 64c1a1db0c5c7850262aaf4d1d72a4b9b4204aec Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 14 May 2025 09:14:54 +0300 Subject: [PATCH 61/63] Revert "test scope mockserver with no dependency to not affect transitive dependencies" This reverts commit d608b87f945337908b4c731a10116eab35b080b0. --- pom.xml | 4 ++-- rule-engine/rule-engine-components/pom.xml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 608a46584a..0260dc09be 100755 --- a/pom.xml +++ b/pom.xml @@ -2233,7 +2233,7 @@ org.mock-server - mockserver-netty-no-dependencies + mockserver-netty ${mock-server.version} test @@ -2245,7 +2245,7 @@ org.mock-server - mockserver-client-java-no-dependencies + mockserver-client-java ${mock-server.version} test diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index b975f7c624..008c422dbf 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -141,12 +141,12 @@ org.mock-server - mockserver-netty-no-dependencies + mockserver-netty test org.mock-server - mockserver-client-java-no-dependencies + mockserver-client-java test From 758ed0f18954378b6457e274267f597288c20cdf Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Wed, 14 May 2025 13:07:07 +0300 Subject: [PATCH 62/63] tbel: Add utility methods to check for Map, List, and Array types in TbUtils --- .../service/script/TbelInvokeDocsIoTest.java | 80 ++++++++++++++++++ .../thingsboard/script/api/tbel/TbUtils.java | 19 +++++ .../script/api/tbel/TbUtilsTest.java | 81 ++++++++++++------- 3 files changed, 153 insertions(+), 27 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java index bf86f98e6e..36ac8d54dc 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java @@ -33,6 +33,9 @@ import java.util.concurrent.atomic.AtomicReference; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { @@ -2345,6 +2348,83 @@ class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { assertEquals(expected, actual); } + @Test + public void isMap_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {} + """; + decoderStr = """ + return isMap(msg); + """; + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertInstanceOf(Boolean.class, actual); + assertTrue((Boolean) actual); + decoderStr = """ + return isList(msg); + """; + actual = invokeScript(evalScript(decoderStr), msgStr); + assertInstanceOf(Boolean.class, actual); + assertFalse((Boolean) actual); + } + + @Test + public void isList_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {} + """; + decoderStr = String.format(""" + var list = []; + list.add(0x35); + return isList(list); + """); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertInstanceOf(Boolean.class, actual); + assertTrue((Boolean) actual); + decoderStr = String.format(""" + var list = []; + list.add(0x35); + return isMap(list); + """); + actual = invokeScript(evalScript(decoderStr), msgStr); + assertInstanceOf(Boolean.class, actual); + assertFalse((Boolean) actual); + decoderStr = String.format(""" + var list = []; + list.add(0x35); + return isArray(list); + """); + actual = invokeScript(evalScript(decoderStr), msgStr); + assertInstanceOf(Boolean.class, actual); + assertFalse((Boolean) actual); + } + + @Test + public void isArray_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {} + """; + decoderStr = """ + var array = new int[3]; + array[0] = 1; + array[1] = 2; + array[2] = 3; + return isArray(array); + """; + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertInstanceOf(Boolean.class, actual); + assertTrue((Boolean) actual); + decoderStr = """ + var array = new int[3]; + array[0] = 1; + array[1] = 2; + array[2] = 3; + return isList(array); + """; + actual = invokeScript(evalScript(decoderStr), msgStr); + assertInstanceOf(Boolean.class, actual); + assertFalse((Boolean) actual); + } + private List splice(List oldList, int start, int deleteCount, Object... values) { start = initStartIndex(oldList, start); deleteCount = deleteCount < 0 ? 0 : Math.min(deleteCount, (oldList.size() - start)); diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index 72792c1093..b782040a99 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -380,6 +380,12 @@ public class TbUtils { double.class, double.class, String.class))); parserConfig.addImport("isInsideCircle", new MethodStub(TbUtils.class.getMethod("isInsideCircle", double.class, double.class, String.class))); + parserConfig.addImport("isMap", new MethodStub(TbUtils.class.getMethod("isMap", + Object.class))); + parserConfig.addImport("isList", new MethodStub(TbUtils.class.getMethod("isList", + Object.class))); + parserConfig.addImport("isArray", new MethodStub(TbUtils.class.getMethod("isArray", + Object.class))); } public static String btoa(String input) { @@ -1462,6 +1468,19 @@ public class TbUtils { return range > GeoUtil.distance(entityCoordinates, perimeterCoordinates, rangeUnit); } + public static boolean isMap(Object obj) { + return obj instanceof Map; + + } + + public static boolean isList(Object obj) { + return obj instanceof List; + } + + public static boolean isArray(Object obj) { + return obj != null && obj.getClass().isArray(); + } + private static byte isValidIntegerToByte(Integer val) { if (val > 255 || val < -128) { throw new NumberFormatException("The value '" + val + "' could not be correctly converted to a byte. " + diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index 1063a8e8de..6d793f2c8d 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -38,11 +38,16 @@ import java.util.Arrays; import java.util.Base64; import java.util.Calendar; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Random; +import java.util.concurrent.ExecutionException; import static java.lang.Character.MAX_RADIX; import static java.lang.Character.MIN_RADIX; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; @Slf4j public class TbUtilsTest { @@ -315,7 +320,7 @@ public class TbUtilsTest { TbUtils.parseBytesToFloat(floatValByte, 0, 4, true); Assertions.fail("Should throw NumberFormatException"); } catch (RuntimeException e) { - Assertions.assertTrue(e.getMessage().contains(message)); + assertTrue(e.getMessage().contains(message)); } // "01752B0367FA000500010488 FFFFFFFF FFFFFFFF 33"; @@ -325,7 +330,7 @@ public class TbUtilsTest { TbUtils.parseBytesToFloat(floatValList, 12, 4, false); Assertions.fail("Should throw NumberFormatException"); } catch (RuntimeException e) { - Assertions.assertTrue(e.getMessage().contains(message)); + assertTrue(e.getMessage().contains(message)); } } @@ -385,7 +390,7 @@ public class TbUtilsTest { TbUtils.parseBytesIntToFloat(byteAT101, byteAT101.size() + 1); Assertions.fail("Should throw NumberFormatException"); } catch (RuntimeException e) { - Assertions.assertTrue(e.getMessage().contains("is out of bounds for array with length:")); + assertTrue(e.getMessage().contains("is out of bounds for array with length:")); } } @@ -499,7 +504,7 @@ public class TbUtilsTest { TbUtils.parseBytesToDouble(doubleValByte, 0, 8, true); Assertions.fail("Should throw NumberFormatException"); } catch (RuntimeException e) { - Assertions.assertTrue(e.getMessage().contains(message)); + assertTrue(e.getMessage().contains(message)); } } @@ -601,20 +606,20 @@ public class TbUtilsTest { String actualStr = TbUtils.bytesToString(listHex); byte[] actualBytes = actualStr.getBytes(); Assertions.assertArrayEquals(expectedBytes, actualBytes); - Assertions.assertTrue(actualStr.isBlank()); + assertTrue(actualStr.isBlank()); listHex = new ArrayList<>(Arrays.asList("0x21", "0x21")); expectedBytes = new byte[]{33, 33}; actualStr = TbUtils.bytesToString(listHex); actualBytes = actualStr.getBytes(); Assertions.assertArrayEquals(expectedBytes, actualBytes); - Assertions.assertFalse(actualStr.isBlank()); + assertFalse(actualStr.isBlank()); Assertions.assertEquals("!!", actualStr); listHex = new ArrayList<>(Arrays.asList("21", "0x21")); expectedBytes = new byte[]{21, 33}; actualStr = TbUtils.bytesToString(listHex); actualBytes = actualStr.getBytes(); Assertions.assertArrayEquals(expectedBytes, actualBytes); - Assertions.assertFalse(actualStr.isBlank()); + assertFalse(actualStr.isBlank()); Assertions.assertEquals("!", actualStr.substring(1)); Assertions.assertEquals('\u0015', actualStr.charAt(0)); Assertions.assertEquals(21, actualStr.charAt(0)); @@ -628,7 +633,7 @@ public class TbUtilsTest { TbUtils.bytesToString(listHex); Assertions.fail("Should throw NumberFormatException"); } catch (NumberFormatException e) { - Assertions.assertTrue(e.getMessage().contains("Value: \"FG\" is not numeric or hexDecimal format!")); + assertTrue(e.getMessage().contains("Value: \"FG\" is not numeric or hexDecimal format!")); } List listIntString = new ArrayList<>(); @@ -637,7 +642,7 @@ public class TbUtilsTest { TbUtils.bytesToString(listIntString); Assertions.fail("Should throw NumberFormatException"); } catch (NumberFormatException e) { - Assertions.assertTrue(e.getMessage().contains("The value '-129' could not be correctly converted to a byte. " + + assertTrue(e.getMessage().contains("The value '-129' could not be correctly converted to a byte. " + "Integer to byte conversion requires the use of only 8 bits (with a range of min/max = -128/255)!")); } @@ -646,7 +651,7 @@ public class TbUtilsTest { TbUtils.bytesToString(listIntString); Assertions.fail("Should throw NumberFormatException"); } catch (NumberFormatException e) { - Assertions.assertTrue(e.getMessage().contains("The value '256' could not be correctly converted to a byte. " + + assertTrue(e.getMessage().contains("The value '256' could not be correctly converted to a byte. " + "Integer to byte conversion requires the use of only 8 bits (with a range of min/max = -128/255)!")); } @@ -656,7 +661,7 @@ public class TbUtilsTest { TbUtils.bytesToString(listIntBytes); Assertions.fail("Should throw NumberFormatException"); } catch (NumberFormatException e) { - Assertions.assertTrue(e.getMessage().contains("The value '-129' could not be correctly converted to a byte. " + + assertTrue(e.getMessage().contains("The value '-129' could not be correctly converted to a byte. " + "Integer to byte conversion requires the use of only 8 bits (with a range of min/max = -128/255)!")); } @@ -665,7 +670,7 @@ public class TbUtilsTest { TbUtils.bytesToString(listIntBytes); Assertions.fail("Should throw NumberFormatException"); } catch (NumberFormatException e) { - Assertions.assertTrue(e.getMessage().contains("The value '256' could not be correctly converted to a byte. " + + assertTrue(e.getMessage().contains("The value '256' could not be correctly converted to a byte. " + "Integer to byte conversion requires the use of only 8 bits (with a range of min/max = -128/255)!")); } @@ -677,7 +682,7 @@ public class TbUtilsTest { TbUtils.bytesToString(listObjects); Assertions.fail("Should throw NumberFormatException"); } catch (NumberFormatException e) { - Assertions.assertTrue(e.getMessage().contains("The value '[0xFD]' could not be correctly converted to a byte. " + + assertTrue(e.getMessage().contains("The value '[0xFD]' could not be correctly converted to a byte. " + "Must be a HexDecimal/String/Integer/Byte format !")); } } @@ -798,25 +803,25 @@ public class TbUtilsTest { input = "0x01752B0367FA000500010488FFFFFFFFFFFFFFFF3"; TbUtils.hexToBytes(ctx, input); } catch (IllegalArgumentException e) { - Assertions.assertTrue(e.getMessage().contains("Hex string must be even-length.")); + assertTrue(e.getMessage().contains("Hex string must be even-length.")); } try { input = "0x01752B0367KA000500010488FFFFFFFFFFFFFFFF33"; TbUtils.hexToBytes(ctx, input); } catch (NumberFormatException e) { - Assertions.assertTrue(e.getMessage().contains("Value: \"" + input + "\" is not numeric or hexDecimal format!")); + assertTrue(e.getMessage().contains("Value: \"" + input + "\" is not numeric or hexDecimal format!")); } try { input = ""; TbUtils.hexToBytes(ctx, input); } catch (IllegalArgumentException e) { - Assertions.assertTrue(e.getMessage().contains("Hex string must be not empty")); + assertTrue(e.getMessage().contains("Hex string must be not empty")); } try { input = null; TbUtils.hexToBytes(ctx, input); } catch (IllegalArgumentException e) { - Assertions.assertTrue(e.getMessage().contains("Hex string must be not empty")); + assertTrue(e.getMessage().contains("Hex string must be not empty")); } } @@ -905,14 +910,14 @@ public class TbUtilsTest { TbUtils.raiseError(message); Assertions.fail("Should throw NumberFormatException"); } catch (RuntimeException e) { - Assertions.assertTrue(e.getMessage().contains("frequency_weighting_type must be 0, 1 or 2. A value of 4 is invalid.")); + assertTrue(e.getMessage().contains("frequency_weighting_type must be 0, 1 or 2. A value of 4 is invalid.")); } message = "frequency_weighting_type must be 0, 1 or 2."; try { TbUtils.raiseError(message); Assertions.fail("Should throw NumberFormatException"); } catch (RuntimeException e) { - Assertions.assertTrue(e.getMessage().contains("frequency_weighting_type must be 0, 1 or 2.")); + assertTrue(e.getMessage().contains("frequency_weighting_type must be 0, 1 or 2.")); } } @@ -1114,7 +1119,7 @@ public class TbUtilsTest { String emptyInput = Base64.getEncoder().encodeToString(new byte[]{}); actual = TbUtils.base64ToBytesList(ctx, emptyInput); - Assertions.assertTrue(actual.isEmpty()); + assertTrue(actual.isEmpty()); String invalidInput = "NotAValidBase64String"; Assertions.assertThrows(IllegalArgumentException.class, () -> { TbUtils.base64ToBytesList(ctx, invalidInput); @@ -1146,28 +1151,50 @@ public class TbUtilsTest { @Test public void isNaN() { - Assertions.assertFalse(TbUtils.isNaN(doubleVal)); - Assertions.assertTrue(TbUtils.isNaN(Double.NaN)); + assertFalse(TbUtils.isNaN(doubleVal)); + assertTrue(TbUtils.isNaN(Double.NaN)); } @Test public void isInsidePolygon() { // outside the polygon String perimeter = "[[[50.75581142688204,29.097910166341073],[50.16785158177623,29.35066098977171],[50.164329922384674,29.773743889862114],[50.16785158177623,30.801230932938843],[50.459245308833495,30.92760634465418],[50.486522489629564,30.68548421850448],[50.703612031034005,30.872660513473573]],[[50.606017492632766,29.36165015600782],[50.54317104075835,29.762754723626013],[50.41021974600505,29.455058069014804]]]"; - Assertions.assertFalse(TbUtils.isInsidePolygon(50.50869555168039, 30.80123093293884, perimeter)); + assertFalse(TbUtils.isInsidePolygon(50.50869555168039, 30.80123093293884, perimeter)); // inside the polygon - Assertions.assertTrue(TbUtils.isInsidePolygon(50.50520628167696, 30.339685951022016, perimeter)); + assertTrue(TbUtils.isInsidePolygon(50.50520628167696, 30.339685951022016, perimeter)); // inside the hole - Assertions.assertFalse(TbUtils.isInsidePolygon(50.52265651287081, 29.488025567723156, perimeter)); + assertFalse(TbUtils.isInsidePolygon(50.52265651287081, 29.488025567723156, perimeter)); } @Test public void isInsideCircle() { // outside the circle String perimeter = "{\"latitude\":50.32254778825905,\"longitude\":28.207787701215757,\"radius\":47477.33130420423}"; - Assertions.assertFalse(TbUtils.isInsideCircle(50.81490715736681, 28.05943395702824, perimeter)); + assertFalse(TbUtils.isInsideCircle(50.81490715736681, 28.05943395702824, perimeter)); // inside the circle - Assertions.assertTrue(TbUtils.isInsideCircle(50.599397971892444, 28.086906872618542, perimeter)); + assertTrue(TbUtils.isInsideCircle(50.599397971892444, 28.086906872618542, perimeter)); + } + + @Test + public void isMap() throws ExecutionException, InterruptedException { + LinkedHashMap msg = new LinkedHashMap<>(Map.of("temperature", 42, "nested", "508")); + assertTrue(TbUtils.isMap(msg)); + assertFalse(TbUtils.isList(msg)); + } + + @Test + public void isList() throws ExecutionException, InterruptedException { + List liat = List.of(0x35); + assertTrue(TbUtils.isList(liat)); + assertFalse(TbUtils.isMap(liat)); + assertFalse(TbUtils.isArray(liat)); + } + + @Test + public void isArray() throws ExecutionException, InterruptedException { + byte [] array = new byte[]{1, 2, 3}; + assertTrue(TbUtils.isArray(array)); + assertFalse(TbUtils.isList(array)); } private static List toList(byte[] data) { From cb2ab78186f3a13e4408a0dccd3b0b07e3ab5bbf Mon Sep 17 00:00:00 2001 From: nickAS21 Date: Wed, 14 May 2025 17:41:36 +0300 Subject: [PATCH 63/63] lwm2m: comments - 1 --- .../rpc/AbstractRpcLwM2MIntegrationTest.java | 4 +- .../sql/RpcLwm2mIntegrationObserveTest.java | 8 +-- .../lwm2m/TelemetryObserveStrategy.java | 2 +- ...esource.java => ResourceUpdateResult.java} | 8 +-- .../uplink/DefaultLwM2mUplinkMsgHandler.java | 62 +++++++++++-------- 5 files changed, 47 insertions(+), 37 deletions(-) rename common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/{ResultUpdateResource.java => ResourceUpdateResult.java} (84%) diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java index fe571eb9b3..be85294f08 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/AbstractRpcLwM2MIntegrationTest.java @@ -29,7 +29,7 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest; import org.thingsboard.server.transport.lwm2m.server.LwM2mTransportServerHelper; -import org.thingsboard.server.transport.lwm2m.server.client.ResultUpdateResource; +import org.thingsboard.server.transport.lwm2m.server.client.ResourceUpdateResult; import java.util.List; import java.util.Set; @@ -259,7 +259,7 @@ public abstract class AbstractRpcLwM2MIntegrationTest extends AbstractLwM2MInteg .filter(invocation -> invocation.getMethod().getName().equals("updateAttrTelemetry") && invocation.getArguments().length > 1 && - ((ResultUpdateResource)invocation.getArguments()[0]).getPaths().toString().contains(idVerRez) + ((ResourceUpdateResult)invocation.getArguments()[0]).getPaths().toString().contains(idVerRez) ) .count(); } diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java index 1ba89720ae..483442382d 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/rpc/sql/RpcLwm2mIntegrationObserveTest.java @@ -24,7 +24,7 @@ import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import org.thingsboard.server.transport.lwm2m.rpc.AbstractRpcLwM2MIntegrationTest; -import org.thingsboard.server.transport.lwm2m.server.client.ResultUpdateResource; +import org.thingsboard.server.transport.lwm2m.server.client.ResourceUpdateResult; import static org.eclipse.leshan.core.LwM2mId.ACCESS_CONTROL; import static org.junit.Assert.assertEquals; @@ -80,7 +80,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT int cntUpdate = 3; verify(defaultUplinkMsgHandlerTest, timeout(10000).atLeast(cntUpdate)) - .updateAttrTelemetry(Mockito.any(ResultUpdateResource.class), eq(null)); + .updateAttrTelemetry(Mockito.any(ResourceUpdateResult.class), eq(null)); } /** @@ -95,7 +95,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT int cntUpdate = 3; verify(defaultUplinkMsgHandlerTest, timeout(10000).atLeast(cntUpdate)) - .updateAttrTelemetry(Mockito.any(ResultUpdateResource.class), eq(null)); + .updateAttrTelemetry(Mockito.any(ResourceUpdateResult.class), eq(null)); } /** @@ -328,7 +328,7 @@ public class RpcLwm2mIntegrationObserveTest extends AbstractRpcLwM2MIntegrationT int cntUpdate = 10; verify(defaultUplinkMsgHandlerTest, timeout(50000).atLeast(cntUpdate)) - .updateAttrTelemetry(Mockito.any(ResultUpdateResource.class), eq(null)); + .updateAttrTelemetry(Mockito.any(ResourceUpdateResult.class), eq(null)); } private void sendRpcObserveWithWithTwoResource(String expectedId_1, String expectedId_2) throws Exception { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryObserveStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryObserveStrategy.java index 264d883353..c3e525633f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryObserveStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/lwm2m/TelemetryObserveStrategy.java @@ -40,7 +40,7 @@ public enum TelemetryObserveStrategy { return strategy; } } - return null; + throw new IllegalArgumentException("Unknown TelemetryObserveStrategy id: " + description); } public static TelemetryObserveStrategy fromId(int id) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultUpdateResource.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResourceUpdateResult.java similarity index 84% rename from common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultUpdateResource.java rename to common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResourceUpdateResult.java index ffbf4595fb..0a47d211b5 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResultUpdateResource.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/ResourceUpdateResult.java @@ -23,11 +23,11 @@ import java.util.Set; @Data @AllArgsConstructor -public class ResultUpdateResource { - LwM2mClient lwM2MClient; - Set paths; +public class ResourceUpdateResult { + private LwM2mClient lwM2MClient; + private Set paths; - public ResultUpdateResource(LwM2mClient lwM2MClient) { + public ResourceUpdateResult(LwM2mClient lwM2MClient) { this.lwM2MClient = lwM2MClient; this.paths = new HashSet<>(); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java index 5897533539..37f7829c12 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java @@ -78,7 +78,7 @@ import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientState; import org.thingsboard.server.transport.lwm2m.server.client.LwM2MClientStateException; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; -import org.thingsboard.server.transport.lwm2m.server.client.ResultUpdateResource; +import org.thingsboard.server.transport.lwm2m.server.client.ResourceUpdateResult; import org.thingsboard.server.transport.lwm2m.server.client.ResultsAddKeyValueProto; import org.thingsboard.server.transport.lwm2m.server.common.LwM2MExecutorAwareService; import org.thingsboard.server.transport.lwm2m.server.downlink.DownlinkRequestCallback; @@ -124,7 +124,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.COMPOSITE_ALL; import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.COMPOSITE_BY_OBJECT; import static org.thingsboard.server.common.data.device.profile.lwm2m.TelemetryObserveStrategy.SINGLE; import static org.thingsboard.server.common.data.lwm2m.LwM2mConstants.LWM2M_SEPARATOR_PATH; @@ -324,7 +323,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl LwM2mClient lwM2MClient = clientContext.getClientByEndpoint(registration.getEndpoint()); ObjectModel objectModelVersion = lwM2MClient.getObjectModel(path, modelProvider); if (objectModelVersion != null) { - ResultUpdateResource updateResource = new ResultUpdateResource(lwM2MClient); + ResourceUpdateResult updateResource = new ResourceUpdateResult(lwM2MClient); int responseCode = response.getCode().getCode(); if (content instanceof LwM2mObject) { this.updateObjectResourceValue(updateResource, (LwM2mObject) content, path, responseCode); @@ -343,7 +342,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl log.trace("ReadCompositeResponse before onUpdateValueAfterReadCompositeResponse: [{}]", response); if (response.getContent() != null) { LwM2mClient lwM2MClient = clientContext.getClientByEndpoint(registration.getEndpoint()); - ResultUpdateResource updateResource = new ResultUpdateResource(lwM2MClient); + ResourceUpdateResult updateResource = new ResourceUpdateResult(lwM2MClient); response.getContent().forEach((k, v) -> { if (v != null) { int responseCode = response.getCode().getCode(); @@ -386,7 +385,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl LwM2mClient lwM2MClient = clientContext.getClientByEndpoint(registration.getEndpoint()); ObjectModel objectModelVersion = lwM2MClient.getObjectModel(path.toString(), modelProvider); if (objectModelVersion != null) { - ResultUpdateResource updateResource = new ResultUpdateResource(lwM2MClient); + ResourceUpdateResult updateResource = new ResourceUpdateResult(lwM2MClient); if (node instanceof LwM2mObject) { this.updateObjectResourceValue(updateResource, (LwM2mObject) node, path.toString(), 0); } else if (node instanceof LwM2mObjectInstance) { @@ -518,19 +517,30 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl targetIds = targetIds.stream().filter(target -> isSupportedTargetId(supportedObjects, target)).collect(Collectors.toSet()); if (!targetIds.isEmpty()) { TelemetryObserveStrategy observeStrategy = profile.getObserveAttr().getObserveStrategy(); - if (SINGLE.equals(observeStrategy)) { - CountDownLatch latch = new CountDownLatch(targetIds.size()); - targetIds.forEach(targetId -> sendObserveRequest(lwM2MClient, targetId, - new TbLwM2MLatchCallback<>(latch, new TbLwM2MObserveCallback(this, logService, lwM2MClient, targetId)))); - latch.await(config.getTimeout(), TimeUnit.MILLISECONDS); - } else if (COMPOSITE_ALL.equals(observeStrategy)) { - String[] versionedIds = targetIds.toArray(new String[0]); - sendObserveCompositeRequest(lwM2MClient, versionedIds); - } else if (COMPOSITE_BY_OBJECT.equals(observeStrategy)) { - Map versionedObjectIds = groupByObjectIdVersionedIds(targetIds); - CountDownLatch latch = new CountDownLatch(versionedObjectIds.size()); - versionedObjectIds.forEach((k, v)-> sendObserveCompositeRequest(lwM2MClient, v)); - latch.await(config.getTimeout(), TimeUnit.MILLISECONDS); + long timeoutMs = config.getTimeout(); + switch (observeStrategy) { + case SINGLE -> { + CountDownLatch latch = new CountDownLatch(targetIds.size()); + targetIds.forEach(targetId -> sendObserveRequest( + lwM2MClient, targetId, + new TbLwM2MLatchCallback<>(latch, new TbLwM2MObserveCallback(this, logService, lwM2MClient, targetId)) + )); + boolean completed = latch.await(timeoutMs, TimeUnit.MILLISECONDS); + if (!completed) log.trace("[{}] Timeout occurred during SINGLE observe init", lwM2MClient.getEndpoint()); + } + case COMPOSITE_ALL -> { + CountDownLatch latch = new CountDownLatch(targetIds.size()); + sendObserveCompositeRequest(lwM2MClient, targetIds.toArray(new String[0])); + boolean completed = latch.await(timeoutMs, TimeUnit.MILLISECONDS); + if (!completed) log.trace("[{}] Timeout occurred during COMPOSITE_ALL observe init", lwM2MClient.getEndpoint()); + } + case COMPOSITE_BY_OBJECT -> { + Map versionedObjectIds = groupByObjectIdVersionedIds(targetIds); + CountDownLatch latch = new CountDownLatch(versionedObjectIds.size()); + versionedObjectIds.forEach((k, v) -> sendObserveCompositeRequest(lwM2MClient, v)); + boolean completed = latch.await(timeoutMs, TimeUnit.MILLISECONDS); + if (!completed) log.trace("[{}] Timeout occurred during COMPOSITE_BY_OBJECT observe init", lwM2MClient.getEndpoint()); + } } } } catch (InterruptedException e) { @@ -586,7 +596,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl defaultLwM2MDownlinkMsgHandler.sendCancelObserveRequest(client, request, new TbLwM2MCancelObserveCallback(logService, client, versionedId)); } - private void updateObjectResourceValue(ResultUpdateResource updateResource, LwM2mObject lwM2mObject, String pathIdVer, int code) { + private void updateObjectResourceValue(ResourceUpdateResult updateResource, LwM2mObject lwM2mObject, String pathIdVer, int code) { LwM2mPath pathIds = new LwM2mPath(fromVersionedIdToObjectId(pathIdVer)); lwM2mObject.getInstances().forEach((instanceId, instance) -> { String pathInstance = pathIds.toString() + "/" + instanceId; @@ -594,7 +604,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl }); } - private void updateObjectInstanceResourceValue(ResultUpdateResource updateResource, LwM2mObjectInstance lwM2mObjectInstance, String pathIdVer, int code) { + private void updateObjectInstanceResourceValue(ResourceUpdateResult updateResource, LwM2mObjectInstance lwM2mObjectInstance, String pathIdVer, int code) { lwM2mObjectInstance.getResources().forEach((resourceId, resource) -> { String pathRez = pathIdVer + "/" + resourceId; this.updateResourcesValue(updateResource, resource, pathRez, Mode.UPDATE, code); @@ -612,7 +622,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl * @param stringPath - resource * @param mode - Replace, Update */ - private void updateResourcesValue(ResultUpdateResource updateResource, LwM2mResource lwM2mResource, String stringPath, Mode mode, int code) { + private void updateResourcesValue(ResourceUpdateResult updateResource, LwM2mResource lwM2mResource, String stringPath, Mode mode, int code) { LwM2mClient lwM2MClient = updateResource.getLwM2MClient(); String path = convertObjectIdToVersionedId(stringPath, lwM2MClient); if (path != null && lwM2MClient.saveResourceValue(path, lwM2mResource, modelProvider, mode)) { @@ -660,7 +670,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl * * @param updateResource - updateResource resource of LwM2M Client */ - public void updateAttrTelemetry(ResultUpdateResource updateResource, Instant ts) { + public void updateAttrTelemetry(ResourceUpdateResult updateResource, Instant ts) { log.trace("UpdateAttrTelemetry paths [{}]", updateResource.getPaths()); try { ResultsAddKeyValueProto results = this.getParametersFromProfile(updateResource); @@ -707,7 +717,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl * * @param updateResource - updateResource resource of LwM2M Client */ - private ResultsAddKeyValueProto getParametersFromProfile(ResultUpdateResource updateResource) { + private ResultsAddKeyValueProto getParametersFromProfile(ResourceUpdateResult updateResource) { Registration registration = updateResource.getLwM2MClient().getRegistration(); Set paths = updateResource.getPaths(); ResultsAddKeyValueProto results = new ResultsAddKeyValueProto(); @@ -823,7 +833,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl @Override public void onWriteResponseOk(LwM2mClient lwM2MClient, String path, WriteRequest request, int code) { - ResultUpdateResource updateResource = new ResultUpdateResource(lwM2MClient); + ResourceUpdateResult updateResource = new ResourceUpdateResult(lwM2MClient); if (request.getNode() instanceof LwM2mResource) { this.updateResourcesValue(updateResource, ((LwM2mResource) request.getNode()), path, request.isReplaceRequest() ? Mode.REPLACE : Mode.UPDATE, code); } else if (request.getNode() instanceof LwM2mObjectInstance) { @@ -840,7 +850,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl @Override public void onCreatebjectInstancesResponseOk(LwM2mClient lwM2MClient, String versionId, CreateRequest request) { if (request.getObjectInstances() != null && !request.getObjectInstances().isEmpty()) { - ResultUpdateResource updateResource = new ResultUpdateResource(lwM2MClient); + ResourceUpdateResult updateResource = new ResourceUpdateResult(lwM2MClient); request.getObjectInstances().forEach(instance -> instance.getResources().forEach((resId, lwM2mResource) ->{ this.updateResourcesValue(updateResource, lwM2mResource, versionId + "/" + resId, Mode.REPLACE, 0); @@ -854,7 +864,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl @Override public void onWriteCompositeResponseOk(LwM2mClient lwM2MClient, WriteCompositeRequest request, int code) { log.trace("ReadCompositeResponse: [{}]", request.getNodes()); - ResultUpdateResource updateResource = new ResultUpdateResource(lwM2MClient); + ResourceUpdateResult updateResource = new ResourceUpdateResult(lwM2MClient); request.getNodes().forEach((k, v) -> { if (v instanceof LwM2mSingleResource) { this.updateResourcesValue(updateResource, (LwM2mResource) v, k.toString(), Mode.REPLACE, code);