Browse Source

Merge branch 'feature/entity-alarm-rules' of github.com:thingsboard/thingsboard into feature/entity-agg-cf

pull/14253/head
IrynaMatveieva 11 months ago
parent
commit
16dfa9279f
  1. 11
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  2. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java
  3. 1
      application/src/test/java/org/thingsboard/server/transport/coap/security/AbstractCoapSecurityIntegrationTest.java
  4. 4
      common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java
  5. 6
      common/message/src/main/java/org/thingsboard/server/common/msg/rule/engine/DeviceAttributesEventNotificationMsg.java
  6. 3
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java
  7. 3
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
  8. 3
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java
  9. 3
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java
  10. 6
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java
  11. 4
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java
  12. 3
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java
  13. 3
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java
  14. 11
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java
  15. 49
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java
  16. 3
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionContext.java
  17. 3
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java
  18. 5
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java
  19. 12
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttTopicMatcher.java
  20. 3
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java
  21. 4
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsEvictEvent.java
  22. 27
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java
  23. 42
      dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceTest.java
  24. 3
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java
  25. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts
  26. 29
      ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component.ts
  27. 18
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html
  28. 11
      ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts
  29. 5
      ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts
  30. 12
      ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts
  31. 5
      ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts
  32. 3
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html
  33. 24
      ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts
  34. 2
      ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts
  35. 4
      ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.html
  36. 12
      ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts
  37. 2
      ui-ngx/src/app/shared/components/time-unit-input.component.ts
  38. 11
      ui-ngx/src/app/shared/models/calculated-field.models.ts

11
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java

@ -115,14 +115,9 @@ import java.util.concurrent.TimeUnit;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/**
* @author Andrew Shvayka
*/
@Slf4j @Slf4j
public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
static final String SESSION_TIMEOUT_MESSAGE = "session timeout!";
final TenantId tenantId; final TenantId tenantId;
final DeviceId deviceId; final DeviceId deviceId;
final LinkedHashMapRemoveEldest<UUID, SessionInfoMetaData> sessions; final LinkedHashMapRemoveEldest<UUID, SessionInfoMetaData> sessions;
@ -178,7 +173,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
private EdgeId findRelatedEdgeId() { private EdgeId findRelatedEdgeId() {
List<EntityRelation> result = List<EntityRelation> result =
systemContext.getRelationService().findByToAndType(tenantId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE); systemContext.getRelationService().findByToAndType(tenantId, deviceId, EntityRelation.CONTAINS_TYPE, RelationTypeGroup.EDGE);
if (result != null && result.size() > 0) { if (result != null && !result.isEmpty()) {
EntityRelation relationToEdge = result.get(0); EntityRelation relationToEdge = result.get(0);
if (relationToEdge.getFrom() != null && relationToEdge.getFrom().getId() != null) { if (relationToEdge.getFrom() != null && relationToEdge.getFrom().getId() != null) {
log.trace("[{}][{}] found edge [{}] for device", tenantId, deviceId, relationToEdge.getFrom().getId()); log.trace("[{}][{}] found edge [{}] for device", tenantId, deviceId, relationToEdge.getFrom().getId());
@ -501,7 +496,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
UUID sessionId = getSessionId(sessionInfo); UUID sessionId = getSessionId(sessionInfo);
DeviceId deviceId = new DeviceId(new UUID(msg.getDeviceIdMSB(), msg.getDeviceIdLSB())); DeviceId deviceId = new DeviceId(new UUID(msg.getDeviceIdMSB(), msg.getDeviceIdLSB()));
ListenableFuture<Void> registrationFuture = systemContext.getClaimDevicesService() ListenableFuture<Void> registrationFuture = systemContext.getClaimDevicesService()
.registerClaimingInfo(tenantId, deviceId, msg.getSecretKey(), msg.getDurationMs()); .registerClaimingInfo(tenantId, deviceId, msg.getSecretKey(), msg.getDurationMs());
Futures.addCallback(registrationFuture, new FutureCallback<>() { Futures.addCallback(registrationFuture, new FutureCallback<>() {
@Override @Override
public void onSuccess(Void result) { public void onSuccess(Void result) {
@ -723,7 +718,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
toDeviceRpcPendingMap.remove(requestId); toDeviceRpcPendingMap.remove(requestId);
status = RpcStatus.FAILED; status = RpcStatus.FAILED;
response = JacksonUtil.newObjectNode().put("error", "There was a Timeout and all retry " + response = JacksonUtil.newObjectNode().put("error", "There was a Timeout and all retry " +
"attempts have been exhausted. Retry attempts set: " + maxRpcRetries); "attempts have been exhausted. Retry attempts set: " + maxRpcRetries);
} }
} else { } else {
md.setRetries(md.getRetries() + 1); md.setRetries(md.getRetries() + 1);

2
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java

@ -193,7 +193,7 @@ public abstract class AbstractBulkImportService<E extends HasId<? extends Entity
}); });
return Map.entry(kvType, kvs); return Map.entry(kvType, kvs);
}) })
.filter(kvsEntry -> kvsEntry.getValue().entrySet().size() > 0) .filter(kvsEntry -> !kvsEntry.getValue().entrySet().isEmpty())
.forEach(kvsEntry -> { .forEach(kvsEntry -> {
BulkImportColumnType kvType = kvsEntry.getKey(); BulkImportColumnType kvType = kvsEntry.getKey();
if (kvType == BulkImportColumnType.SHARED_ATTRIBUTE || kvType == BulkImportColumnType.SERVER_ATTRIBUTE) { if (kvType == BulkImportColumnType.SHARED_ATTRIBUTE || kvType == BulkImportColumnType.SERVER_ATTRIBUTE) {

1
application/src/test/java/org/thingsboard/server/transport/coap/security/AbstractCoapSecurityIntegrationTest.java

@ -64,6 +64,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
"coap.server.enabled=true", "coap.server.enabled=true",
"coap.dtls.enabled=true", "coap.dtls.enabled=true",
"coap.dtls.credentials.pem.cert_file=coap/credentials/server/cert.pem", "coap.dtls.credentials.pem.cert_file=coap/credentials/server/cert.pem",
"coap.dtls.x509.skip_validity_check_for_client_cert=true",
"device.connectivity.coaps.enabled=true", "device.connectivity.coaps.enabled=true",
"service.integrations.supported=ALL", "service.integrations.supported=ALL",
"transport.coap.enabled=true", "transport.coap.enabled=true",

4
common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java

@ -24,11 +24,15 @@ import org.thingsboard.server.common.data.HasVersion;
import org.thingsboard.server.common.data.id.DeviceCredentialsId; import org.thingsboard.server.common.data.id.DeviceCredentialsId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import java.io.Serial;
@Schema @Schema
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class DeviceCredentials extends BaseData<DeviceCredentialsId> implements DeviceCredentialsFilter, HasVersion { public class DeviceCredentials extends BaseData<DeviceCredentialsId> implements DeviceCredentialsFilter, HasVersion {
@Serial
private static final long serialVersionUID = -7869261127032877765L; private static final long serialVersionUID = -7869261127032877765L;
private DeviceId deviceId; private DeviceId deviceId;
private DeviceCredentialsType credentialsType; private DeviceCredentialsType credentialsType;
private String credentialsId; private String credentialsId;

6
common/message/src/main/java/org/thingsboard/server/common/msg/rule/engine/DeviceAttributesEventNotificationMsg.java

@ -23,16 +23,15 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg;
import java.io.Serial;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
/**
* @author Andrew Shvayka
*/
@Data @Data
public class DeviceAttributesEventNotificationMsg implements ToDeviceActorNotificationMsg { public class DeviceAttributesEventNotificationMsg implements ToDeviceActorNotificationMsg {
@Serial
private static final long serialVersionUID = 2422071590415277039L; private static final long serialVersionUID = 2422071590415277039L;
private final TenantId tenantId; private final TenantId tenantId;
@ -56,4 +55,5 @@ public class DeviceAttributesEventNotificationMsg implements ToDeviceActorNotifi
public MsgType getMsgType() { public MsgType getMsgType() {
return MsgType.DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG; return MsgType.DEVICE_ATTRIBUTES_UPDATE_TO_DEVICE_ACTOR_MSG;
} }
} }

3
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java

@ -46,9 +46,6 @@ import java.security.cert.X509Certificate;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/**
* Created by valerii.sosliuk on 11/6/16.
*/
@Slf4j @Slf4j
@Component("MqttSslHandlerProvider") @Component("MqttSslHandlerProvider")
@ConditionalOnProperty(prefix = "transport.mqtt.ssl", value = "enabled", havingValue = "true", matchIfMissing = false) @ConditionalOnProperty(prefix = "transport.mqtt.ssl", value = "enabled", havingValue = "true", matchIfMissing = false)

3
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java

@ -133,9 +133,6 @@ import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetr
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopic.parseTopic; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopic.parseTopic;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.parseTopicPublish; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.parseTopicPublish;
/**
* @author Andrew Shvayka
*/
@Slf4j @Slf4j
public class MqttTransportHandler extends ChannelInboundHandlerAdapter implements GenericFutureListener<Future<? super Void>>, SessionMsgListener { public class MqttTransportHandler extends ChannelInboundHandlerAdapter implements GenericFutureListener<Future<? super Void>>, SessionMsgListener {

3
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java

@ -25,9 +25,6 @@ import io.netty.handler.ssl.SslHandler;
import org.thingsboard.server.transport.mqtt.limits.IpFilter; import org.thingsboard.server.transport.mqtt.limits.IpFilter;
import org.thingsboard.server.transport.mqtt.limits.ProxyIpFilter; import org.thingsboard.server.transport.mqtt.limits.ProxyIpFilter;
/**
* @author Andrew Shvayka
*/
public class MqttTransportServerInitializer extends ChannelInitializer<SocketChannel> { public class MqttTransportServerInitializer extends ChannelInitializer<SocketChannel> {
private final MqttTransportContext context; private final MqttTransportContext context;

3
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java

@ -35,9 +35,6 @@ import org.thingsboard.server.common.data.TbTransportService;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
/**
* @author Andrew Shvayka
*/
@Service("MqttTransportService") @Service("MqttTransportService")
@TbMqttTransportComponent @TbMqttTransportComponent
@Slf4j @Slf4j

6
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java

@ -45,10 +45,6 @@ import java.util.UUID;
import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_SOFTWARE_FIRMWARE_RESPONSES_TOPIC_FORMAT; import static org.thingsboard.server.common.data.device.profile.MqttTopics.DEVICE_SOFTWARE_FIRMWARE_RESPONSES_TOPIC_FORMAT;
/**
* @author Andrew Shvayka
*/
@Component @Component
@Slf4j @Slf4j
public class JsonMqttAdaptor implements MqttTransportAdaptor { public class JsonMqttAdaptor implements MqttTransportAdaptor {
@ -122,7 +118,7 @@ public class JsonMqttAdaptor implements MqttTransportAdaptor {
public Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException { public Optional<MqttMessage> convertToGatewayPublish(MqttDeviceAwareSessionContext ctx, String deviceName, TransportProtos.GetAttributeResponseMsg responseMsg) throws AdaptorException {
return processConvertFromGatewayAttributeResponseMsg(ctx, deviceName, responseMsg); return processConvertFromGatewayAttributeResponseMsg(ctx, deviceName, responseMsg);
} }
@Override @Override
public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.AttributeUpdateNotificationMsg notificationMsg, String topic) { public Optional<MqttMessage> convertToPublish(MqttDeviceAwareSessionContext ctx, TransportProtos.AttributeUpdateNotificationMsg notificationMsg, String topic) {
return Optional.of(createMqttPublishMsg(ctx, topic, JsonConverter.toJson(notificationMsg))); return Optional.of(createMqttPublishMsg(ctx, topic, JsonConverter.toJson(notificationMsg)));

4
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java

@ -41,9 +41,6 @@ import org.thingsboard.server.transport.mqtt.session.MqttDeviceAwareSessionConte
import java.util.Optional; import java.util.Optional;
/**
* @author Andrew Shvayka
*/
public interface MqttTransportAdaptor { public interface MqttTransportAdaptor {
ByteBufAllocator ALLOCATOR = new UnpooledByteBufAllocator(false); ByteBufAllocator ALLOCATOR = new UnpooledByteBufAllocator(false);
@ -90,4 +87,5 @@ public interface MqttTransportAdaptor {
payload.writeBytes(payloadInBytes); payload.writeBytes(payloadInBytes);
return new MqttPublishMessage(mqttFixedHeader, header, payload); return new MqttPublishMessage(mqttFixedHeader, header, payload);
} }
} }

3
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/ProtoMqttAdaptor.java

@ -48,7 +48,7 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor {
public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException {
DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx;
byte[] bytes = toBytes(inbound.payload()); byte[] bytes = toBytes(inbound.payload());
Descriptors.Descriptor telemetryDynamicMsgDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getTelemetryDynamicMsgDescriptor()); Descriptors.Descriptor telemetryDynamicMsgDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getTelemetryDynamicMessageDescriptor());
try { try {
return JsonConverter.convertToTelemetryProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor))); return JsonConverter.convertToTelemetryProto(JsonParser.parseString(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor)));
} catch (Exception e) { } catch (Exception e) {
@ -228,4 +228,5 @@ public class ProtoMqttAdaptor implements MqttTransportAdaptor {
private int getRequestId(String topicName, String topic) { private int getRequestId(String topicName, String topic) {
return Integer.parseInt(topicName.substring(topic.length())); return Integer.parseInt(topicName.substring(topic.length()));
} }
} }

3
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java

@ -33,9 +33,6 @@ import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
/**
* Created by ashvayka on 19.01.17.
*/
@ToString(callSuper = true) @ToString(callSuper = true)
@Slf4j @Slf4j
public abstract class AbstractGatewayDeviceSessionContext<T extends AbstractGatewaySessionHandler> extends MqttDeviceAwareSessionContext implements SessionMsgListener { public abstract class AbstractGatewayDeviceSessionContext<T extends AbstractGatewaySessionHandler> extends MqttDeviceAwareSessionContext implements SessionMsgListener {

11
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java

@ -96,9 +96,6 @@ import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConn
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.STATE; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.STATE;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.messageName; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.messageName;
/**
* Created by ashvayka on 19.01.17.
*/
@Slf4j @Slf4j
public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDeviceSessionContext> { public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDeviceSessionContext> {
@ -116,6 +113,7 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
@Getter @Getter
protected final UUID sessionId; protected final UUID sessionId;
private final ConcurrentMap<String, Lock> deviceCreationLockMap; private final ConcurrentMap<String, Lock> deviceCreationLockMap;
@Getter
private final ConcurrentMap<String, T> devices; private final ConcurrentMap<String, T> devices;
private final ConcurrentMap<String, ListenableFuture<T>> deviceFutures; private final ConcurrentMap<String, ListenableFuture<T>> deviceFutures;
protected final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap; protected final ConcurrentMap<MqttTopicMatcher, Integer> mqttQoSMap;
@ -821,11 +819,7 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
transportService.process(sessionInfo, postTelemetryMsg, pubAckCallback); transportService.process(sessionInfo, postTelemetryMsg, pubAckCallback);
} }
public ConcurrentMap<String, T> getDevices() { protected <T> TransportServiceCallback<Void> getAggregatePubAckCallback(
return this.devices;
}
protected <T>TransportServiceCallback<Void> getAggregatePubAckCallback(
final ChannelHandlerContext ctx, final ChannelHandlerContext ctx,
final int msgId, final int msgId,
final String deviceName, final String deviceName,
@ -915,4 +909,5 @@ public abstract class AbstractGatewaySessionHandler<T extends AbstractGatewayDev
log.trace("Failed to send device disconnect to gateway session", e); log.trace("Failed to send device disconnect to gateway session", e);
} }
} }
} }

49
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/DeviceSessionCtx.java

@ -49,9 +49,6 @@ import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer; import java.util.function.Consumer;
/**
* @author Andrew Shvayka
*/
@Slf4j @Slf4j
public class DeviceSessionCtx extends MqttDeviceAwareSessionContext { public class DeviceSessionCtx extends MqttDeviceAwareSessionContext {
@ -84,13 +81,18 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext {
private volatile MqttTopicFilter attributesSubscribeTopicFilter = MqttTopicFilterFactory.getDefaultAttributesFilter(); private volatile MqttTopicFilter attributesSubscribeTopicFilter = MqttTopicFilterFactory.getDefaultAttributesFilter();
@Getter @Getter
private volatile TransportPayloadType payloadType = TransportPayloadType.JSON; private volatile TransportPayloadType payloadType = TransportPayloadType.JSON;
@Getter
private volatile Descriptors.Descriptor attributesDynamicMessageDescriptor; private volatile Descriptors.Descriptor attributesDynamicMessageDescriptor;
@Getter
private volatile Descriptors.Descriptor telemetryDynamicMessageDescriptor; private volatile Descriptors.Descriptor telemetryDynamicMessageDescriptor;
@Getter
private volatile Descriptors.Descriptor rpcResponseDynamicMessageDescriptor; private volatile Descriptors.Descriptor rpcResponseDynamicMessageDescriptor;
@Getter
private volatile DynamicMessage.Builder rpcRequestDynamicMessageBuilder; private volatile DynamicMessage.Builder rpcRequestDynamicMessageBuilder;
private volatile MqttTransportAdaptor adaptor; private volatile MqttTransportAdaptor adaptor;
private volatile boolean jsonPayloadFormatCompatibilityEnabled; private volatile boolean jsonPayloadFormatCompatibilityEnabled;
private volatile boolean useJsonPayloadFormatForDefaultDownlinkTopics; private volatile boolean useJsonPayloadFormatForDefaultDownlinkTopics;
@Getter
private volatile boolean sendAckOnValidationException; private volatile boolean sendAckOnValidationException;
@Getter @Getter
@ -131,26 +133,6 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext {
return payloadType.equals(TransportPayloadType.JSON); return payloadType.equals(TransportPayloadType.JSON);
} }
public boolean isSendAckOnValidationException() {
return sendAckOnValidationException;
}
public Descriptors.Descriptor getTelemetryDynamicMsgDescriptor() {
return telemetryDynamicMessageDescriptor;
}
public Descriptors.Descriptor getAttributesDynamicMessageDescriptor() {
return attributesDynamicMessageDescriptor;
}
public Descriptors.Descriptor getRpcResponseDynamicMessageDescriptor() {
return rpcResponseDynamicMessageDescriptor;
}
public DynamicMessage.Builder getRpcRequestDynamicMessageBuilder() {
return rpcRequestDynamicMessageBuilder;
}
@Override @Override
public void setDeviceProfile(DeviceProfile deviceProfile) { public void setDeviceProfile(DeviceProfile deviceProfile) {
super.setDeviceProfile(deviceProfile); super.setDeviceProfile(deviceProfile);
@ -166,8 +148,7 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext {
private void updateDeviceSessionConfiguration(DeviceProfile deviceProfile) { private void updateDeviceSessionConfiguration(DeviceProfile deviceProfile) {
DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration();
if (transportConfiguration.getType().equals(DeviceTransportType.MQTT) && if (transportConfiguration.getType().equals(DeviceTransportType.MQTT) &&
transportConfiguration instanceof MqttDeviceProfileTransportConfiguration) { transportConfiguration instanceof MqttDeviceProfileTransportConfiguration mqttConfig) {
MqttDeviceProfileTransportConfiguration mqttConfig = (MqttDeviceProfileTransportConfiguration) transportConfiguration;
TransportPayloadTypeConfiguration transportPayloadTypeConfiguration = mqttConfig.getTransportPayloadTypeConfiguration(); TransportPayloadTypeConfiguration transportPayloadTypeConfiguration = mqttConfig.getTransportPayloadTypeConfiguration();
payloadType = transportPayloadTypeConfiguration.getTransportPayloadType(); payloadType = transportPayloadTypeConfiguration.getTransportPayloadType();
deviceProfileMqttTransportType = true; deviceProfileMqttTransportType = true;
@ -199,16 +180,12 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext {
} }
public MqttTransportAdaptor getAdaptor(TopicType topicType) { public MqttTransportAdaptor getAdaptor(TopicType topicType) {
switch (topicType) { return switch (topicType) {
case V2: case V2 -> getDefaultAdaptor();
return getDefaultAdaptor(); case V2_JSON -> context.getJsonMqttAdaptor();
case V2_JSON: case V2_PROTO -> context.getProtoMqttAdaptor();
return context.getJsonMqttAdaptor(); default -> useJsonPayloadFormatForDefaultDownlinkTopics ? context.getJsonMqttAdaptor() : getDefaultAdaptor();
case V2_PROTO: };
return context.getProtoMqttAdaptor();
default:
return useJsonPayloadFormatForDefaultDownlinkTopics ? context.getJsonMqttAdaptor() : getDefaultAdaptor();
}
} }
private MqttTransportAdaptor getDefaultAdaptor() { private MqttTransportAdaptor getDefaultAdaptor() {
@ -269,7 +246,7 @@ public class DeviceSessionCtx extends MqttDeviceAwareSessionContext {
} }
} }
public Collection<MqttMessage> getMsgQueueSnapshot(){ public Collection<MqttMessage> getMsgQueueSnapshot() {
return Collections.unmodifiableCollection(msgQueue); return Collections.unmodifiableCollection(msgQueue);
} }

3
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionContext.java

@ -22,9 +22,6 @@ import org.thingsboard.server.common.transport.auth.TransportDeviceInfo;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
/**
* Created by nickAS21 on 26.12.22
*/
@ToString(callSuper = true) @ToString(callSuper = true)
public class GatewayDeviceSessionContext extends AbstractGatewayDeviceSessionContext<GatewaySessionHandler> { public class GatewayDeviceSessionContext extends AbstractGatewayDeviceSessionContext<GatewaySessionHandler> {

3
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java

@ -28,9 +28,6 @@ import org.thingsboard.server.gen.transport.TransportProtos;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
/**
* Created by nickAS21 on 26.12.22
*/
@Slf4j @Slf4j
public class GatewaySessionHandler extends AbstractGatewaySessionHandler<GatewayDeviceSessionContext> { public class GatewaySessionHandler extends AbstractGatewaySessionHandler<GatewayDeviceSessionContext> {

5
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java

@ -25,9 +25,6 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/**
* Created by ashvayka on 30.08.18.
*/
@ToString(callSuper = true) @ToString(callSuper = true)
public abstract class MqttDeviceAwareSessionContext extends DeviceAwareSessionContext { public abstract class MqttDeviceAwareSessionContext extends DeviceAwareSessionContext {
@ -47,7 +44,7 @@ public abstract class MqttDeviceAwareSessionContext extends DeviceAwareSessionCo
.stream() .stream()
.filter(entry -> entry.getKey().matches(topic)) .filter(entry -> entry.getKey().matches(topic))
.map(Map.Entry::getValue) .map(Map.Entry::getValue)
.collect(Collectors.toList()); .toList();
if (!qosList.isEmpty()) { if (!qosList.isEmpty()) {
return MqttQoS.valueOf(qosList.get(0)); return MqttQoS.valueOf(qosList.get(0));
} else { } else {

12
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttTopicMatcher.java

@ -15,26 +15,25 @@
*/ */
package org.thingsboard.server.transport.mqtt.session; package org.thingsboard.server.transport.mqtt.session;
import lombok.Getter;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class MqttTopicMatcher { public class MqttTopicMatcher {
@Getter
private final String topic; private final String topic;
private final Pattern topicRegex; private final Pattern topicRegex;
public MqttTopicMatcher(String topic) { public MqttTopicMatcher(String topic) {
if(topic == null){ if (topic == null) {
throw new NullPointerException("topic"); throw new NullPointerException("topic");
} }
this.topic = topic; this.topic = topic;
this.topicRegex = Pattern.compile(topic.replace("+", "[^/]+").replace("#", ".+") + "$"); this.topicRegex = Pattern.compile(topic.replace("+", "[^/]+").replace("#", ".+") + "$");
} }
public String getTopic() { public boolean matches(String topic) {
return topic;
}
public boolean matches(String topic){
return this.topicRegex.matcher(topic).matches(); return this.topicRegex.matcher(topic).matches();
} }
@ -52,4 +51,5 @@ public class MqttTopicMatcher {
public int hashCode() { public int hashCode() {
return topic.hashCode(); return topic.hashCode();
} }
} }

3
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java

@ -64,9 +64,6 @@ import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetr
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_SPLIT_REGEXP; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_SPLIT_REGEXP;
import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_STATE_REGEXP; import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicService.TOPIC_STATE_REGEXP;
/**
* Created by nickAS21 on 12.12.22
*/
@Slf4j @Slf4j
@SpecVersion(spec = "sparkplug", version = "3.0.0") @SpecVersion(spec = "sparkplug", version = "3.0.0")
public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler<SparkplugDeviceSessionContext> { public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler<SparkplugDeviceSessionContext> {

4
dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsEvictEvent.java

@ -18,9 +18,9 @@ package org.thingsboard.server.dao.device;
import lombok.Data; import lombok.Data;
@Data @Data
class DeviceCredentialsEvictEvent { public class DeviceCredentialsEvictEvent {
private final String newCedentialsId; private final String newCredentialsId;
private final String oldCredentialsId; private final String oldCredentialsId;
} }

27
dao/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsServiceImpl.java

@ -47,6 +47,8 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException;
import org.thingsboard.server.dao.service.validator.DeviceCredentialsDataValidator; import org.thingsboard.server.dao.service.validator.DeviceCredentialsDataValidator;
import java.util.Objects;
import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validateString; import static org.thingsboard.server.dao.service.Validator.validateString;
@ -61,8 +63,8 @@ public class DeviceCredentialsServiceImpl extends AbstractCachedEntityService<St
@TransactionalEventListener(classes = DeviceCredentialsEvictEvent.class) @TransactionalEventListener(classes = DeviceCredentialsEvictEvent.class)
@Override @Override
public void handleEvictEvent(DeviceCredentialsEvictEvent event) { public void handleEvictEvent(DeviceCredentialsEvictEvent event) {
cache.evict(event.getNewCedentialsId()); cache.evict(event.getNewCredentialsId());
if (StringUtils.isNotEmpty(event.getOldCredentialsId()) && !event.getNewCedentialsId().equals(event.getOldCredentialsId())) { if (StringUtils.isNotEmpty(event.getOldCredentialsId()) && !event.getNewCredentialsId().equals(event.getOldCredentialsId())) {
cache.evict(event.getOldCredentialsId()); cache.evict(event.getOldCredentialsId());
} }
} }
@ -107,7 +109,7 @@ public class DeviceCredentialsServiceImpl extends AbstractCachedEntityService<St
try { try {
var value = deviceCredentialsDao.saveAndFlush(tenantId, deviceCredentials); var value = deviceCredentialsDao.saveAndFlush(tenantId, deviceCredentials);
publishEvictEvent(new DeviceCredentialsEvictEvent(value.getCredentialsId(), oldDeviceCredentials != null ? oldDeviceCredentials.getCredentialsId() : null)); publishEvictEvent(new DeviceCredentialsEvictEvent(value.getCredentialsId(), oldDeviceCredentials != null ? oldDeviceCredentials.getCredentialsId() : null));
if (oldDeviceCredentials != null) { if (oldDeviceCredentials != null && isCredentialsChanged(oldDeviceCredentials, value)) {
eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).entity(value).entityId(value.getDeviceId()).actionType(ActionType.CREDENTIALS_UPDATED).build()); eventPublisher.publishEvent(ActionEntityEvent.builder().tenantId(tenantId).entity(value).entityId(value.getDeviceId()).actionType(ActionType.CREDENTIALS_UPDATED).build());
} }
return value; return value;
@ -140,13 +142,11 @@ public class DeviceCredentialsServiceImpl extends AbstractCachedEntityService<St
@Override @Override
public JsonNode toCredentialsInfo(DeviceCredentials deviceCredentials) { public JsonNode toCredentialsInfo(DeviceCredentials deviceCredentials) {
switch (deviceCredentials.getCredentialsType()) { return switch (deviceCredentials.getCredentialsType()) {
case ACCESS_TOKEN: case ACCESS_TOKEN -> JacksonUtil.valueToTree(deviceCredentials.getCredentialsId());
return JacksonUtil.valueToTree(deviceCredentials.getCredentialsId()); case X509_CERTIFICATE -> JacksonUtil.valueToTree(deviceCredentials.getCredentialsValue());
case X509_CERTIFICATE: default -> JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), JsonNode.class);
return JacksonUtil.valueToTree(deviceCredentials.getCredentialsValue()); };
}
return JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), JsonNode.class);
} }
private void formatSimpleMqttCredentials(DeviceCredentials deviceCredentials) { private void formatSimpleMqttCredentials(DeviceCredentials deviceCredentials) {
@ -407,4 +407,11 @@ public class DeviceCredentialsServiceImpl extends AbstractCachedEntityService<St
} }
} }
private boolean isCredentialsChanged(DeviceCredentials oldCredentials, DeviceCredentials newCredentials) {
return !Objects.equals(oldCredentials.getCredentialsId(), newCredentials.getCredentialsId())
|| oldCredentials.getCredentialsType() != newCredentials.getCredentialsType()
|| !Objects.equals(oldCredentials.getCredentialsValue(), newCredentials.getCredentialsValue())
|| !Objects.equals(oldCredentials.getDeviceId(), newCredentials.getDeviceId());
}
} }

42
dao/src/test/java/org/thingsboard/server/dao/service/DeviceCredentialsServiceTest.java

@ -19,7 +19,10 @@ import com.datastax.oss.driver.api.core.uuid.Uuids;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.DeviceCredentialsId; import org.thingsboard.server.common.data.id.DeviceCredentialsId;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
@ -27,6 +30,7 @@ import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
@DaoSqlTest @DaoSqlTest
@ -36,6 +40,8 @@ public class DeviceCredentialsServiceTest extends AbstractServiceTest {
DeviceCredentialsService deviceCredentialsService; DeviceCredentialsService deviceCredentialsService;
@Autowired @Autowired
DeviceService deviceService; DeviceService deviceService;
@MockitoBean
ApplicationEventPublisher eventPublisher;
@Test @Test
public void testCreateDeviceCredentials() { public void testCreateDeviceCredentials() {
@ -185,5 +191,39 @@ public class DeviceCredentialsServiceTest extends AbstractServiceTest {
Assert.assertEquals(deviceCredentials, foundDeviceCredentials); Assert.assertEquals(deviceCredentials, foundDeviceCredentials);
deviceService.deleteDevice(tenantId, savedDevice.getId()); deviceService.deleteDevice(tenantId, savedDevice.getId());
} }
}
@Test
public void testUpdateDeviceCredentialsWithSameValuesDoesNotPublishEvent() {
Device device = new Device();
device.setTenantId(tenantId);
device.setName("My device");
device.setType("default");
Device savedDevice = deviceService.saveDevice(device);
try {
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, savedDevice.getId());
Assert.assertNotNull(deviceCredentials);
DeviceCredentials updatedCredentials = new DeviceCredentials(deviceCredentials.getId());
updatedCredentials.setDeviceId(deviceCredentials.getDeviceId());
updatedCredentials.setCredentialsType(deviceCredentials.getCredentialsType());
updatedCredentials.setCredentialsId(deviceCredentials.getCredentialsId());
updatedCredentials.setCredentialsValue(deviceCredentials.getCredentialsValue());
Mockito.reset(eventPublisher);
DeviceCredentials result = deviceCredentialsService.updateDeviceCredentials(tenantId, updatedCredentials);
Assert.assertEquals(deviceCredentials.getCredentialsId(), result.getCredentialsId());
Assert.assertEquals(deviceCredentials.getCredentialsType(), result.getCredentialsType());
Assert.assertEquals(deviceCredentials.getCredentialsValue(), result.getCredentialsValue());
Assert.assertEquals(deviceCredentials.getDeviceId(), result.getDeviceId());
Mockito.verify(eventPublisher, Mockito.never()).publishEvent(Mockito.any(ActionEntityEvent.class));
} finally {
deviceService.deleteDevice(tenantId, savedDevice.getId());
}
}
}

3
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java

@ -15,9 +15,6 @@
*/ */
package org.thingsboard.rule.engine.api; package org.thingsboard.rule.engine.api;
/**
* Created by ashvayka on 02.04.18.
*/
public interface RuleEngineTelemetryService { public interface RuleEngineTelemetryService {
void saveTimeseries(TimeseriesSaveRequest request); void saveTimeseries(TimeseriesSaveRequest request);

3
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/calculated-field-argument-panel.component.ts

@ -71,6 +71,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
@Input() hiddenEntityTypes = false; @Input() hiddenEntityTypes = false;
@Input() defaultValueRequired = false; @Input() defaultValueRequired = false;
@Input() hint: string; @Input() hint: string;
@Input() predefinedEntityFilter: EntityFilter;
@Input() argumentEntityTypes = Object.values(ArgumentEntityType).filter(value => value !== ArgumentEntityType.RelationQuery) as ArgumentEntityType[]; @Input() argumentEntityTypes = Object.values(ArgumentEntityType).filter(value => value !== ArgumentEntityType.RelationQuery) as ArgumentEntityType[];
@ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent; @ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent;
@ -222,6 +223,8 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI
} }
if (!onInit) { if (!onInit) {
this.argumentFormGroup.get('refEntityKey').get('key').setValue(''); this.argumentFormGroup.get('refEntityKey').get('key').setValue('');
} else if (this.predefinedEntityFilter) {
entityFilter = this.predefinedEntityFilter;
} }
this.entityFilter = entityFilter; this.entityFilter = entityFilter;
this.cd.markForCheck(); this.cd.markForCheck();

29
ui-ngx/src/app/modules/home/components/calculated-fields/components/calculated-field-arguments/related-aggregation-arguments-table.component.ts

@ -14,7 +14,15 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { ChangeDetectorRef, Component, DestroyRef, forwardRef, Renderer2, ViewContainerRef, } from '@angular/core'; import {
ChangeDetectorRef,
Component,
DestroyRef,
forwardRef,
Input,
Renderer2,
ViewContainerRef,
} from '@angular/core';
import { FormBuilder, NG_VALIDATORS, NG_VALUE_ACCESSOR, } from '@angular/forms'; import { FormBuilder, NG_VALIDATORS, NG_VALUE_ACCESSOR, } from '@angular/forms';
import { TbPopoverService } from '@shared/components/popover.service'; import { TbPopoverService } from '@shared/components/popover.service';
import { EntityService } from '@core/http/entity.service'; import { EntityService } from '@core/http/entity.service';
@ -23,7 +31,9 @@ import { AppState } from '@core/core.state';
import { import {
CalculatedFieldArgumentsTableComponent CalculatedFieldArgumentsTableComponent
} from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component'; } from '@home/components/calculated-fields/components/calculated-field-arguments/calculated-field-arguments-table.component';
import { ArgumentEntityType } from '@shared/models/calculated-field.models'; import { ArgumentEntityType, RelationPathLevel } from '@shared/models/calculated-field.models';
import { AliasFilterType } from '@shared/models/alias.models';
import { EntityType } from '@shared/models/entity-type.models';
@Component({ @Component({
selector: 'tb-related-aggregation-arguments-table', selector: 'tb-related-aggregation-arguments-table',
@ -44,6 +54,21 @@ import { ArgumentEntityType } from '@shared/models/calculated-field.models';
}) })
export class RelatedAggregationArgumentsTableComponent extends CalculatedFieldArgumentsTableComponent { export class RelatedAggregationArgumentsTableComponent extends CalculatedFieldArgumentsTableComponent {
@Input({required: true})
set relation(value: RelationPathLevel) {
this.panelAdditionalCtx.predefinedEntityFilter = {
type: AliasFilterType.relationsQuery,
rootStateEntity: false,
rootEntity: this.entityId,
direction: value.direction,
filters: [{
relationType: value.relationType,
entityTypes: [EntityType.DEVICE, EntityType.ASSET, EntityType.CUSTOMER, EntityType.TENANT]
}],
maxLevel: 1,
};
}
constructor( constructor(
protected fb: FormBuilder, protected fb: FormBuilder,
protected popoverService: TbPopoverService, protected popoverService: TbPopoverService,

18
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html

@ -64,23 +64,26 @@
</div> </div>
@switch (fieldFormGroup.get('type').value) { @switch (fieldFormGroup.get('type').value) {
@case (CalculatedFieldType.GEOFENCING) { @case (CalculatedFieldType.GEOFENCING) {
<tb-geofencing-configuration formControlName="configuration" [entityId]="data.entityId" [entityName]="data.entityName" [tenantId]="data.tenantId"> <tb-geofencing-configuration formControlName="configuration"
</tb-geofencing-configuration> [entityId]="data.entityId"
[entityName]="data.entityName"
[tenantId]="data.tenantId"
></tb-geofencing-configuration>
} }
@case (CalculatedFieldType.PROPAGATION) { @case (CalculatedFieldType.PROPAGATION) {
<tb-propagation-configuration formControlName="configuration" <tb-propagation-configuration formControlName="configuration"
[entityId]="data.entityId" [entityId]="data.entityId"
[entityName]="data.entityName" [entityName]="data.entityName"
[tenantId]="data.tenantId" [tenantId]="data.tenantId"
[testScript]="onTestScript.bind(this)"> [testScript]="onTestScript.bind(this)"
</tb-propagation-configuration> ></tb-propagation-configuration>
} }
@case (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) { @case (CalculatedFieldType.RELATED_ENTITIES_AGGREGATION) {
<tb-related-entities-aggregation-component formControlName="configuration" <tb-related-entities-aggregation-component formControlName="configuration"
[entityId]="data.entityId" [entityId]="data.entityId"
[entityName]="data.entityName" [entityName]="data.entityName"
[tenantId]="data.tenantId"> [tenantId]="data.tenantId"
</tb-related-entities-aggregation-component> ></tb-related-entities-aggregation-component>
} }
@default { @default {
<tb-simple-configuration formControlName="configuration" <tb-simple-configuration formControlName="configuration"
@ -89,8 +92,7 @@
[tenantId]="data.tenantId" [tenantId]="data.tenantId"
[isScript]="fieldFormGroup.get('type').value === CalculatedFieldType.SCRIPT" [isScript]="fieldFormGroup.get('type').value === CalculatedFieldType.SCRIPT"
[testScript]="onTestScript.bind(this)" [testScript]="onTestScript.bind(this)"
> ></tb-simple-configuration>
</tb-simple-configuration>
} }
} }
</div> </div>

11
ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts

@ -30,7 +30,7 @@ import {
} from '@shared/models/calculated-field.models'; } from '@shared/models/calculated-field.models';
import { oneSpaceInsideRegex } from '@shared/models/regex.constants'; import { oneSpaceInsideRegex } from '@shared/models/regex.constants';
import { EntityType } from '@shared/models/entity-type.models'; import { EntityType } from '@shared/models/entity-type.models';
import { switchMap } from 'rxjs/operators'; import { pairwise, switchMap } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@ -106,7 +106,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
onTestScript(): Observable<string> { onTestScript(): Observable<string> {
const calculatedFieldId = this.data.value?.id?.id; const calculatedFieldId = this.data.value?.id?.id;
if (calculatedFieldId) { if (calculatedFieldId) {
return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId) return this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId, {ignoreLoading: true})
.pipe( .pipe(
switchMap(event => { switchMap(event => {
const args = event?.arguments ? JSON.parse(event.arguments) : null; const args = event?.arguments ? JSON.parse(event.arguments) : null;
@ -121,6 +121,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
private applyDialogData(): void { private applyDialogData(): void {
const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {}; const { configuration = {} as CalculatedFieldConfiguration, type = CalculatedFieldType.SIMPLE, debugSettings = { failuresEnabled: true, allEnabled: true }, ...value } = this.data.value ?? {};
this.fieldFormGroup.patchValue({ configuration, type, debugSettings, ...value }, {emitEvent: false}); this.fieldFormGroup.patchValue({ configuration, type, debugSettings, ...value }, {emitEvent: false});
setTimeout(() => this.fieldFormGroup.get('type').updateValueAndValidity({onlySelf: true}));
} }
private observeIsLoading(): void { private observeIsLoading(): void {
@ -138,9 +139,11 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
private observeType(): void { private observeType(): void {
this.fieldFormGroup.get('type').valueChanges.pipe( this.fieldFormGroup.get('type').valueChanges.pipe(
pairwise(),
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
).subscribe((type) => { ).subscribe(([prevType, nextType]) => {
if (type !== CalculatedFieldType.SIMPLE && type !== CalculatedFieldType.SCRIPT) { if (![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(prevType) ||
![CalculatedFieldType.SIMPLE, CalculatedFieldType.SCRIPT].includes(nextType)) {
this.fieldFormGroup.get('configuration').setValue(({} as CalculatedFieldConfiguration), {emitEvent: false}); this.fieldFormGroup.get('configuration').setValue(({} as CalculatedFieldConfiguration), {emitEvent: false});
} }
}); });

5
ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-configuration/geofencing-configuration.component.ts

@ -31,6 +31,7 @@ import {
CalculatedFieldOutput, CalculatedFieldOutput,
CalculatedFieldType, CalculatedFieldType,
getCalculatedFieldCurrentEntityFilter, getCalculatedFieldCurrentEntityFilter,
notEmptyObjectValidator,
OutputType OutputType
} from '@shared/models/calculated-field.models'; } from '@shared/models/calculated-field.models';
import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models';
@ -76,7 +77,7 @@ export class GeofencingConfigurationComponent implements ControlValueAccessor, V
latitudeKeyName: [null, [Validators.required]], latitudeKeyName: [null, [Validators.required]],
longitudeKeyName: [null, [Validators.required]], longitudeKeyName: [null, [Validators.required]],
}), }),
zoneGroups: this.fb.control<Record<string, CalculatedFieldGeofencing>>({}), zoneGroups: this.fb.control<Record<string, CalculatedFieldGeofencing>>({}, notEmptyObjectValidator()),
scheduledUpdateEnabled: [true], scheduledUpdateEnabled: [true],
scheduledUpdateInterval: [this.minAllowedScheduledUpdateIntervalInSecForCF], scheduledUpdateInterval: [this.minAllowedScheduledUpdateIntervalInSecForCF],
output: this.fb.control<CalculatedFieldOutput>({scope: AttributeScope.SERVER_SCOPE, type: OutputType.Timeseries}) output: this.fb.control<CalculatedFieldOutput>({scope: AttributeScope.SERVER_SCOPE, type: OutputType.Timeseries})
@ -114,7 +115,7 @@ export class GeofencingConfigurationComponent implements ControlValueAccessor, V
} }
validate(): ValidationErrors | null { validate(): ValidationErrors | null {
return this.geofencingConfiguration.valid || this.geofencingConfiguration.status === "DISABLED" ? null : { geofencingConfigError: false }; return this.geofencingConfiguration.valid || this.geofencingConfiguration.disabled ? null : { geofencingConfigError: false };
} }
writeValue(config: CalculatedFieldGeofencingConfiguration): void { writeValue(config: CalculatedFieldGeofencingConfiguration): void {

12
ui-ngx/src/app/modules/home/components/calculated-fields/components/output/calculated-field-output.component.ts

@ -113,7 +113,7 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
} }
validate(): ValidationErrors | null { validate(): ValidationErrors | null {
return this.outputForm.valid ? null : {outputConfig: false}; return this.outputForm.valid || this.outputForm.disabled ? null : {outputConfig: false};
} }
writeValue(value: CalculatedFieldOutput | CalculatedFieldSimpleOutput): void { writeValue(value: CalculatedFieldOutput | CalculatedFieldSimpleOutput): void {
@ -127,6 +127,16 @@ export class CalculatedFieldOutputComponent implements ControlValueAccessor, Val
registerOnTouched(_: any): void { } registerOnTouched(_: any): void { }
setDisabledState(isDisabled: boolean): void {
if (isDisabled) {
this.outputForm.disable({emitEvent: false});
} else {
this.outputForm.enable({emitEvent: false});
this.updatedFormWithMode();
this.toggleScopeByOutputType(this.outputForm.get('type').value);
}
}
private updatedModel(value: CalculatedFieldOutput | CalculatedFieldSimpleOutput) { private updatedModel(value: CalculatedFieldOutput | CalculatedFieldSimpleOutput) {
if (this.simpleMode && 'name' in value) { if (this.simpleMode && 'name' in value) {
value.name = value.name?.trim() ?? ''; value.name = value.name?.trim() ?? '';

5
ui-ngx/src/app/modules/home/components/calculated-fields/components/propagation-configuration/propagation-configuration.component.ts

@ -33,6 +33,7 @@ import {
CalculatedFieldType, CalculatedFieldType,
getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsEditorCompleter,
getCalculatedFieldArgumentsHighlights, getCalculatedFieldArgumentsHighlights,
notEmptyObjectValidator,
OutputType, OutputType,
PropagationDirectionTranslations, PropagationDirectionTranslations,
PropagationWithExpression PropagationWithExpression
@ -74,7 +75,7 @@ export class PropagationConfigurationComponent implements ControlValueAccessor,
testScript: () => Observable<string>; testScript: () => Observable<string>;
propagateConfiguration = this.fb.group({ propagateConfiguration = this.fb.group({
arguments: this.fb.control({}), arguments: this.fb.control({}, notEmptyObjectValidator()),
applyExpressionToResolvedArguments: [false], applyExpressionToResolvedArguments: [false],
relation: this.fb.group({ relation: this.fb.group({
direction: [EntitySearchDirection.TO, Validators.required], direction: [EntitySearchDirection.TO, Validators.required],
@ -122,7 +123,7 @@ export class PropagationConfigurationComponent implements ControlValueAccessor,
} }
validate(): ValidationErrors | null { validate(): ValidationErrors | null {
return this.propagateConfiguration.valid || this.propagateConfiguration.status === "DISABLED" ? null : {invalidPropagateConfig: false}; return this.propagateConfiguration.valid || this.propagateConfiguration.disabled ? null : {invalidPropagateConfig: false};
} }
writeValue(value: PropagationWithExpression): void { writeValue(value: PropagationWithExpression): void {

3
ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.html

@ -47,7 +47,8 @@
<tb-related-aggregation-arguments-table formControlName="arguments" <tb-related-aggregation-arguments-table formControlName="arguments"
[entityId]="entityId" [entityId]="entityId"
[tenantId]="tenantId" [tenantId]="tenantId"
[entityName]="entityName"/> [entityName]="entityName"
[relation]="relatedAggregationConfiguration.get('relation').value"/>
</div> </div>
<div class="tb-form-panel"> <div class="tb-form-panel">
<div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'calculated-fields.hint.metrics' | translate }}"> <div class="tb-form-panel-title" tb-hint-tooltip-icon="{{ 'calculated-fields.hint.metrics' | translate }}">

24
ui-ngx/src/app/modules/home/components/calculated-fields/components/related-entities-aggregation-configuration/related-entities-aggregation-component.component.ts

@ -32,6 +32,7 @@ import {
CalculatedFieldType, CalculatedFieldType,
getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsEditorCompleter,
getCalculatedFieldArgumentsHighlights, getCalculatedFieldArgumentsHighlights,
notEmptyObjectValidator,
OutputType, OutputType,
PropagationDirectionTranslations PropagationDirectionTranslations
} from '@shared/models/calculated-field.models'; } from '@shared/models/calculated-field.models';
@ -72,14 +73,21 @@ export class RelatedEntitiesAggregationComponentComponent implements ControlValu
@Input({required: true}) @Input({required: true})
entityName: string; entityName: string;
readonly ScriptLanguage = ScriptLanguage;
readonly CalculatedFieldType = CalculatedFieldType;
readonly OutputType = OutputType;
readonly Directions = Object.values(EntitySearchDirection) as Array<EntitySearchDirection>;
readonly PropagationDirectionTranslations = PropagationDirectionTranslations;
readonly minAllowedDeduplicationIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedDeduplicationIntervalInSecForCF;
relatedAggregationConfiguration = this.fb.group({ relatedAggregationConfiguration = this.fb.group({
relation: this.fb.group({ relation: this.fb.group({
direction: [EntitySearchDirection.FROM, Validators.required], direction: [EntitySearchDirection.FROM, Validators.required],
relationType: ['Contains', Validators.required], relationType: ['Contains', Validators.required],
}), }),
arguments: this.fb.control({}), arguments: this.fb.control({}, notEmptyObjectValidator()),
metrics: this.fb.control({}), metrics: this.fb.control({}, notEmptyObjectValidator()),
deduplicationIntervalInSec: [], deduplicationIntervalInSec: [this.minAllowedDeduplicationIntervalInSecForCF],
output: this.fb.control<CalculatedFieldOutput>({ output: this.fb.control<CalculatedFieldOutput>({
scope: AttributeScope.SERVER_SCOPE, scope: AttributeScope.SERVER_SCOPE,
type: OutputType.Timeseries, type: OutputType.Timeseries,
@ -87,14 +95,6 @@ export class RelatedEntitiesAggregationComponentComponent implements ControlValu
useLatestTs: [false] useLatestTs: [false]
}); });
readonly ScriptLanguage = ScriptLanguage;
readonly CalculatedFieldType = CalculatedFieldType;
readonly OutputType = OutputType;
readonly Directions = Object.values(EntitySearchDirection) as Array<EntitySearchDirection>;
readonly PropagationDirectionTranslations = PropagationDirectionTranslations;
readonly minAllowedDeduplicationIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedDeduplicationIntervalInSecForCF;
arguments$ = this.relatedAggregationConfiguration.get('arguments').valueChanges.pipe( arguments$ = this.relatedAggregationConfiguration.get('arguments').valueChanges.pipe(
map(argumentsObj => Object.keys(argumentsObj)) map(argumentsObj => Object.keys(argumentsObj))
); );
@ -120,7 +120,7 @@ export class RelatedEntitiesAggregationComponentComponent implements ControlValu
} }
validate(): ValidationErrors | null { validate(): ValidationErrors | null {
return this.relatedAggregationConfiguration.valid || this.relatedAggregationConfiguration.status === "DISABLED" ? null : {invalidPropagateConfig: false}; return this.relatedAggregationConfiguration.valid || this.relatedAggregationConfiguration.disabled ? null : {invalidPropagateConfig: false};
} }
writeValue(value: CalculatedFieldRelatedAggregationConfiguration): void { writeValue(value: CalculatedFieldRelatedAggregationConfiguration): void {

2
ui-ngx/src/app/modules/home/components/calculated-fields/components/simple-configuration/simple-configuration.component.ts

@ -140,7 +140,7 @@ export class SimpleConfigurationComponent implements ControlValueAccessor, Valid
} }
validate(): ValidationErrors | null { validate(): ValidationErrors | null {
return this.simpleConfiguration.valid || this.simpleConfiguration.status === "DISABLED" ? null : {invalidSimpleConfig: false}; return this.simpleConfiguration.valid || this.simpleConfiguration.disabled ? null : {invalidSimpleConfig: false};
} }
writeValue(value: SimpeConfiguration): void { writeValue(value: SimpeConfiguration): void {

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

@ -22,13 +22,13 @@
required required
(focusin)="keyInputSubject.next()" (focusin)="keyInputSubject.next()"
[matAutocomplete]="keysAutocomplete"> [matAutocomplete]="keysAutocomplete">
@if (keyControl.value) { @if (keyControl.value && keyControl.enabled) {
<button type="button" <button type="button"
matSuffix mat-icon-button aria-label="Clear" matSuffix mat-icon-button aria-label="Clear"
(click)="clear()"> (click)="clear()">
<mat-icon class="material-icons">close</mat-icon> <mat-icon class="material-icons">close</mat-icon>
</button> </button>
} @else if (keyControl.hasError('required') && keyControl.touched) { } @else if (keyControl.hasError('required') && keyControl.touched && keyControl.enabled) {
<mat-icon matSuffix <mat-icon matSuffix
matTooltipPosition="above" matTooltipPosition="above"
matTooltipClass="tb-error-tooltip" matTooltipClass="tb-error-tooltip"

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

@ -84,7 +84,7 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val
return this.cachedResult ? of(this.cachedResult) : this.entityService.findEntityKeysByQuery({ return this.cachedResult ? of(this.cachedResult) : this.entityService.findEntityKeysByQuery({
pageLink: { page: 0, pageSize: 100 }, pageLink: { page: 0, pageSize: 100 },
entityFilter: this.entityFilter(), entityFilter: this.entityFilter(),
}, this.dataKeyType() === DataKeyType.attribute, this.dataKeyType() === DataKeyType.timeseries, this.keyScopeType()); }, this.dataKeyType() === DataKeyType.attribute, this.dataKeyType() === DataKeyType.timeseries, this.keyScopeType(), {ignoreLoading: true});
}), }),
map(result => { map(result => {
this.cachedResult = result; this.cachedResult = result;
@ -152,10 +152,18 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val
registerOnTouched(_): void {} registerOnTouched(_): void {}
validate(): ValidationErrors | null { validate(): ValidationErrors | null {
return this.keyControl.valid ? null : { keyControl: false }; return this.keyControl.valid || this.keyControl.disabled ? null : { keyControl: false };
} }
writeValue(value: string): void { writeValue(value: string): void {
this.keyControl.patchValue(value, {emitEvent: false}); this.keyControl.patchValue(value, {emitEvent: false});
} }
setDisabledState(isDisabled: boolean): void {
if (isDisabled) {
this.keyControl.disable({emitEvent: false});
} else {
this.keyControl.enable({emitEvent: false});
}
}
} }

2
ui-ngx/src/app/shared/components/time-unit-input.component.ts

@ -200,7 +200,7 @@ export class TimeUnitInputComponent implements ControlValueAccessor, Validator,
} }
validate(): ValidationErrors | null { validate(): ValidationErrors | null {
return this.timeInputForm.valid ? null : { return this.timeInputForm.disabled || this.timeInputForm.valid ? null : {
timeInput: false timeInput: false
}; };
} }

11
ui-ngx/src/app/shared/models/calculated-field.models.ts

@ -30,6 +30,7 @@ import {
endGroupHighlightRule endGroupHighlightRule
} from '@shared/models/ace/ace.models'; } from '@shared/models/ace/ace.models';
import { EntitySearchDirection } from '@shared/models/relation.models'; import { EntitySearchDirection } from '@shared/models/relation.models';
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
interface BaseCalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasEntityDebugSettings, HasTenantId, ExportableEntity<CalculatedFieldId> { interface BaseCalculatedField extends Omit<BaseData<CalculatedFieldId>, 'label'>, HasVersion, HasEntityDebugSettings, HasTenantId, ExportableEntity<CalculatedFieldId> {
entityId: EntityId; entityId: EntityId;
@ -847,3 +848,13 @@ export const calculatedFieldDefaultScript =
'return {\n' + 'return {\n' +
' "temperatureC": (temperatureF - 32) / 1.8\n' + ' "temperatureC": (temperatureF - 32) / 1.8\n' +
'};' '};'
export function notEmptyObjectValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value;
if (typeof value === 'object' && value !== null && Object.keys(value).length === 0) {
return {emptyObject: true};
}
return null;
};
}

Loading…
Cancel
Save