diff --git a/application/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java b/application/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java new file mode 100644 index 0000000000..158f29a03a --- /dev/null +++ b/application/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java @@ -0,0 +1,68 @@ +/** + * Copyright © 2016-2024 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.springframework.http.converter.xml; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.util.Assert; + +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; + +/** + * RestTemplate firstly uses MappingJackson2XmlHttpMessageConverter converter instead of MappingJackson2HttpMessageConverter. + * It produces error UnsupportedMediaType, so this converter had to be shadowed for read and write operations to use the correct converter + */ +public class MappingJackson2XmlHttpMessageConverter extends AbstractJackson2HttpMessageConverter { + private static final List problemDetailMediaTypes; + + public MappingJackson2XmlHttpMessageConverter() { + this(Jackson2ObjectMapperBuilder.xml().build()); + } + + public MappingJackson2XmlHttpMessageConverter(ObjectMapper objectMapper) { + super(objectMapper, new MediaType[]{new MediaType("application", "xml", StandardCharsets.UTF_8), new MediaType("text", "xml", StandardCharsets.UTF_8), new MediaType("application", "*+xml", StandardCharsets.UTF_8)}); + Assert.isInstanceOf(XmlMapper.class, objectMapper, "XmlMapper required"); + } + + public void setObjectMapper(ObjectMapper objectMapper) { + Assert.isInstanceOf(XmlMapper.class, objectMapper, "XmlMapper required"); + super.setObjectMapper(objectMapper); + } + + protected List getMediaTypesForProblemDetail() { + return problemDetailMediaTypes; + } + + static { + problemDetailMediaTypes = Collections.singletonList(MediaType.APPLICATION_PROBLEM_XML); + } + + @Override + public boolean canRead(Type type, Class contextClass, MediaType mediaType) { + return false; + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + return false; + } +} diff --git a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java index 33b6045248..26c30f85d1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EdgeController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EdgeController.java @@ -490,7 +490,7 @@ public class EdgeController extends BaseController { if (fromEdgeSyncResponse.isSuccess()) { response.setResult(new ResponseEntity<>(HttpStatus.OK)); } else { - response.setErrorResult(new ThingsboardException("Edge is not connected", ThingsboardErrorCode.GENERAL)); + response.setErrorResult(new ThingsboardException(fromEdgeSyncResponse.getError(), ThingsboardErrorCode.GENERAL)); } } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index 04e6fe2185..46bf3f69a4 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -287,22 +287,31 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private void startSyncProcess(TenantId tenantId, EdgeId edgeId, UUID requestId) { EdgeGrpcSession session = sessions.get(edgeId); if (session != null) { - boolean success = false; - if (session.isConnected()) { - session.startSyncProcess(true); - success = true; + if (!session.isSyncCompleted()) { + clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, false, "Sync process is active at the moment")); + } else { + boolean success = false; + if (session.isConnected()) { + session.startSyncProcess(true); + success = true; + } + clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success, "")); } - clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success)); } } @Override public void processSyncRequest(ToEdgeSyncRequest request, Consumer responseConsumer) { - log.trace("[{}][{}] Processing sync edge request [{}]", request.getTenantId(), request.getId(), request.getEdgeId()); UUID requestId = request.getId(); - localSyncEdgeRequests.put(requestId, responseConsumer); - clusterService.pushEdgeSyncRequestToCore(request); - scheduleSyncRequestTimeout(request, requestId); + EdgeGrpcSession session = sessions.get(request.getEdgeId()); + if (session != null && !session.isSyncCompleted()) { + responseConsumer.accept(new FromEdgeSyncResponse(requestId, request.getTenantId(), request.getEdgeId(), false, "Sync process is active at the moment")); + } else { + log.trace("[{}][{}] Processing sync edge request [{}]", request.getTenantId(), request.getId(), request.getEdgeId()); + localSyncEdgeRequests.put(requestId, responseConsumer); + clusterService.pushEdgeSyncRequestToCore(request); + scheduleSyncRequestTimeout(request, requestId); + } } private void scheduleSyncRequestTimeout(ToEdgeSyncRequest request, UUID requestId) { @@ -312,7 +321,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i Consumer consumer = localSyncEdgeRequests.remove(requestId); if (consumer != null) { log.trace("[{}] timeout for processing sync edge request.", requestId); - consumer.accept(new FromEdgeSyncResponse(requestId, request.getTenantId(), request.getEdgeId(), false)); + consumer.accept(new FromEdgeSyncResponse(requestId, request.getTenantId(), request.getEdgeId(), false, "Edge is not connected")); } }, 20, TimeUnit.SECONDS); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 81f90fee51..97b28c083f 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -247,6 +247,7 @@ public final class EdgeGrpcSession implements Closeable { } }, ctx.getGrpcCallbackExecutorService()); } else { + log.info("[{}][{}] sync process completed", this.tenantId, edge.getId()); DownlinkMsg syncCompleteDownlinkMsg = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .setSyncCompletedMsg(SyncCompletedMsg.newBuilder().build()) @@ -325,7 +326,11 @@ public final class EdgeGrpcSession implements Closeable { } private void sendDownlinkMsg(ResponseMsg downlinkMsg) { - log.trace("[{}][{}] Sending downlink msg [{}]", this.tenantId, this.sessionId, downlinkMsg); + if (downlinkMsg.getDownlinkMsg().getWidgetTypeUpdateMsgCount() > 0) { + log.trace("[{}][{}] Sending downlink widgetTypeUpdateMsg, downlinkMsgId = {}", this.tenantId, this.sessionId, downlinkMsg.getDownlinkMsg().getDownlinkMsgId()); + } else { + log.trace("[{}][{}] Sending downlink msg [{}]", this.tenantId, this.sessionId, downlinkMsg); + } if (isConnected()) { downlinkMsgLock.lock(); try { @@ -337,7 +342,7 @@ public final class EdgeGrpcSession implements Closeable { } finally { downlinkMsgLock.unlock(); } - log.trace("[{}][{}] Response msg successfully sent [{}]", this.tenantId, this.sessionId, downlinkMsg); + log.trace("[{}][{}] Response msg successfully sent. downlinkMsgId = {}", this.tenantId, this.sessionId, downlinkMsg.getDownlinkMsg().getDownlinkMsgId()); } } @@ -551,9 +556,13 @@ public final class EdgeGrpcSession implements Closeable { DownlinkMsg downlinkMsg = null; try { switch (edgeEvent.getAction()) { - case UPDATED, ADDED, DELETED, ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE, ALARM_ACK, ALARM_CLEAR, ALARM_DELETE, CREDENTIALS_UPDATED, RELATION_ADD_OR_UPDATE, RELATION_DELETED, CREDENTIALS_REQUEST, RPC_CALL, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, ADDED_COMMENT, UPDATED_COMMENT, DELETED_COMMENT -> { + case UPDATED, ADDED, DELETED, ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE, ALARM_ACK, ALARM_CLEAR, ALARM_DELETE, CREDENTIALS_UPDATED, RELATION_ADD_OR_UPDATE, RELATION_DELETED, RPC_CALL, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, ADDED_COMMENT, UPDATED_COMMENT, DELETED_COMMENT -> { downlinkMsg = convertEntityEventToDownlink(edgeEvent); - log.trace("[{}][{}] entity message processed [{}]", this.tenantId, this.sessionId, downlinkMsg); + if (downlinkMsg != null && downlinkMsg.getWidgetTypeUpdateMsgCount() > 0) { + log.trace("[{}][{}] widgetTypeUpdateMsg message processed, downlinkMsgId = {}", this.tenantId, this.sessionId, downlinkMsg.getDownlinkMsgId()); + } else { + log.trace("[{}][{}] entity message processed [{}]", this.tenantId, this.sessionId, downlinkMsg); + } } case ATTRIBUTES_UPDATED, POST_ATTRIBUTES, ATTRIBUTES_DELETED, TIMESERIES_UPDATED -> downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edge, edgeEvent); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java index 6321991586..125fd48463 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.service.edge.rpc; +import lombok.Getter; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.EntityId; @@ -53,6 +54,7 @@ public class EdgeSyncCursor { private final List fetchers = new LinkedList<>(); + @Getter private int currentIdx = 0; public EdgeSyncCursor(EdgeContextComponent ctx, Edge edge, boolean fullSync) { @@ -62,12 +64,12 @@ public class EdgeSyncCursor { fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService())); fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService())); fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService())); - Customer publicCustomer = ctx.getCustomerService().findOrCreatePublicCustomer(edge.getTenantId()); - fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId())); - if (edge.getCustomerId() != null && !EntityId.NULL_UUID.equals(edge.getCustomerId().getId())) { - fetchers.add(new CustomerEdgeEventFetcher(edge.getCustomerId())); - fetchers.add(new CustomerUsersEdgeEventFetcher(ctx.getUserService(), edge.getCustomerId())); - } + } + Customer publicCustomer = ctx.getCustomerService().findOrCreatePublicCustomer(edge.getTenantId()); + fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId())); + if (edge.getCustomerId() != null && !EntityId.NULL_UUID.equals(edge.getCustomerId().getId())) { + fetchers.add(new CustomerEdgeEventFetcher(edge.getCustomerId())); + fetchers.add(new CustomerUsersEdgeEventFetcher(ctx.getUserService(), edge.getCustomerId())); } fetchers.add(new DashboardsEdgeEventFetcher(ctx.getDashboardService())); fetchers.add(new DefaultProfilesEdgeEventFetcher(ctx.getDeviceProfileService(), ctx.getAssetProfileService())); @@ -102,9 +104,4 @@ public class EdgeSyncCursor { currentIdx++; return edgeEventFetcher; } - - public int getCurrentIdx() { - return currentIdx; - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java index f46c07ab5a..b1c1fc63c5 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java @@ -373,7 +373,7 @@ public abstract class BaseEdgeProcessor { private boolean doSaveIfEdgeIsOffline(EdgeEventType type, EdgeEventActionType action) { return switch (action) { - case TIMESERIES_UPDATED, ALARM_ACK, ALARM_CLEAR, ALARM_ASSIGNED, ALARM_UNASSIGNED, CREDENTIALS_REQUEST, ADDED_COMMENT, UPDATED_COMMENT -> + case TIMESERIES_UPDATED, ALARM_ACK, ALARM_CLEAR, ALARM_ASSIGNED, ALARM_UNASSIGNED, ADDED_COMMENT, UPDATED_COMMENT -> true; default -> switch (type) { case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE, WIDGETS_BUNDLE, WIDGET_TYPE, diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java index 5e73034b1c..f448766994 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java @@ -43,7 +43,6 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponseActorMsg; import org.thingsboard.server.dao.exception.DataValidationException; -import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; @@ -70,7 +69,7 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements case ENTITY_CREATED_RPC_MESSAGE: case ENTITY_UPDATED_RPC_MESSAGE: saveOrUpdateDevice(tenantId, deviceId, deviceUpdateMsg, edge); - return saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, EdgeEventActionType.CREDENTIALS_REQUEST, deviceId, null); + return Futures.immediateFuture(null); case ENTITY_DELETED_RPC_MESSAGE: Device deviceToDelete = deviceService.findDeviceById(tenantId, deviceId); if (deviceToDelete != null) { @@ -232,6 +231,12 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addDeviceUpdateMsg(deviceUpdateMsg); + DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(edgeEvent.getTenantId(), deviceId); + if (deviceCredentials != null) { + DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = ((DeviceMsgConstructor) + deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceCredentialsUpdatedMsg(deviceCredentials); + builder.addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg).build(); + } if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) { DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId()); deviceProfile = checkIfDeviceProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, deviceProfile, edgeVersion); @@ -269,22 +274,7 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) .constructDeviceRpcCallMsg(edgeEvent.getEntityId(), edgeEvent.getBody())) .build(); - case CREDENTIALS_REQUEST: - return convertCredentialsRequestEventToDownlink(edgeEvent); } return downlinkMsg; } - - private DownlinkMsg convertCredentialsRequestEventToDownlink(EdgeEvent edgeEvent) { - DeviceId deviceId = new DeviceId(edgeEvent.getEntityId()); - DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = DeviceCredentialsRequestMsg.newBuilder() - .setDeviceIdMSB(deviceId.getId().getMostSignificantBits()) - .setDeviceIdLSB(deviceId.getId().getLeastSignificantBits()) - .build(); - DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() - .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addDeviceCredentialsRequestMsg(deviceCredentialsRequestMsg); - return builder.build(); - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java index 30f74ae42d..f63aa8b829 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java @@ -61,10 +61,19 @@ public class RuleChainEdgeProcessor extends BaseEdgeProcessor { RuleChainUpdateMsg ruleChainUpdateMsg = ((RuleChainMsgConstructor) ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) .constructRuleChainUpdatedMsg(msgType, ruleChain, isRoot); - downlinkMsg = DownlinkMsg.newBuilder() + + DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addRuleChainUpdateMsg(ruleChainUpdateMsg) - .build(); + .addRuleChainUpdateMsg(ruleChainUpdateMsg); + + RuleChainMetaData ruleChainMetaData = ruleChainService.loadRuleChainMetaData(edgeEvent.getTenantId(), ruleChainId); + RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = ((RuleChainMsgConstructor) + ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) + .constructRuleChainMetadataUpdatedMsg(edgeEvent.getTenantId(), msgType, ruleChainMetaData, edgeVersion); + if (ruleChainMetadataUpdateMsg != null) { + builder.addRuleChainMetadataUpdateMsg(ruleChainMetadataUpdateMsg); + } + downlinkMsg = builder.build(); } } case DELETED, UNASSIGNED_FROM_EDGE -> downlinkMsg = DownlinkMsg.newBuilder() diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java index 607c5e665c..cd072b4a1e 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java @@ -43,10 +43,16 @@ public class UserEdgeProcessor extends BaseEdgeProcessor { User user = userService.findUserById(edgeEvent.getTenantId(), userId); if (user != null) { UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); - downlinkMsg = DownlinkMsg.newBuilder() + DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addUserUpdateMsg(((UserMsgConstructor) userMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructUserUpdatedMsg(msgType, user)) - .build(); + .addUserUpdateMsg(((UserMsgConstructor) userMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructUserUpdatedMsg(msgType, user)); + UserCredentials userCredentialsByUserId = userService.findUserCredentialsByUserId(edgeEvent.getTenantId(), userId); + if (userCredentialsByUserId != null && userCredentialsByUserId.isEnabled()) { + UserCredentialsUpdateMsg userCredentialsUpdateMsg = + ((UserMsgConstructor) userMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructUserCredentialsUpdatedMsg(userCredentialsByUserId); + builder.addUserCredentialsUpdateMsg(userCredentialsUpdateMsg); + } + downlinkMsg = builder.build(); } } case DELETED -> downlinkMsg = DownlinkMsg.newBuilder() diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java index 3f61734801..6e4117869b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java @@ -82,8 +82,6 @@ import java.util.UUID; @Slf4j public class DefaultEdgeRequestsService implements EdgeRequestsService { - private static final int DEFAULT_PAGE_SIZE = 1000; - @Autowired private EdgeEventService edgeEventService; @@ -142,8 +140,10 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { private ListenableFuture processEntityAttributesAndAddToEdgeQueue(TenantId tenantId, EntityId entityId, Edge edge, EdgeEventType entityType, String scope, List ssAttributes, AttributesRequestMsg attributesRequestMsg) { + Map entityData = null; + ObjectNode attributes = null; + ListenableFuture future; try { - ListenableFuture future; if (ssAttributes == null || ssAttributes.isEmpty()) { log.trace("[{}][{}] No attributes found for entity {} [{}]", tenantId, edge.getName(), @@ -151,8 +151,8 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { entityId.getId()); future = Futures.immediateFuture(null); } else { - Map entityData = new HashMap<>(); - ObjectNode attributes = JacksonUtil.newObjectNode(); + entityData = new HashMap<>(); + attributes = JacksonUtil.newObjectNode(); for (AttributeKvEntry attr : ssAttributes) { if (DefaultDeviceStateService.PERSISTENT_ATTRIBUTES.contains(attr.getKey()) && !DefaultDeviceStateService.INACTIVITY_TIMEOUT.equals(attr.getKey())) { @@ -170,7 +170,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { attributes.put(attr.getKey(), attr.getValueAsString()); } } - if (attributes.size() > 0) { + if (!attributes.isEmpty()) { entityData.put("kv", attributes); entityData.put("scope", scope); JsonNode body = JacksonUtil.valueToTree(entityData); @@ -182,12 +182,13 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService { } return Futures.transformAsync(future, v -> processLatestTimeseriesAndAddToEdgeQueue(tenantId, entityId, edge, entityType), dbCallbackExecutorService); } catch (Exception e) { - String errMsg = String.format("[%s][%s] Failed to save attribute updates to the edge [%s]", tenantId, edge.getId(), attributesRequestMsg); + String errMsg = String.format("[%s][%s] Failed to save attribute updates to the edge [%s], scope = %s, entityData = %s, attributes = %s", + tenantId, edge.getId(), attributesRequestMsg, scope, entityData, attributes); log.error(errMsg, e); return Futures.immediateFailedFuture(new RuntimeException(errMsg, e)); } } - + private ListenableFuture processLatestTimeseriesAndAddToEdgeQueue(TenantId tenantId, EntityId entityId, Edge edge, EdgeEventType entityType) { ListenableFuture> getAllLatestFuture = timeseriesService.findAllLatest(tenantId, entityId); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java index b20abb001e..632ea44940 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java @@ -44,6 +44,7 @@ import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.TsValue; +import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.entity.EntityService; @@ -355,6 +356,9 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc private void handleWsCmdRuntimeException(String sessionId, RuntimeException e, EntityDataCmd cmd) { log.debug("[{}] Failed to process ws cmd: {}", sessionId, cmd, e); + if (e instanceof TbRateLimitsException) { + return; + } wsService.close(sessionId, CloseStatus.SERVICE_RESTARTED); } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java index 9367439d4f..8fdb18cd1d 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java @@ -18,12 +18,15 @@ package org.thingsboard.server.service.subscription; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; +import org.thingsboard.common.util.DeduplicationUtil; import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; +import org.thingsboard.server.cache.limits.RateLimitService; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; @@ -36,10 +39,12 @@ import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.limit.LimitedApi; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.gen.transport.TransportProtos; @@ -48,6 +53,7 @@ import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.event.ClusterTopologyChangeEvent; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.ws.WebSocketService; +import org.thingsboard.server.service.ws.WebSocketSessionRef; import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate; import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate; import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate; @@ -88,13 +94,20 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer private final TbClusterService clusterService; private final SubscriptionManagerService subscriptionManagerService; private final WebSocketService webSocketService; + private final RateLimitService rateLimitService; private ExecutorService tsCallBackExecutor; private ScheduledExecutorService staleSessionCleanupExecutor; + @Value("${server.ws.rate_limits.subscriptions_per_tenant:2000:60}") + private String subscriptionsPerTenantRateLimit; + @Value("${server.ws.rate_limits.subscriptions_per_user:500:60}") + private String subscriptionsPerUserRateLimit; + public DefaultTbLocalSubscriptionService(AttributesService attrService, TimeseriesService tsService, TbServiceInfoProvider serviceInfoProvider, PartitionService partitionService, TbClusterService clusterService, - @Lazy SubscriptionManagerService subscriptionManagerService, @Lazy WebSocketService webSocketService) { + @Lazy SubscriptionManagerService subscriptionManagerService, @Lazy WebSocketService webSocketService, + RateLimitService rateLimitService) { this.attrService = attrService; this.tsService = tsService; this.serviceInfoProvider = serviceInfoProvider; @@ -102,6 +115,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer this.clusterService = clusterService; this.subscriptionManagerService = subscriptionManagerService; this.webSocketService = webSocketService; + this.rateLimitService = rateLimitService; } private String serviceId; @@ -185,9 +199,18 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer } @Override - public void addSubscription(TbSubscription subscription) { + public void addSubscription(TbSubscription subscription, WebSocketSessionRef sessionRef) { TenantId tenantId = subscription.getTenantId(); EntityId entityId = subscription.getEntityId(); + if (!rateLimitService.checkRateLimit(LimitedApi.WS_SUBSCRIPTIONS, (Object) tenantId, subscriptionsPerTenantRateLimit)) { + handleRateLimitError(subscription, sessionRef, "Exceeded rate limit for WS subscriptions per tenant"); + return; + } + if (sessionRef.getSecurityCtx() != null && !rateLimitService.checkRateLimit(LimitedApi.WS_SUBSCRIPTIONS, sessionRef.getSecurityCtx().getId(), subscriptionsPerUserRateLimit)) { + handleRateLimitError(subscription, sessionRef, "Exceeded rate limit for WS subscriptions per user"); + return; + } + log.debug("[{}][{}] Register subscription: {}", tenantId, entityId, subscription); SubscriptionModificationResult result; subsLock.lock(); @@ -584,4 +607,13 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer subscriptionsBySessionId.keySet().forEach(webSocketService::cleanupIfStale); } + private void handleRateLimitError(TbSubscription subscription, WebSocketSessionRef sessionRef, String message) { + String deduplicationKey = sessionRef.getSessionId() + message; + if (!DeduplicationUtil.alreadyProcessed(deduplicationKey, TimeUnit.SECONDS.toMillis(15))) { + log.info("{} {}", sessionRef, message); + webSocketService.sendError(sessionRef, subscription.getSubscriptionId(), SubscriptionErrorCode.BAD_REQUEST, message); + } + throw new TbRateLimitsException(message); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java index f65eec8453..77ef960d72 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java @@ -132,7 +132,7 @@ public abstract class TbAbstractDataSubCtx> keysByType = getEntityKeyByTypeMap(keys); for (EntityData entityData : data.getData()) { List entitySubscriptions = addSubscriptions(entityData, keysByType, latestValues, startTs, endTs); - entitySubscriptions.forEach(localSubscriptionService::addSubscription); + entitySubscriptions.forEach(subscription -> localSubscriptionService.addSubscription(subscription, sessionRef)); } } @@ -254,4 +254,5 @@ public abstract class TbAbstractDataSubCtx { .scope(TbAttributeSubscriptionScope.SERVER_SCOPE) .build(); subToDynamicValueKeySet.add(subIdx); - localSubscriptionService.addSubscription(sub); + localSubscriptionService.addSubscription(sub, sessionRef); } } catch (InterruptedException | ExecutionException e) { log.info("[{}][{}][{}] Failed to resolve dynamic values: {}", tenantId, customerId, userId, dynamicValues.keySet()); diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java index 0d1f2f94b2..828da388fb 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java @@ -177,7 +177,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { .updateProcessor((sub, update) -> sendWsMsg(sub.getSessionId(), update)) .ts(startTs) .build(); - localSubscriptionService.addSubscription(subscription); + localSubscriptionService.addSubscription(subscription, sessionRef); } @Override @@ -342,7 +342,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { newSubsList.forEach(entity -> createAlarmSubscriptionForEntity(query.getPageLink(), startTs, entity)); } subIdsToCancel.forEach(subId -> localSubscriptionService.cancelSubscription(getSessionId(), subId)); - subsToAdd.forEach(localSubscriptionService::addSubscription); + subsToAdd.forEach(subscription -> localSubscriptionService.addSubscription(subscription, sessionRef)); } private void resetInvocationCounter() { @@ -361,4 +361,5 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder); return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters()); } + } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java index 98ec81b798..48458ca6fd 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java @@ -226,7 +226,7 @@ public class TbEntityDataSubCtx extends TbAbstractDataSubCtx { } } subIdsToCancel.forEach(subId -> localSubscriptionService.cancelSubscription(getSessionId(), subId)); - subsToAdd.forEach(localSubscriptionService::addSubscription); + subsToAdd.forEach(subscription -> localSubscriptionService.addSubscription(subscription, sessionRef)); sendWsMsg(new EntityDataUpdate(cmdId, data, null, maxEntitiesPerDataSubscription)); } @@ -239,4 +239,5 @@ public class TbEntityDataSubCtx extends TbAbstractDataSubCtx { protected EntityDataQuery buildEntityDataQuery() { return query; } + } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java index ddb2a1b590..59e7ad532d 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.discovery.event.ClusterTopologyChangeEvent; +import org.thingsboard.server.service.ws.WebSocketSessionRef; import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate; import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate; @@ -29,7 +30,7 @@ import java.util.List; public interface TbLocalSubscriptionService { - void addSubscription(TbSubscription subscription); + void addSubscription(TbSubscription subscription, WebSocketSessionRef sessionRef); void onSubEventCallback(TransportProtos.TbEntitySubEventCallbackProto subEventCallback, TbCallback callback); diff --git a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java index b72a1a5841..33b011cc0a 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java @@ -49,6 +49,7 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.msg.tools.TbRateLimitsException; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -223,9 +224,10 @@ public class DefaultWebSocketService implements WebSocketService { try { Optional.ofNullable(cmdsHandlers.get(cmd.getType())) .ifPresent(cmdHandler -> cmdHandler.handle(sessionRef, cmd)); + } catch (TbRateLimitsException e) { + log.debug("{} Failed to handle WS cmd: {}", sessionRef, cmd, e); } catch (Exception e) { - log.error("[sessionId: {}, tenantId: {}, userId: {}] Failed to handle WS cmd: {}", sessionId, - sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), cmd, e); + log.error("{} Failed to handle WS cmd: {}", sessionRef, cmd, e); } } } @@ -467,7 +469,7 @@ public class DefaultWebSocketService implements WebSocketService { subLock.lock(); try { - oldSubService.addSubscription(sub); + oldSubService.addSubscription(sub, sessionRef); sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData)); } finally { subLock.unlock(); @@ -580,7 +582,7 @@ public class DefaultWebSocketService implements WebSocketService { subLock.lock(); try { - oldSubService.addSubscription(sub); + oldSubService.addSubscription(sub, sessionRef); sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData)); } finally { subLock.unlock(); @@ -677,7 +679,7 @@ public class DefaultWebSocketService implements WebSocketService { subLock.lock(); try { - oldSubService.addSubscription(sub); + oldSubService.addSubscription(sub, sessionRef); sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data)); } finally { subLock.unlock(); @@ -732,7 +734,7 @@ public class DefaultWebSocketService implements WebSocketService { subLock.lock(); try { - oldSubService.addSubscription(sub); + oldSubService.addSubscription(sub, sessionRef); sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data)); } finally { subLock.unlock(); diff --git a/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java b/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java index b8c83fe286..285c58a0f5 100644 --- a/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java +++ b/application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java @@ -81,7 +81,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH .limit(cmd.getLimit()) .notificationTypes(cmd.getTypes()) .build(); - localSubscriptionService.addSubscription(subscription); + localSubscriptionService.addSubscription(subscription, sessionRef); fetchUnreadNotifications(subscription); sendUpdate(sessionRef.getSessionId(), subscription.createFullUpdate()); @@ -99,7 +99,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH .entityId(securityCtx.getId()) .updateProcessor(this::handleNotificationsCountSubscriptionUpdate) .build(); - localSubscriptionService.addSubscription(subscription); + localSubscriptionService.addSubscription(subscription, sessionRef); fetchUnreadNotificationsCount(subscription); sendUpdate(sessionRef.getSessionId(), subscription.createUpdate()); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 3c9afc4b33..f47663805b 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -78,6 +78,11 @@ server: max_queue_messages_per_session: "${TB_SERVER_WS_DEFAULT_QUEUE_MESSAGES_PER_SESSION:1000}" # Maximum time between WS session opening and sending auth command auth_timeout_ms: "${TB_SERVER_WS_AUTH_TIMEOUT_MS:10000}" + rate_limits: + # Per-tenant rate limit for WS subscriptions + subscriptions_per_tenant: "${TB_SERVER_WS_SUBSCRIPTIONS_PER_TENANT_RATE_LIMIT:2000:60}" + # Per-user rate limit for WS subscriptions + subscriptions_per_user: "${TB_SERVER_WS_SUBSCRIPTIONS_PER_USER_RATE_LIMIT:500:60}" rest: server_side_rpc: # Minimum value of the server-side RPC timeout. May override value provided in the REST API call. diff --git a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java index 49dfa71223..de530b5268 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java @@ -55,14 +55,18 @@ import org.thingsboard.server.common.data.asset.AssetProfile; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EdgeId; +import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.dao.edge.EdgeDao; import org.thingsboard.server.dao.exception.DataValidationException; @@ -73,11 +77,13 @@ import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg; import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; +import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg; import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg; import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg; import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; @@ -93,6 +99,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.UUID; import java.util.concurrent.TimeUnit; import static org.hamcrest.Matchers.containsString; @@ -887,23 +894,24 @@ public class EdgeControllerTest extends AbstractControllerTest { edgeImitator.ignoreType(UserCredentialsUpdateMsg.class); edgeImitator.ignoreType(OAuth2UpdateMsg.class); - edgeImitator.expectMessageAmount(24); + edgeImitator.expectMessageAmount(27); edgeImitator.connect(); waitForMessages(edgeImitator); - verifyFetchersMsgs(edgeImitator); + verifyFetchersMsgs(edgeImitator, savedDevice); // verify queue msgs Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); Assert.assertTrue(popDeviceMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Device 1")); + Assert.assertTrue(popDeviceCredentialsMsg(edgeImitator.getDownlinkMsgs(), savedDevice.getId())); Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "test")); Assert.assertTrue(popAssetMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Asset 1")); Assert.assertTrue(edgeImitator.getDownlinkMsgs().isEmpty()); - edgeImitator.expectMessageAmount(20); + edgeImitator.expectMessageAmount(22); doPost("/api/edge/sync/" + edge.getId()); waitForMessages(edgeImitator); - verifyFetchersMsgs(edgeImitator); + verifyFetchersMsgs(edgeImitator, savedDevice); Assert.assertTrue(edgeImitator.getDownlinkMsgs().isEmpty()); edgeImitator.allowIgnoredTypes(); @@ -920,6 +928,23 @@ public class EdgeControllerTest extends AbstractControllerTest { .andExpect(status().isOk()); } + private RuleChainId getEdgeRootRuleChainId(EdgeImitator edgeImitator) { + try { + EdgeId edgeId = new EdgeId(new UUID(edgeImitator.getConfiguration().getEdgeIdMSB(), edgeImitator.getConfiguration().getEdgeIdLSB())); + List edgeRuleChains = doGetTypedWithPageLink("/api/edge/" + edgeId.getId() + "/ruleChains?", + new TypeReference>() { + }, new PageLink(100)).getData(); + for (RuleChain edgeRuleChain : edgeRuleChains) { + if (edgeRuleChain.isRoot()) { + return edgeRuleChain.getId(); + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } + throw new RuntimeException("Root rule chain not found"); + } + private void simulateEdgeActivation(Edge edge) throws Exception { ObjectNode attributes = JacksonUtil.newObjectNode(); attributes.put("active", true); @@ -949,9 +974,10 @@ public class EdgeControllerTest extends AbstractControllerTest { } } - private void verifyFetchersMsgs(EdgeImitator edgeImitator) { + private void verifyFetchersMsgs(EdgeImitator edgeImitator, Device savedDevice) { Assert.assertTrue(popQueueMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Main")); Assert.assertTrue(popRuleChainMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Edge Root Rule Chain")); + Assert.assertTrue(popRuleChainMetadataMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, getEdgeRootRuleChainId(edgeImitator))); Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "general")); Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "mail")); Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "connectivity")); @@ -965,6 +991,7 @@ public class EdgeControllerTest extends AbstractControllerTest { Assert.assertTrue(popCustomerMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Public")); Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); Assert.assertTrue(popDeviceMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Device 1")); + Assert.assertTrue(popDeviceCredentialsMsg(edgeImitator.getDownlinkMsgs(), savedDevice.getId())); Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "test")); Assert.assertTrue(popAssetMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Asset 1")); Assert.assertTrue(popTenantMsg(edgeImitator.getDownlinkMsgs(), tenantId)); @@ -1002,6 +1029,21 @@ public class EdgeControllerTest extends AbstractControllerTest { return false; } + private boolean popRuleChainMetadataMsg(List messages, UpdateMsgType msgType, RuleChainId ruleChainId) { + for (AbstractMessage message : messages) { + if (message instanceof RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg) { + RuleChainMetaData ruleChainMetaData = JacksonUtil.fromString(ruleChainMetadataUpdateMsg.getEntity(), RuleChainMetaData.class, true); + Assert.assertNotNull(ruleChainMetaData); + if (msgType.equals(ruleChainMetadataUpdateMsg.getMsgType()) + && ruleChainId.equals(ruleChainMetaData.getRuleChainId())) { + messages.remove(message); + return true; + } + } + } + return false; + } + private boolean popAdminSettingsMsg(List messages, String key) { for (AbstractMessage message : messages) { if (message instanceof AdminSettingsUpdateMsg adminSettingsUpdateMsg) { @@ -1046,6 +1088,20 @@ public class EdgeControllerTest extends AbstractControllerTest { return false; } + private boolean popDeviceCredentialsMsg(List messages, DeviceId deviceId) { + for (AbstractMessage message : messages) { + if (message instanceof DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg) { + DeviceCredentials deviceCredentials = JacksonUtil.fromString(deviceCredentialsUpdateMsg.getEntity(), DeviceCredentials.class, true); + Assert.assertNotNull(deviceCredentials); + if (deviceId.equals(deviceCredentials.getDeviceId())) { + messages.remove(message); + return true; + } + } + } + return false; + } + private boolean popAssetProfileMsg(List messages, UpdateMsgType msgType, String name) { for (AbstractMessage message : messages) { if (message instanceof AssetProfileUpdateMsg assetProfileUpdateMsg) { diff --git a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java index e7f664477c..f7a066f7e0 100644 --- a/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java @@ -83,6 +83,7 @@ import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; +import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; @@ -96,6 +97,7 @@ import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UplinkMsg; +import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import java.util.ArrayList; @@ -143,7 +145,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret()); edgeImitator.ignoreType(OAuth2UpdateMsg.class); - edgeImitator.expectMessageAmount(21); + edgeImitator.expectMessageAmount(24); edgeImitator.connect(); requestEdgeRuleChainMetadata(); @@ -250,7 +252,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { UUID ruleChainUUID = validateRuleChains(); // 1 from request message - validateMsgsCnt(RuleChainMetadataUpdateMsg.class, 1); + validateMsgsCnt(RuleChainMetadataUpdateMsg.class, 2); validateRuleChainMetadataUpdates(ruleChainUUID); // 4 messages ('general', 'mail', 'connectivity', 'jwt') @@ -275,6 +277,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { validateMsgsCnt(DeviceUpdateMsg.class, 1); validateDevices(); + validateMsgsCnt(DeviceCredentialsUpdateMsg.class, 1); + // 1 from asset fetcher validateMsgsCnt(AssetUpdateMsg.class, 1); validateAssets(); @@ -287,6 +291,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest { validateMsgsCnt(UserUpdateMsg.class, 1); validateUsers(); + validateMsgsCnt(UserCredentialsUpdateMsg.class, 1); + // 1 from tenant fetcher validateMsgsCnt(TenantUpdateMsg.class, 1); validateTenant(); diff --git a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java index 9e09a16fd9..c5f34e4f47 100644 --- a/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java @@ -150,25 +150,25 @@ public class DeviceEdgeTest extends AbstractEdgeTest { + "/edge/" + edge.getUuidId(), Edge.class); Assert.assertTrue(edgeImitator.waitForMessages()); - edgeImitator.expectMessageAmount(1); + edgeImitator.expectMessageAmount(2); doPost("/api/customer/" + savedCustomer.getUuidId() + "/device/" + savedDevice.getUuidId(), Device.class); Assert.assertTrue(edgeImitator.waitForMessages()); - latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); - deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; + deviceUpdateMsgOpt = edgeImitator.findMessageByType(DeviceUpdateMsg.class); + Assert.assertTrue(deviceUpdateMsgOpt.isPresent()); + deviceUpdateMsg = deviceUpdateMsgOpt.get(); deviceFromMsg = JacksonUtil.fromString(deviceUpdateMsg.getEntity(), Device.class, true); Assert.assertNotNull(deviceFromMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); Assert.assertEquals(savedCustomer.getId(), deviceFromMsg.getCustomerId()); // unassign device #2 from customer - edgeImitator.expectMessageAmount(1); + edgeImitator.expectMessageAmount(2); doDelete("/api/customer/device/" + savedDevice.getUuidId(), Device.class); Assert.assertTrue(edgeImitator.waitForMessages()); - latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); - deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; + deviceUpdateMsgOpt = edgeImitator.findMessageByType(DeviceUpdateMsg.class); + Assert.assertTrue(deviceUpdateMsgOpt.isPresent()); + deviceUpdateMsg = deviceUpdateMsgOpt.get(); deviceFromMsg = JacksonUtil.fromString(deviceUpdateMsg.getEntity(), Device.class, true); Assert.assertNotNull(deviceFromMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); @@ -243,7 +243,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { Assert.assertTrue(edgeImitator.waitForMessages()); // update device - edgeImitator.expectMessageAmount(1); + edgeImitator.expectMessageAmount(2); savedDevice.setFirmwareId(firmwareOtaPackageInfo.getId()); savedDevice.setSoftwareId(softwareOtaPackageInfo.getId()); @@ -256,9 +256,9 @@ public class DeviceEdgeTest extends AbstractEdgeTest { savedDevice = doPost("/api/device", savedDevice, Device.class); Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); - DeviceUpdateMsg deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; + Optional deviceUpdateMsgOpt = edgeImitator.findMessageByType(DeviceUpdateMsg.class); + Assert.assertTrue(deviceUpdateMsgOpt.isPresent()); + DeviceUpdateMsg deviceUpdateMsg = deviceUpdateMsgOpt.get(); Device deviceMsg = JacksonUtil.fromString(deviceUpdateMsg.getEntity(), Device.class, true); Assert.assertNotNull(deviceMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); @@ -504,7 +504,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest { uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build()); edgeImitator.expectResponsesAmount(1); - edgeImitator.expectMessageAmount(2); + edgeImitator.expectMessageAmount(1); testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); @@ -526,18 +526,6 @@ public class DeviceEdgeTest extends AbstractEdgeTest { Device device = doGet("/api/device/" + newDeviceId, Device.class); Assert.assertNotNull(device); Assert.assertNotEquals(deviceOnCloudName, device.getName()); - - Optional deviceCredentialsUpdateMsgOpt = edgeImitator.findMessageByType(DeviceCredentialsRequestMsg.class); - Assert.assertTrue(deviceCredentialsUpdateMsgOpt.isPresent()); - DeviceCredentialsRequestMsg latestDeviceCredentialsRequestMsg = deviceCredentialsUpdateMsgOpt.get(); - Assert.assertEquals(deviceMsg.getUuidId().getMostSignificantBits(), latestDeviceCredentialsRequestMsg.getDeviceIdMSB()); - Assert.assertEquals(device.getUuidId().getLeastSignificantBits(), latestDeviceCredentialsRequestMsg.getDeviceIdLSB()); - - newDeviceId = new UUID(latestDeviceCredentialsRequestMsg.getDeviceIdMSB(), latestDeviceCredentialsRequestMsg.getDeviceIdLSB()); - - device = doGet("/api/device/" + newDeviceId, Device.class); - Assert.assertNotNull(device); - Assert.assertNotEquals(deviceOnCloudName, device.getName()); } @Test @@ -553,22 +541,10 @@ public class DeviceEdgeTest extends AbstractEdgeTest { uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build()); edgeImitator.expectResponsesAmount(1); - edgeImitator.expectMessageAmount(1); - edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); - Assert.assertTrue(edgeImitator.waitForResponses()); - Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof DeviceCredentialsRequestMsg); - DeviceCredentialsRequestMsg latestDeviceCredentialsRequestMsg = (DeviceCredentialsRequestMsg) latestMessage; - Assert.assertEquals(deviceMsg.getUuidId().getMostSignificantBits(), latestDeviceCredentialsRequestMsg.getDeviceIdMSB()); - Assert.assertEquals(deviceMsg.getUuidId().getLeastSignificantBits(), latestDeviceCredentialsRequestMsg.getDeviceIdLSB()); - - UUID newDeviceId = new UUID(latestDeviceCredentialsRequestMsg.getDeviceIdMSB(), latestDeviceCredentialsRequestMsg.getDeviceIdLSB()); - - Device device = doGet("/api/device/" + newDeviceId, Device.class); + Device device = doGet("/api/device/" + deviceMsg.getId().getId(), Device.class); Assert.assertNotNull(device); Assert.assertEquals("Edge Device 2", device.getName()); } diff --git a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java index 62d2c5eade..c694df84f8 100644 --- a/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java @@ -193,7 +193,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { ruleChain.setType(RuleChainType.EDGE); RuleChain savedRuleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); - edgeImitator.expectMessageAmount(2); + edgeImitator.expectMessageAmount(4); doPost("/api/edge/" + edge.getUuidId() + "/ruleChain/" + savedRuleChain.getUuidId(), RuleChain.class); RuleChainMetaData metaData = createRuleChainMetadata(savedRuleChain); @@ -201,7 +201,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest { // set new rule chain as root RuleChainId currentRootRuleChainId = edge.getRootRuleChainId(); - edgeImitator.expectMessageAmount(1); + edgeImitator.expectMessageAmount(2); doPost("/api/edge/" + edge.getUuidId() + "/" + savedRuleChain.getUuidId() + "/root", Edge.class); Assert.assertTrue(edgeImitator.waitForMessages()); diff --git a/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java index 00b8cc4b74..b77e0b5ccc 100644 --- a/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java @@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.edge.EdgeEventType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.gen.edge.v1.AttributeDeleteMsg; +import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.EntityDataProto; import org.thingsboard.server.gen.edge.v1.UplinkMsg; @@ -183,7 +184,7 @@ public class TelemetryEdgeTest extends AbstractEdgeTest { edgeImitator.setRandomFailuresOnTimeseriesDownlink(true); // imitator will generate failure in 100% of timeseries cases edgeImitator.setFailureProbability(100); - edgeImitator.expectMessageAmount(numberOfMsgsToSend); + edgeImitator.expectMessageAmount(numberOfMsgsToSend * 2); for (int idx = 1; idx <= numberOfMsgsToSend; idx++) { String timeseriesData = "{\"data\":{\"idx\":" + idx + "},\"ts\":" + System.currentTimeMillis() + "}"; JsonNode timeseriesEntityData = JacksonUtil.toJsonNode(timeseriesData); @@ -204,6 +205,9 @@ public class TelemetryEdgeTest extends AbstractEdgeTest { List deviceUpdateMsgs = edgeImitator.findAllMessagesByType(DeviceUpdateMsg.class); Assert.assertEquals(numberOfMsgsToSend, deviceUpdateMsgs.size()); + List deviceCredentialsUpdateMsgs = edgeImitator.findAllMessagesByType(DeviceCredentialsUpdateMsg.class); + Assert.assertEquals(numberOfMsgsToSend, deviceCredentialsUpdateMsgs.size()); + edgeImitator.setRandomFailuresOnTimeseriesDownlink(false); } diff --git a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java index 6314cf53b9..503d87cf6f 100644 --- a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java @@ -71,13 +71,13 @@ public class UserEdgeTest extends AbstractEdgeTest { Assert.assertTrue(userCredentialsUpdateMsgOpt.isPresent()); // update user - edgeImitator.expectMessageAmount(1); + edgeImitator.expectMessageAmount(2); savedTenantAdmin.setLastName("Borisov"); savedTenantAdmin = doPost("/api/user", savedTenantAdmin, User.class); Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof UserUpdateMsg); - userUpdateMsg = (UserUpdateMsg) latestMessage; + userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); + Assert.assertTrue(userUpdateMsgOpt.isPresent()); + userUpdateMsg = userUpdateMsgOpt.get(); userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); Assert.assertNotNull(userMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); @@ -92,7 +92,7 @@ public class UserEdgeTest extends AbstractEdgeTest { changePasswordRequest.setNewPassword("newTenant"); doPost("/api/auth/changePassword", changePasswordRequest); Assert.assertTrue(edgeImitator.waitForMessages()); - latestMessage = edgeImitator.getLatestMessage(); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true); @@ -155,13 +155,13 @@ public class UserEdgeTest extends AbstractEdgeTest { Assert.assertEquals(savedCustomerUser.getLastName(), userMsg.getLastName()); // update user - edgeImitator.expectMessageAmount(1); + edgeImitator.expectMessageAmount(2); savedCustomerUser.setLastName("Addams"); savedCustomerUser = doPost("/api/user", savedCustomerUser, User.class); Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof UserUpdateMsg); - userUpdateMsg = (UserUpdateMsg) latestMessage; + userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); + Assert.assertTrue(userUpdateMsgOpt.isPresent()); + userUpdateMsg = userUpdateMsgOpt.get(); userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); Assert.assertNotNull(userMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); @@ -176,7 +176,7 @@ public class UserEdgeTest extends AbstractEdgeTest { changePasswordRequest.setNewPassword("newCustomer"); doPost("/api/auth/changePassword", changePasswordRequest); Assert.assertTrue(edgeImitator.waitForMessages()); - latestMessage = edgeImitator.getLatestMessage(); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true); diff --git a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/AbstractDeviceProcessorTest.java b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/AbstractDeviceProcessorTest.java index 0ec40fb908..31524e467a 100644 --- a/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/AbstractDeviceProcessorTest.java +++ b/application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/AbstractDeviceProcessorTest.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessorTest; @@ -61,6 +62,9 @@ public abstract class AbstractDeviceProcessorTest extends BaseEdgeProcessorTest deviceProfile.setProfileData(deviceProfileData); deviceProfile.setTransportType(DeviceTransportType.DEFAULT); + DeviceCredentials deviceCredentials = new DeviceCredentials(); + deviceCredentials.setDeviceId(deviceId); + Device device = new Device(); device.setDeviceProfileId(deviceProfileId); device.setId(deviceId); @@ -71,9 +75,9 @@ public abstract class AbstractDeviceProcessorTest extends BaseEdgeProcessorTest edgeEvent.setTenantId(tenantId); edgeEvent.setAction(EdgeEventActionType.ADDED); - willReturn(device).given(deviceService).findDeviceById(tenantId, deviceId); willReturn(deviceProfile).given(deviceProfileService).findDeviceProfileById(tenantId, deviceProfileId); + willReturn(deviceCredentials).given(deviceCredentialsService).findDeviceCredentialsByDeviceId(tenantId, deviceId); } protected void updateDeviceProfileDefaultFields(long expectedDashboardIdMSB, long expectedDashboardIdLSB, diff --git a/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java b/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java index 74c2dfda37..5fde907bf1 100644 --- a/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java +++ b/application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java @@ -17,20 +17,45 @@ package org.thingsboard.server.system; import lombok.extern.slf4j.Slf4j; import org.junit.Test; -import org.junit.jupiter.api.Assertions; +import org.springframework.http.MediaType; +import org.springframework.mock.http.client.MockClientHttpRequest; +import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.util.ClassUtils; import org.springframework.web.client.RestTemplate; +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + @Slf4j public class RestTemplateConvertersTest { @Test - public void testJacksonXmlConverter() { + public void testMappingJackson2HttpMessageConverterIsUsedInsteadOfMappingJackson2XmlHttpMessageConverter() { ClassLoader classLoader = RestTemplate.class.getClassLoader(); boolean jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader); - Assertions.assertFalse(jackson2XmlPresent, "XmlMapper must not be present in classpath, please, exclude \"jackson-dataformat-xml\" dependency!"); - //If this xml mapper will be present in classpath then we will get "Unsupported Media Type" in RestTemplate + assertThat(jackson2XmlPresent).isTrue(); + + RestTemplate restTemplate = new RestTemplate(); + MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate); + mockServer.expect(requestTo("/test")) + .andExpect(request -> { + MockClientHttpRequest mockRequest = (MockClientHttpRequest) request; + byte[] body = mockRequest.getBodyAsBytes(); + String requestBody = new String(body, StandardCharsets.UTF_8); + assertThat(requestBody).contains("{\"name\":\"test\",\"value\":1}"); + }) + .andRespond(withSuccess("{\"name\":\"test\",\"value\":1}", MediaType.APPLICATION_JSON)); + + TestObject requestObject = new TestObject("test", 1); + TestObject actualObject = restTemplate.postForObject("/test", requestObject, TestObject.class); + assertThat(actualObject).isEqualTo(requestObject); + mockServer.verify(); } + record TestObject(String name, int value) {} + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java index 7bee0c6094..176de62f69 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java @@ -43,7 +43,7 @@ public enum EdgeEventActionType { DELETED_COMMENT(ActionType.DELETED_COMMENT), ASSIGNED_TO_EDGE(ActionType.ASSIGNED_TO_EDGE), UNASSIGNED_FROM_EDGE(ActionType.UNASSIGNED_FROM_EDGE), - CREDENTIALS_REQUEST(null), + CREDENTIALS_REQUEST(null), // deprecated ENTITY_MERGE_REQUEST(null); // deprecated private final ActionType actionType; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java index 7aa472bea1..6532a4fe0d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java @@ -42,7 +42,8 @@ public enum LimitedApi { TRANSPORT_MESSAGES_PER_DEVICE("transport messages per device", false), TRANSPORT_MESSAGES_PER_GATEWAY("transport messages per gateway", false), TRANSPORT_MESSAGES_PER_GATEWAY_DEVICE("transport messages per gateway device", false), - EMAILS("emails sending", true); + EMAILS("emails sending", true), + WS_SUBSCRIPTIONS("WS subscriptions", false); private Function configExtractor; @Getter diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/edge/FromEdgeSyncResponse.java b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/FromEdgeSyncResponse.java index 733129b4de..baf483310c 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/edge/FromEdgeSyncResponse.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/edge/FromEdgeSyncResponse.java @@ -25,12 +25,13 @@ import java.util.UUID; @Data public class FromEdgeSyncResponse implements EdgeSessionMsg { - private static final long serialVersionUID = -6360890886315347486L; + private static final long serialVersionUID = -6360890556315667486L; private final UUID id; private final TenantId tenantId; private final EdgeId edgeId; private final boolean success; + private final String error; @Override public MsgType getMsgType() { diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimitsException.java b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimitsException.java index b39477fa38..32d55048ae 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimitsException.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimitsException.java @@ -30,4 +30,10 @@ public class TbRateLimitsException extends AbstractRateLimitException { super(entityType.name() + " rate limits reached!"); this.entityType = entityType; } + + public TbRateLimitsException(String message) { + super(message); + this.entityType = null; + } + } 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 a7c5d5185e..e4997cfafd 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 @@ -162,6 +162,7 @@ public class ProtoUtils { .setEdgeIdMSB(response.getEdgeId().getId().getMostSignificantBits()) .setEdgeIdLSB(response.getEdgeId().getId().getLeastSignificantBits()) .setSuccess(response.isSuccess()) + .setError(response.getError()) .build(); } @@ -170,7 +171,8 @@ public class ProtoUtils { new UUID(proto.getResponseIdMSB(), proto.getResponseIdLSB()), TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), new EdgeId(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())), - proto.getSuccess() + proto.getSuccess(), + proto.getError() ); } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 2b715d71af..13ebe13832 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -1157,6 +1157,7 @@ message FromEdgeSyncResponseMsgProto { int64 edgeIdMSB = 5; int64 edgeIdLSB = 6; bool success = 7; + string error = 8; } message DeviceEdgeUpdateMsgProto { diff --git a/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java b/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java index 904e85f702..a513d3e311 100644 --- a/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java +++ b/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java @@ -124,7 +124,7 @@ class ProtoUtilsTest { @Test void protoFromEdgeSyncResponseSerialization() { - FromEdgeSyncResponse msg = new FromEdgeSyncResponse(id, tenantId, edgeId, true); + FromEdgeSyncResponse msg = new FromEdgeSyncResponse(id, tenantId, edgeId, true, "Error Msg"); assertThat(ProtoUtils.fromProto(ProtoUtils.toProto(msg))).as("deserialized").isEqualTo(msg); } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java index 2bcb5461e1..1ba7c9aff1 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java @@ -39,8 +39,8 @@ import org.thingsboard.server.common.transport.auth.SessionInfoCreator; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.coap.ConfigProtos; import org.thingsboard.server.gen.transport.coap.DeviceInfoProtos; -import org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos; import org.thingsboard.server.gen.transport.coap.MeasurementsProtos; +import org.thingsboard.server.gen.transport.coap.MeasurementsProtos.ProtoChannel; import org.thingsboard.server.transport.coap.AbstractCoapTransportResource; import org.thingsboard.server.transport.coap.CoapTransportContext; import org.thingsboard.server.transport.coap.callback.CoapDeviceAuthCallback; @@ -49,19 +49,21 @@ import org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils; import java.nio.ByteBuffer; import java.text.SimpleDateFormat; -import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import static com.google.gson.JsonParser.parseString; import static org.thingsboard.server.transport.coap.CoapTransportService.CONFIGURATION; import static org.thingsboard.server.transport.coap.CoapTransportService.CURRENT_TIMESTAMP; import static org.thingsboard.server.transport.coap.CoapTransportService.DEVICE_INFO; import static org.thingsboard.server.transport.coap.CoapTransportService.MEASUREMENTS; +import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.isBinarySensor; +import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.isSensorError; @Slf4j public class CoapEfentoTransportResource extends AbstractCoapTransportResource { @@ -220,127 +222,186 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource { } } - private List getEfentoMeasurements(MeasurementsProtos.ProtoMeasurements protoMeasurements, UUID sessionId) { + List getEfentoMeasurements(MeasurementsProtos.ProtoMeasurements protoMeasurements, UUID sessionId) { String serialNumber = CoapEfentoUtils.convertByteArrayToString(protoMeasurements.getSerialNum().toByteArray()); boolean batteryStatus = protoMeasurements.getBatteryStatus(); int measurementPeriodBase = protoMeasurements.getMeasurementPeriodBase(); int measurementPeriodFactor = protoMeasurements.getMeasurementPeriodFactor(); int signal = protoMeasurements.getSignal(); - List channelsList = protoMeasurements.getChannelsList(); + long nextTransmissionAtMillis = TimeUnit.SECONDS.toMillis(protoMeasurements.getNextTransmissionAt()); + + List channelsList = protoMeasurements.getChannelsList(); + if (CollectionUtils.isEmpty(channelsList)) { + throw new IllegalStateException("[" + sessionId + "]: Failed to get Efento measurements, reason: channels list is empty!"); + } + Map valuesMap = new TreeMap<>(); - if (!CollectionUtils.isEmpty(channelsList)) { - int channel = 0; - JsonObject values; - for (MeasurementsProtos.ProtoChannel protoChannel : channelsList) { - channel++; - boolean isBinarySensor = false; - MeasurementTypeProtos.MeasurementType measurementType = protoChannel.getType(); - String measurementTypeName = measurementType.name(); - if (measurementType.equals(MeasurementTypeProtos.MeasurementType.OK_ALARM) - || measurementType.equals(MeasurementTypeProtos.MeasurementType.FLOODING)) { - isBinarySensor = true; - } - if (measurementPeriodFactor == 0 && isBinarySensor) { - measurementPeriodFactor = 14; - } else { - measurementPeriodFactor = 1; + for (int channel = 0; channel < channelsList.size(); channel++) { + ProtoChannel protoChannel = channelsList.get(channel); + List sampleOffsetsList = protoChannel.getSampleOffsetsList(); + if (CollectionUtils.isEmpty(sampleOffsetsList)) { + log.trace("[{}][{}] sampleOffsetsList list is empty!", sessionId, protoChannel.getType().name()); + continue; + } + boolean isBinarySensor = isBinarySensor(protoChannel.getType()); + int channelPeriodFactor = (measurementPeriodFactor == 0 ? (isBinarySensor ? 14 : 1) : measurementPeriodFactor); + int measurementPeriod = measurementPeriodBase * channelPeriodFactor; + long measurementPeriodMillis = TimeUnit.SECONDS.toMillis(measurementPeriod); + long startTimestampMillis = TimeUnit.SECONDS.toMillis(protoChannel.getTimestamp()); + + for (int i = 0; i < sampleOffsetsList.size(); i++) { + int sampleOffset = sampleOffsetsList.get(i); + if (isSensorError(sampleOffset)) { + log.warn("[{}],[{}] Sensor error value! Ignoring.", sessionId, sampleOffset); + continue; } - int measurementPeriod = measurementPeriodBase * measurementPeriodFactor; - long measurementPeriodMillis = TimeUnit.SECONDS.toMillis(measurementPeriod); - long nextTransmissionAtMillis = TimeUnit.SECONDS.toMillis(protoMeasurements.getNextTransmissionAt()); - int startPoint = protoChannel.getStartPoint(); - int startTimestamp = protoChannel.getTimestamp(); - long startTimestampMillis = TimeUnit.SECONDS.toMillis(startTimestamp); - List sampleOffsetsList = protoChannel.getSampleOffsetsList(); - if (!CollectionUtils.isEmpty(sampleOffsetsList)) { - int sampleOfssetsListSize = sampleOffsetsList.size(); - for (int i = 0; i < sampleOfssetsListSize; i++) { - int sampleOffset = sampleOffsetsList.get(i); - Integer previousSampleOffset = isBinarySensor && i > 0 ? sampleOffsetsList.get(i - 1) : null; - if (sampleOffset == -32768) { - log.warn("[{}],[{}] Sensor error value! Ignoring.", sessionId, sampleOffset); - } else { - switch (measurementType) { - case TEMPERATURE: - values = valuesMap.computeIfAbsent(startTimestampMillis, k -> - CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); - values.addProperty("temperature_" + channel, ((double) (startPoint + sampleOffset)) / 10f); - startTimestampMillis = startTimestampMillis + measurementPeriodMillis; - break; - case WATER_METER: - values = valuesMap.computeIfAbsent(startTimestampMillis, k -> - CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); - values.addProperty("pulse_counter_water_" + channel, ((double) (startPoint + sampleOffset))); - startTimestampMillis = startTimestampMillis + measurementPeriodMillis; - break; - case HUMIDITY: - values = valuesMap.computeIfAbsent(startTimestampMillis, k -> - CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); - values.addProperty("humidity_" + channel, (double) (startPoint + sampleOffset)); - startTimestampMillis = startTimestampMillis + measurementPeriodMillis; - break; - case ATMOSPHERIC_PRESSURE: - values = valuesMap.computeIfAbsent(startTimestampMillis, k -> - CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); - values.addProperty("pressure_" + channel, (double) (startPoint + sampleOffset) / 10f); - startTimestampMillis = startTimestampMillis + measurementPeriodMillis; - break; - case DIFFERENTIAL_PRESSURE: - values = valuesMap.computeIfAbsent(startTimestampMillis, k -> - CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); - values.addProperty("pressure_diff_" + channel, (double) (startPoint + sampleOffset)); - startTimestampMillis = startTimestampMillis + measurementPeriodMillis; - break; - case OK_ALARM: - boolean currentIsOk = sampleOffset < 0; - if (previousSampleOffset != null) { - boolean previousIsOk = previousSampleOffset < 0; - boolean isOk = previousIsOk && currentIsOk; - boolean isAlarm = !previousIsOk && !currentIsOk; - if (isOk || isAlarm) { - break; - } - } - String data = currentIsOk ? "OK" : "ALARM"; - long sampleOffsetMillis = TimeUnit.SECONDS.toMillis(sampleOffset); - long measurementTimestamp = startTimestampMillis + Math.abs(sampleOffsetMillis); - values = valuesMap.computeIfAbsent(measurementTimestamp - 1000, k -> - CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); - values.addProperty("ok_alarm_" + channel, data); - break; - case PULSE_CNT: - values = valuesMap.computeIfAbsent(startTimestampMillis, k -> - CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); - values.addProperty("pulse_cnt_" + channel, (double) (startPoint + sampleOffset)); - startTimestampMillis = startTimestampMillis + measurementPeriodMillis; - break; - case NO_SENSOR: - case UNRECOGNIZED: - log.trace("[{}][{}] Sensor error value! Ignoring.", sessionId, measurementTypeName); - break; - default: - log.trace("[{}],[{}] Unsupported measurementType! Ignoring.", sessionId, measurementTypeName); - break; - } + + JsonObject values; + if (isBinarySensor) { + boolean currentIsOk = sampleOffset < 0; + Integer previousSampleOffset = i > 0 ? sampleOffsetsList.get(i - 1) : null; + if (previousSampleOffset != null) { //compare with previous value + boolean previousIsOk = previousSampleOffset < 0; + if (currentIsOk == previousIsOk) { + break; } } + long sampleOffsetMillis = TimeUnit.SECONDS.toMillis(sampleOffset); + long measurementTimestamp = startTimestampMillis + Math.abs(sampleOffsetMillis); + values = valuesMap.computeIfAbsent(measurementTimestamp - 1000, k -> + CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); + addBinarySample(protoChannel, currentIsOk, values, channel + 1, sessionId); } else { - log.trace("[{}][{}] sampleOffsetsList list is empty!", sessionId, measurementTypeName); + long timestampMillis = startTimestampMillis + i * measurementPeriodMillis; + values = valuesMap.computeIfAbsent(timestampMillis, k -> CoapEfentoUtils.setDefaultMeasurements( + serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k)); + addContinuesSample(protoChannel, sampleOffset, values, channel + 1, sessionId); } } - } else { - throw new IllegalStateException("[" + sessionId + "]: Failed to get Efento measurements, reason: channels list is empty!"); } - if (!CollectionUtils.isEmpty(valuesMap)) { - List efentoMeasurements = new ArrayList<>(); - for (Long ts : valuesMap.keySet()) { - EfentoTelemetry measurement = new EfentoTelemetry(ts, valuesMap.get(ts)); - efentoMeasurements.add(measurement); - } - return efentoMeasurements; - } else { + + if (CollectionUtils.isEmpty(valuesMap)) { throw new IllegalStateException("[" + sessionId + "]: Failed to collect Efento measurements, reason, values map is empty!"); } + + return valuesMap.entrySet().stream() + .map(entry -> new EfentoTelemetry(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + private void addContinuesSample(ProtoChannel protoChannel, int sampleOffset, JsonObject values, int channelNumber, UUID sessionId) { + int startPoint = protoChannel.getStartPoint(); + + switch (protoChannel.getType()) { + case MEASUREMENT_TYPE_TEMPERATURE: + values.addProperty("temperature_" + channelNumber, ((double) (startPoint + sampleOffset)) / 10f); + break; + case MEASUREMENT_TYPE_WATER_METER: + values.addProperty("pulse_counter_water_" + channelNumber, ((double) (startPoint + sampleOffset))); + break; + case MEASUREMENT_TYPE_HUMIDITY: + values.addProperty("humidity_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_ATMOSPHERIC_PRESSURE: + values.addProperty("pressure_" + channelNumber, (double) (startPoint + sampleOffset) / 10f); + break; + case MEASUREMENT_TYPE_DIFFERENTIAL_PRESSURE: + values.addProperty("pressure_diff_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_PULSE_CNT: + values.addProperty("pulse_cnt_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_IAQ: + values.addProperty("iaq_" + channelNumber, (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_ELECTRICITY_METER: + values.addProperty("watt_hour_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_SOIL_MOISTURE: + values.addProperty("soil_moisture_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_AMBIENT_LIGHT: + values.addProperty("ambient_light_" + channelNumber, (double) (startPoint + sampleOffset) / 10f); + break; + case MEASUREMENT_TYPE_HIGH_PRESSURE: + values.addProperty("high_pressure_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_DISTANCE_MM: + values.addProperty("distance_mm_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_WATER_METER_ACC_MINOR: + values.addProperty("acc_counter_water_minor_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_WATER_METER_ACC_MAJOR: + values.addProperty("acc_counter_water_major_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_HUMIDITY_ACCURATE: + values.addProperty("humidity_relative_" + channelNumber, (double) (startPoint + sampleOffset) / 10f); + break; + case MEASUREMENT_TYPE_STATIC_IAQ: + values.addProperty("static_iaq_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_CO2_EQUIVALENT: + values.addProperty("co2_ppm_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_BREATH_VOC: + values.addProperty("breath_voc_ppm_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_PERCENTAGE: + values.addProperty("percentage_" + channelNumber, (double) (startPoint + sampleOffset) / 100f); + break; + case MEASUREMENT_TYPE_VOLTAGE: + values.addProperty("voltage_" + channelNumber, (double) (startPoint + sampleOffset) / 10f); + break; + case MEASUREMENT_TYPE_CURRENT: + values.addProperty("current_" + channelNumber, (double) (startPoint + sampleOffset) / 100f); + break; + case MEASUREMENT_TYPE_PULSE_CNT_ACC_MINOR: + values.addProperty("pulse_cnt_minor_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_PULSE_CNT_ACC_MAJOR: + values.addProperty("pulse_cnt_major_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_ELEC_METER_ACC_MINOR: + values.addProperty("elec_meter_minor_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_ELEC_METER_ACC_MAJOR: + values.addProperty("elec_meter_major_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MINOR: + values.addProperty("pulse_cnt_wide_minor_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MAJOR: + values.addProperty("pulse_cnt_wide_major_" + channelNumber, (double) (startPoint + sampleOffset)); + break; + case MEASUREMENT_TYPE_CURRENT_PRECISE: + values.addProperty("current_precise_" + channelNumber, (double) (startPoint + sampleOffset)/1000f); + break; + case MEASUREMENT_TYPE_NO_SENSOR: + case UNRECOGNIZED: + log.trace("[{}][{}] Sensor error value! Ignoring.", sessionId, protoChannel.getType().name()); + break; + default: + log.trace("[{}],[{}] Unsupported measurementType! Ignoring.", sessionId, protoChannel.getType().name()); + break; + } + } + + private void addBinarySample(ProtoChannel protoChannel, boolean valueIsOk, JsonObject values, int channel, UUID sessionId) { + switch (protoChannel.getType()) { + case MEASUREMENT_TYPE_OK_ALARM: + values.addProperty("ok_alarm_" + channel, valueIsOk ? "OK" : "ALARM"); + break; + case MEASUREMENT_TYPE_FLOODING: + values.addProperty("flooding_" + channel, valueIsOk ? "OK" : "WATER_DETECTED"); + break; + case MEASUREMENT_TYPE_OUTPUT_CONTROL: + values.addProperty("output_control_" + channel, valueIsOk ? "OFF" : "ON"); + break; + default: + log.trace("[{}],[{}] Unsupported binary measurementType! Ignoring.", sessionId, protoChannel.getType().name()); + break; + } } private EfentoTelemetry getEfentoDeviceInfo(DeviceInfoProtos.ProtoDeviceInfo protoDeviceInfo) { diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/utils/CoapEfentoUtils.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/utils/CoapEfentoUtils.java index d87c97543e..96c1805be3 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/utils/CoapEfentoUtils.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/utils/CoapEfentoUtils.java @@ -16,11 +16,16 @@ package org.thingsboard.server.transport.coap.efento.utils; import com.google.gson.JsonObject; +import org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.MeasurementType; import java.text.SimpleDateFormat; import java.util.Date; import java.util.TimeZone; +import static org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_FLOODING; +import static org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_OK_ALARM; +import static org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_OUTPUT_CONTROL; + public class CoapEfentoUtils { public static String convertByteArrayToString(byte[] a) { @@ -50,4 +55,12 @@ public class CoapEfentoUtils { return values; } + public static boolean isBinarySensor(MeasurementType type) { + return type == MEASUREMENT_TYPE_OK_ALARM || type == MEASUREMENT_TYPE_FLOODING || type == MEASUREMENT_TYPE_OUTPUT_CONTROL; + } + + public static boolean isSensorError(int sampleOffset) { + return sampleOffset >= 8355840 && sampleOffset <= 8388607; + } + } diff --git a/common/transport/coap/src/main/proto/efento/proto_config.proto b/common/transport/coap/src/main/proto/efento/proto_config.proto index 6212681403..25dbec883f 100644 --- a/common/transport/coap/src/main/proto/efento/proto_config.proto +++ b/common/transport/coap/src/main/proto/efento/proto_config.proto @@ -22,26 +22,13 @@ option java_package = "org.thingsboard.server.gen.transport.coap"; option java_outer_classname = "ConfigProtos"; /* Message containing optional channels control parameters */ -message ProtoChannelControl { +message ProtoOutputControlState { /* Channel index */ uint32 channel_index = 1; - /* Control parameters. Maximal number equals 4. This field is channel specific: */ - /* IO_control channel: */ - /* - control_params[0]: */ - /* - Byte 0: On state configuration */ - /* 0x01 - Low */ - /* 0x02 - High */ - /* 0x03 - High-Z (disconnected) */ - /* - Byte 1: Off state configuration */ - /* 0x01 - Low */ - /* 0x02 - High */ - /* 0x03 - High-Z (disconnected) */ - /* - Byte 2: Power on channel state */ - /* 0x01 - On */ - /* 0x02 - Off */ - repeated uint32 control_params = 2; + /* Channel state ON/OFF. Range (1 - OFF; 2 - ON) */ + uint32 channel_state = 2; } /* Message containing request data for accesing calibration parameters */ @@ -58,6 +45,39 @@ message ProtoCalibrationParameters { repeated int32 parameters = 3; } +enum BleAdvertisingPeriodMode { + + /* Invalid value */ + BLE_ADVERTISING_PERIOD_MODE_UNSPECIFIED = 0; + + /* Default behavior - faster advertising when measurement period is < 15s. */ + BLE_ADVERTISING_PERIOD_MODE_DEFAULT = 1; + + /* User-configured normal interval is used. */ + BLE_ADVERTISING_PERIOD_MODE_NORMAL = 2; + + /* User-configured fast interval is used. */ + BLE_ADVERTISING_PERIOD_MODE_FAST = 3; +} + +/* Message containing BLE advertising period configuration */ +message ProtoBleAdvertisingPeriod { + + /* BLE advertising mode: */ + /* - 1: Default, BLE advertising interval is set to 1022.5ms or some lower value, based on continuous measurement period. */ + /* - 2: Normal, uses user-configured value from 'normal' field. */ + /* - 3: Fast, uses user-configured value from 'fast' field (must be lower than or equal to 'normal' field). */ + BleAdvertisingPeriodMode mode = 1; + + /* BLE advertising interval when in normal mode, configured in 0.625ms steps. */ + /* Range: [32:16384] */ + uint32 normal = 2; + + /* BLE advertising interval when in fast mode, configured in 0.625ms steps. */ + /* Range: [32:16384] */ + uint32 fast = 3; +} + /* Main message sent in the payload. Each field in this message is independent of the others - only parameters that should be */ /* changed need to be sent in the payload. */ /* If the value of a selected parameter shall not be changed, do not include it in the payload */ @@ -115,16 +135,26 @@ message ProtoConfig { /* 65535 - disable transfer limit function */ uint32 transfer_limit_timer = 10; - /* Data (measurements) server IP address */ - /* IP of the data server as string in form x.x.x.x. For example: 18.184.24.239 */ + /* For firmware >= 6.07.00: */ + /* IP or URL address of the data (measurements) server */ + /* The IP or URL of the data server, provided as string with a maximum length of 31 characters */ + /* For example, use "18.184.24.239" for an IP address or "efento.test.io" for a URL */ + /* For firmware < 6.07.00: */ + /* IP address of the data (measurements) server */ + /* For example, use "18.184.24.239" */ string data_server_ip = 11; /* Data (measurements) server port */ /* Range: [1:65535] */ uint32 data_server_port = 12; - /* Update server IP address */ - /* IP of data server as string in form x.x.x.x. For example: 18.184.24.239 */ + /* For firmware >= 6.07.00: */ + /* IP or URL address of the update server */ + /* The IP or URL of the update server, provided as string with a maximum length of 31 characters */ + /* For example, use "18.184.24.239" for an IP address or "efento.test.io" for a URL */ + /* For firmware < 6.07.00: */ + /* IP address of the update server */ + /* For example, use "18.184.24.239" */ string update_server_ip = 13; /* Update server port for UDP transmission */ @@ -144,7 +174,8 @@ message ProtoConfig { /* 0xFFFFFFFF or 1000000 - automatic selection */ uint32 plmn_selection = 17; - /* Device will power off its cellular modem for requested number of seconds. Maximum number of seconds 604800 (7 days) */ + /* Device will power off its cellular modem for requested number of seconds. */ + /* Range: [60:604800] (1 minute : 7 days) */ /* This field is only sent by server */ uint32 disable_modem_request = 18; @@ -263,9 +294,8 @@ message ProtoConfig { /* Calendar configuration. Up to 6 calendars are supported */ repeated ProtoCalendar calendars = 47; - /* Control parameters for channels. Maximal number of requests equals 6 */ - /* This field is only sent by server */ - repeated ProtoChannelControl channels_control_request = 48; + /* DEPRECATED - Used for backward compatibility */ + reserved 48; /* Set/get calibration parameters for single channel. */ ProtoCalibrationParameters calibration_parameters_request = 49; @@ -295,7 +325,7 @@ message ProtoConfig { reserved 52, 53; /* Encryption key configuration. Sensor sends in this field two last bytes of SHA256 hash calculated from its current */ - /* encryption_key configuration. */ + /* encryption_key configuration. When encryption key is disabled one byte 0x7F (DEL) is sent. */ /* Max length: 16 bytes. */ /* 0x7F - encryption key disabled. */ bytes encryption_key = 54; @@ -309,4 +339,14 @@ message ProtoConfig { /* String with special character 0x7F (DEL) only indicates that automatic password is turn on */ /* Password can only be set to custom value if apn_user_name has been configured (is not automatic) */ string apn_password = 56; + + /* Reserved by versions above 06.20.00 */ + reserved 57; + + /* Control output state on channel pin. Maximal number of requests equals 3 */ + /* This field is only sent by server */ + repeated ProtoOutputControlState output_control_state_request = 58; + + /* BLE advertising period configuration. */ + ProtoBleAdvertisingPeriod ble_advertising_period = 59; } \ No newline at end of file diff --git a/common/transport/coap/src/main/proto/efento/proto_device_info.proto b/common/transport/coap/src/main/proto/efento/proto_device_info.proto index 791d01acd7..a0cb9bc2f2 100644 --- a/common/transport/coap/src/main/proto/efento/proto_device_info.proto +++ b/common/transport/coap/src/main/proto/efento/proto_device_info.proto @@ -106,6 +106,11 @@ message ProtoModem /* parameters[32] - Rx_time - [0.1s] - Range: [0:2147483647]. Unknown value: -1 */ /* parameters[33] - Tx_time - [0.1s] - Range: [0:2147483647]. Unknown value: -1 */ repeated sint32 parameters = 2; + + /* ICCID of inserted/soldered sim card. String up to 22 characters long. */ + /* 0x7F if sim card is not detected, empty (not sent) if device does not have modem. */ + /* This field is only sent by device */ + string sim_card_identification = 3; } message ProtoUpdateInfo @@ -115,16 +120,16 @@ message ProtoUpdateInfo uint32 timestamp = 1; /* Update status, possible values: */ - /* - 0x1 - No update yet */ - /* - 0x2 - No error */ - /* - 0x3 - UDP socekt error */ - /* - 0x4 - Hash error */ - /* - 0x5 - Missing packet error */ - /* - 0x6 - Invalid data error */ - /* - 0x7 - Sending timeout error */ - /* - 0x8 - No SW to update error */ - /* - 0x9 - Sending unexpected error */ - /* - 0x10 - Unexpected error */ + /* - 1 - No update yet */ + /* - 2 - No error */ + /* - 3 - UDP socekt error */ + /* - 4 - Hash error */ + /* - 5 - Missing packet error */ + /* - 6 - Invalid data error */ + /* - 7 - Sending timeout error */ + /* - 8 - No SW to update error */ + /* - 9 - Sending unexpected error */ + /* - 10 - Unexpected error */ uint32 status = 2; } diff --git a/common/transport/coap/src/main/proto/efento/proto_measurement_types.proto b/common/transport/coap/src/main/proto/efento/proto_measurement_types.proto index 66a6c95d09..98029bed51 100644 --- a/common/transport/coap/src/main/proto/efento/proto_measurement_types.proto +++ b/common/transport/coap/src/main/proto/efento/proto_measurement_types.proto @@ -19,79 +19,160 @@ option java_package = "org.thingsboard.server.gen.transport.coap"; option java_outer_classname = "MeasurementTypeProtos"; enum MeasurementType { - NO_SENSOR = 0; - - /* [°C] - Celsius degree. Resolution 0.1°C. Range [-273.2-4000.0]. Type: Continuous */ - TEMPERATURE = 1; - - /* [% RH] - Relative humidity. Resolution 1%. Range [0-100]. Type: Continuous */ - HUMIDITY = 2; - - /* [hPa] - Hectopascal (1hPa = 100Pa). Resolution 0.1hPa. Range: [1.0-2000.0]. Type: Continuous */ - ATMOSPHERIC_PRESSURE = 3; - - /* [Pa] - Pascal. Resolution 1Pa. Range [-10000-10000]Type: Continuous */ - DIFFERENTIAL_PRESSURE = 4; - - /* Sign indicates state: (+) ALARM, (-) OK. Type: Binary */ - OK_ALARM = 5; - - /* [IAQ] - Iaq index. Resolution 1IAQ. Range [0-500]. Sensor return also calibration status */ - /* as offset to measured value: */ - /* - offset 3000: Sensor not stabilized (always returns 25 IAQ value) */ - /* - offset 2000: Calibration required (sensor returns not accurate values) */ - /* - offset 1000: Calibration on-going (sensor returns not accurate values) */ - /* - offset 0: Calibration done (best accuracy of IAQ sensor) */ - /* Type: Continuous */ - IAQ = 6; - - /* Sign indicates water presence: (+) water not detected, (-) water detected. Type: Binary */ - FLOODING = 7; - - /* [NB] Number of pulses. Resolution 1 pulse. Range [0-16711679]. Type: Continuous */ - PULSE_CNT = 8; - - /* [Wh] - Watthour; Resolution 1Wh. Range [0-16711679]. Number of Watthours in a single period. Type: Continuous */ - ELECTRICITY_METER = 9; - - /* [l] - Liter. Resolution 1l. Range [0-16711679]. Number of litres in a single period. Type: Continuous */ - WATER_METER = 10; - - /* [kPa] - Kilopascal (1kPa = 1000Pa); Resolution 1kPa. Range [-1000-0]. Soil moisture (tension). Type: Continuous */ - SOIL_MOISTURE = 11; - - /* [ppm] - Parts per million. Resolution 1ppm. Range [0-1000000]. Carbon monoxide concentration. Type: Continuous */ - CO_GAS = 12; - - /* [ppm] - Parts per million. Resolution 0.01ppm. Range [0-1000000.00]. Nitrogen dioxide concentration. Type: Continuous*/ - NO2_GAS = 13; - - /* [ppm] - Parts per million. Resolution 1ppm. Range [0-1000000]. Hydrogen sulfide concentration. Type: Continuous */ - H2S_GAS = 14; - - /* [lx] - Illuminance. Resolution 0.1lx. Range [0-100000.0]. Type: Continuous */ - AMBIENT_LIGHT = 15; - - /* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0-1000]. */ - /* particles with an aerodynamic diameter less than 1 micrometer. Type: Continuous */ - PM_1_0 = 16; // µg/m^3 - - /* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0-1000]. */ - /* particles with an aerodynamic diameter less than 2.5 micrometers. Type: Continuous */ - PM_2_5 = 17; // µg/m^3 - - /* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0-1000]. */ - /* particles with an aerodynamic diameter less than 10 micrometers. Type: Continuous */ - PM_10_0 = 18; // µg/m^3 - - /* [dB] - Decibels. Resolution 0.1 dB. Range: [0-130.0]. Noise level. Type: Continuous */ - NOISE_LEVEL = 19; // 0.1 dB - - /* [ppm] - Parts per million. Resolution 1ppm. Range [0-1000000]. Ammonia concentration. Type: Continuous */ - NH3_GAS = 20; - - /* [ppm] - Parts per million. Resolution 1ppm. Range [0-1000000]. Methane concentration. Type: Continuous */ - CH4_GAS = 21; + /* [] - No sensor on the channel */ + MEASUREMENT_TYPE_NO_SENSOR = 0; + + /* [°C] - Celsius degree. Resolution 0.1°C. Range [-273.2:4000.0]. Type: Continuous */ + MEASUREMENT_TYPE_TEMPERATURE = 1; + + /* [% RH] - Relative humidity. Resolution 1%. Range [0:100]. Type: Continuous */ + MEASUREMENT_TYPE_HUMIDITY = 2; + + /* [hPa] - Hectopascal (1hPa = 100Pa). Resolution 0.1hPa. Range: [1.0:2000.0]. Atmospheric pressure. Type: Continuous */ + MEASUREMENT_TYPE_ATMOSPHERIC_PRESSURE = 3; + + /* [Pa] - Pascal. Resolution 1Pa. Range [-10000:10000]. Differential pressure. Type: Continuous */ + MEASUREMENT_TYPE_DIFFERENTIAL_PRESSURE = 4; + + /* Sign indicates state: (+) ALARM, (-) OK. Type: Binary */ + MEASUREMENT_TYPE_OK_ALARM = 5; + + /* [IAQ] - IAQ index. Resolution 1IAQ. Range [0:500]. To get IAQ index the value should be divided by 3. */ + /* Sensor return also calibration status as metadata (is the remainder when the absolute value is divided by 3): */ + /* - 0: Calibration required (sensor returns not accurate values) */ + /* - 1: Calibration on-going (sensor returns not accurate values) */ + /* - 2: Calibration done (best accuracy of IAQ sensor) */ + /* Type: Continuous */ + MEASUREMENT_TYPE_IAQ = 6; + + /* Sign indicates water presence: (+) water not detected, (-) water detected. Type: Binary */ + MEASUREMENT_TYPE_FLOODING = 7; + + /* [NB] Number of pulses. Resolution 1 pulse. Range [0:8000000]. Type: Continuous */ + MEASUREMENT_TYPE_PULSE_CNT = 8; + + /* [Wh] - Watthour; Resolution 1Wh. Range [0:8000000]. Number of Watthours in a single period. Type: Continuous */ + MEASUREMENT_TYPE_ELECTRICITY_METER = 9; + + /* [l] - Liter. Resolution 1l. Range [0:8000000]. Number of litres in a single period. Type: Continuous */ + MEASUREMENT_TYPE_WATER_METER = 10; + + /* [kPa] - Kilopascal (1kPa = 1000Pa); Resolution 1kPa. Range [-1000:0]. Soil moisture (tension). Type: Continuous */ + MEASUREMENT_TYPE_SOIL_MOISTURE = 11; + + /* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon monoxide concentration. Type: Continuous */ + MEASUREMENT_TYPE_CO_GAS = 12; + + /* [ppm] - Parts per million. Resolution 1.0ppm. Range [0:1000000]. Nitrogen dioxide concentration. Type: Continuous */ + MEASUREMENT_TYPE_NO2_GAS = 13; + + /* [ppm] - Parts per million. Resolution 0.01ppm. Range [0.00:80000.00]. Hydrogen sulfide concentration. Type: Continuous */ + MEASUREMENT_TYPE_H2S_GAS = 14; + + /* [lx] - Lux. Resolution 0.1lx. Range [0.0:100000.0]. Illuminance. Type: Continuous */ + MEASUREMENT_TYPE_AMBIENT_LIGHT = 15; + + /* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3. Range [0:1000]. */ + /* Particles with an aerodynamic diameter less than 1 micrometer. Type: Continuous */ + MEASUREMENT_TYPE_PM_1_0 = 16; + + /* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3. Range [0:1000]. */ + /* Particles with an aerodynamic diameter less than 2.5 micrometers. Type: Continuous */ + MEASUREMENT_TYPE_PM_2_5 = 17; + + /* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3. Range [0:1000]. */ + /* Particles with an aerodynamic diameter less than 10 micrometers. Type: Continuous */ + MEASUREMENT_TYPE_PM_10_0 = 18; + + /* [dB] - Decibels. Resolution 0.1 dB. Range: [0.0:200.0]. Noise level. Type: Continuous */ + MEASUREMENT_TYPE_NOISE_LEVEL = 19; + + /* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Ammonia concentration. Type: Continuous */ + MEASUREMENT_TYPE_NH3_GAS = 20; + + /* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Methane concentration. Type: Continuous */ + MEASUREMENT_TYPE_CH4_GAS = 21; + + /* [kPa] - Kilopascal (1kPa = 1000Pa, 100kPa = 1bar). Resolution 1kPa. Range [0:200000]. Pressure. Type: Continuous */ + MEASUREMENT_TYPE_HIGH_PRESSURE = 22; + + /* [mm] - Millimeter. Resolution 1mm. Range [0:100000]. Distance. Type: Continuous */ + MEASUREMENT_TYPE_DISTANCE_MM = 23; + + /* [l] - Liter. Resolution 1l. Range [0:1000000]. Accumulative water meter (minor). Type: Continuous */ + MEASUREMENT_TYPE_WATER_METER_ACC_MINOR = 24; + + /* [hl] - Hectoliter. Resolution 1hl. Range [0:1000000]. Accumulative water meter (major). Type: Continuous */ + MEASUREMENT_TYPE_WATER_METER_ACC_MAJOR = 25; + + /* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon dioxide concentration. Type: Continuous */ + MEASUREMENT_TYPE_CO2_GAS = 26; + + /* [% RH] - Relative humidity. Resolution 0.1%. Range [0.0:100.0]. Type: Continuous */ + MEASUREMENT_TYPE_HUMIDITY_ACCURATE = 27; + + /* [sIAQ] - Static IAQ index. Resolution 1IAQ. Range [0:10000]. To get static IAQ index the value should be divided by 3. */ + /* Sensor return also calibration status as metadata (is the remainder when the absolute value is divided by 3): */ + /* - 0: Calibration required (sensor returns not accurate values) */ + /* - 1: Calibration on-going (sensor returns not accurate values) */ + /* - 2: Calibration done (best accuracy of IAQ sensor) */ + /* Type: Continuous */ + MEASUREMENT_TYPE_STATIC_IAQ = 28; + + /* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. CO2 equivalent. */ + /* To get CO2 equivalent the value should be divided by 3. */ + /* Sensor return also calibration status as metadata (is the remainder when the absolute value is divided by 3): */ + /* - 0: Calibration required (sensor returns not accurate values) */ + /* - 1: Calibration on-going (sensor returns not accurate values) */ + /* - 2: Calibration done (best accuracy of IAQ sensor) */ + /* Type: Continuous */ + MEASUREMENT_TYPE_CO2_EQUIVALENT = 29; + + /* [ppm] - Parts per million. Resolution 1ppm. Range [0:100000]. Breath VOC estimate. */ + /* To get breath VOC estimate the value should be divided by 3. */ + /* Sensor return also calibration status as metadata (is the remainder when the absolute value is divided by 3): */ + /* - 0: Calibration required (sensor returns not accurate values) */ + /* - 1: Calibration on-going (sensor returns not accurate values) */ + /* - 2: Calibration done (best accuracy of IAQ sensor) */ + /* Type: Continuous */ + MEASUREMENT_TYPE_BREATH_VOC = 30; + + /* Special measurement type reserved for cellular gateway. */ + /* Type: Continuous */ + MEASUREMENT_TYPE_CELLULAR_GATEWAY = 31; + + /* [%] - Percentage. Resolution 0.01%. Range [0.00:100.00]. Type: Continuous */ + MEASUREMENT_TYPE_PERCENTAGE = 32; + + /* [mV] - Milivolt. Resolution 0.1mV. Range [0.0:100000.0]. Type: Continuous */ + MEASUREMENT_TYPE_VOLTAGE = 33; + + /* [mA] - Milliampere. Resolution 0.01mA. Range [0.0:10000.00]. Type: Continuous */ + MEASUREMENT_TYPE_CURRENT = 34; + + /* [NB] Number of pulses. Resolution 1 pulse. Range [0:1000000]. Type: Continuous */ + MEASUREMENT_TYPE_PULSE_CNT_ACC_MINOR = 35; + + /* [kNB] Number of kilopulses. Resolution 1 kilopulse. Range [0:1000000]. Type: Continuous */ + MEASUREMENT_TYPE_PULSE_CNT_ACC_MAJOR = 36; + + /* [Wh] - Watt-hour; Resolution 1Wh. Range [0:1000000]. Number of watt-hours in a single period. Type: Continuous */ + MEASUREMENT_TYPE_ELEC_METER_ACC_MINOR = 37; + + /* [kWh] - Kilowatt-hour; Resolution 1kWh. Range [0:1000000]. Number of kilowatt-hours in a single period. Type: Continuous */ + MEASUREMENT_TYPE_ELEC_METER_ACC_MAJOR = 38; + + /* [NB] Number of pulses (wide range). Resolution 1 pulse. Range [0:999999]. Type: Continuous */ + MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MINOR = 39; + + /* [MNB] Number of megapulses (wide range). Resolution 1 megapulse. Range [0:999999]. Type: Continuous */ + MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MAJOR = 40; + + /* [mA] - Milliampere. Resolution 0.001mA. Range [-4 000.000:4 000.000]. Type: Continuous */ + MEASUREMENT_TYPE_CURRENT_PRECISE = 41; + + /* Sign indicates state: (+) ON, (-) OFF. Type: Binary */ + MEASUREMENT_TYPE_OUTPUT_CONTROL = 42; } diff --git a/common/transport/coap/src/main/proto/efento/proto_measurements.proto b/common/transport/coap/src/main/proto/efento/proto_measurements.proto index f8549feb54..80af07d958 100644 --- a/common/transport/coap/src/main/proto/efento/proto_measurements.proto +++ b/common/transport/coap/src/main/proto/efento/proto_measurements.proto @@ -19,103 +19,108 @@ import "efento/proto_measurement_types.proto"; option java_package = "org.thingsboard.server.gen.transport.coap"; option java_outer_classname = "MeasurementsProtos"; -message ProtoChannel{ - /* Type of channel */ +message ProtoChannel { + + /* Type of channel */ MeasurementType type = 1; - /* Timestamp of the first sample (the oldest one) in seconds since UNIX EPOCH 01-01-1970 */ + /* Timestamp of the first sample (the oldest one) in seconds since UNIX EPOCH 01-01-1970 */ int32 timestamp = 2; - /* Only used for 'Continuous' sensor types. Value used as the starting point for calculating the values of all */ - /* measurements in the package. */ - /* Format defined by 'MeasurementType' field */ + /* Only used for 'Continuous' sensor types. Value used as the starting point for calculating the values of all */ + /* measurements in the package. */ + /* Format defined by 'MeasurementType' field */ sint32 start_point = 4; - /* 'Continuous' sensor types */ - /* Value of the offset from the 'start_point' for each measurement in the package. The oldest sample first ([0]). */ - /* 'sample_offsets' format defined by 'MeasurementType' field. */ - /* Example: MeasurementType = 1 (temperature), start_point = 100, sample_offsets[0] = 15, sample_offsets[1] = 20 */ - /* 1st sample in the package temperature value = 11.5 °C, 2nd sample in the package temperature value = 12 °C */ - /* Calculating timestamps of the measurements: timestamp = 1606391700, measurement_period_base = 60, */ - /* measurement_period_factor = 1. Timestamp of the 1st sample = 1606391700, timestamp of the 2nd sample = 1606391760 */ - /* 'Binary' sensor types: */ - /* Absolute value of the 'sample_offsets' field indicates the offset in seconds from 'timestamp' field. */ - /* Sign (- or +) indicates the state of measurements depend of sensor type. */ - /* Value of this field equals to '1' or '-1' indicates the state at the 'timestamp'. Other values */ - /* indicate the state of the relay at the time (in seconds) equal to 'timestamp' + value. */ - /* Values of this field are incremented starting from 1 (1->0: state at the time */ - /* of 'timestamp', 2->1: state at the time equal to 'timestamp' + 1 s, 3->2 : */ - /* state at the time equal to 'timestamp' + 2 s, etc.). The first and the last sample define the time range of the */ - /* measurements. Only state changes in the time range are included in the 'sample_offsets' field */ - /* Examples: if 'timestamp' value is 1553518060 and 'sample_offsets' equals '1', it means that at 1553518060 the state */ - /* was high, if 'timestamp' value is 1553518060 and 'sample_offsets' equals '-9', it means at 1553518068 the state was low */ + /* 'Continuous' sensor types */ + /* Value of the offset from the 'start_point' for each measurement in the package. The oldest sample first ([0]). */ + /* 'sample_offsets' format defined by 'MeasurementType' field. */ + /* If the 'sample_offset' has a value from the range [8355840: 8388607], it should be interpreted as a sensor error code. */ + /* In that case value of the 'start_point' field should not be added to this 'sample_offset'. See ES6-264 for error codes. */ + /* Example: MeasurementType = 1 (temperature), start_point = 100, sample_offsets[0] = 15, sample_offsets[1] = 20, */ + /* sample_offset[2] = 8388605 */ + /* 1st sample in the package temperature value = 11.5 °C, 2nd sample in the package temperature value = 12 °C */ + /* 3rd sample in the package has no temperature value. It has information about failure of MCP9808 (temperature) sensor. */ + /* Calculating timestamps of the measurements: timestamp = 1606391700, measurement_period_base = 60, */ + /* measurement_period_factor = 1. Timestamp of the 1st sample = 1606391700, timestamp of the 2nd sample = 1606391760, */ + /* timestamp of the 3rd sample 1606391820 */ + + /* 'Binary' sensor types: */ + /* Absolute value of the 'sample_offsets' field indicates the offset in seconds from 'timestamp' field. */ + /* Sign (- or +) indicates the state of measurements depending on the sensor type. */ + /* Value of this field equals to '1' or '-1' indicates the state at the 'timestamp'. Other values */ + /* indicate the state of the relay at the time (in seconds) equal to 'timestamp' + absolute value -1. */ + /* Values of this field are incremented starting from 1 (1->0: state at the time */ + /* of 'timestamp', 2->1: state at the time equal to 'timestamp' + 1 s, 3->2 : */ + /* state at the time equal to 'timestamp' + 2 s, etc.). The first and the last sample define the time range of the */ + /* measurements. Only state changes in the time range are included in the 'sample_offsets' field */ + /* Examples: if 'timestamp' value is 1553518060 and 'sample_offsets' equals '1', it means that at 1553518060 the state */ + /* was high, if 'timestamp' value is 1553518060 and 'sample_offsets' equals '-9', it means at 1553518068 the state was low */ repeated sint32 sample_offsets = 5 [packed=true]; - /* Deprecated - configuration is sent to endpoint 'c' */ - //int32 lo_threshold = 6; + /* Deprecated - configuration is sent to endpoint 'c' */ + /* int32 lo_threshold = 6; */ + reserved 6; - /* Deprecated - configuration is sent to endpoint 'c' */ - //int32 hi_threshold = 7; + /* Deprecated - configuration is sent to endpoint 'c' */ + /* int32 hi_threshold = 7; */ + reserved 7; - /* Deprecated - configurationis sent to endpoint 'c' */ - //int32 diff_threshold = 8; - } + /* Deprecated - configurations sent to endpoint 'c' */ + /* int32 diff_threshold = 8; */ + reserved 8; +} message ProtoMeasurements { - /* serial number of the device */ + /* Serial number of the device */ bytes serial_num = 1; - /* true - battery ok, false - battery low */ + /* Battery status: true - battery ok, false - battery low */ bool battery_status = 2; - /* 'Measurement_period_base' and 'measurement_period_factor' define how often the measurements are taken. */ - /* Sensors of 'Continuous' type take measurement each Measurement_period_base * measurement_period_factor. */ - /* Sensors of 'Binary' type take measurement each Measurement_period_base. */ - /* For backward compatibility with versions 5.x in case of binary/mixed sensors, if the 'measurement_period_factor' is */ - /* not sent (equal to 0), then the default value '14' shall be used for period calculation. */ - /* For backward compatibility with versions 5.x in case of continues sensors, if the measurement_period_factor is */ - /* not sent (equal to 0), then the default value '1' shall be used for period calculation. */ - /* measurement period base in seconds */ + /* 'Measurement_period_base' and 'measurement_period_factor' define how often the measurements are taken. */ + /* Sensors of 'Continuous' type take measurement each Measurement_period_base * measurement_period_factor. */ + /* Sensors of 'Binary' type take measurement each Measurement_period_base. */ + /* For backward compatibility with versions 5.x in case of binary/mixed sensors, if the 'measurement_period_factor' is */ + /* not sent (equal to 0), then the default value '14' shall be used for period calculation. */ + /* For backward compatibility with versions 5.x in case of continues sensors, if the measurement_period_factor is */ + /* not sent (equal to 0), then the default value '1' shall be used for period calculation. */ + /* measurement period base in seconds */ uint32 measurement_period_base = 3; - /* Measurement period factor */ + /* Measurement period factor */ uint32 measurement_period_factor = 8; repeated ProtoChannel channels = 4; - /* Timestamp of the next scheduled transmission. If the device will not send data until this time, */ - /* it should be considered as 'lost' */ + /* Timestamp of the next scheduled transmission. If the device will not send data until this time, */ + /* it should be considered as 'lost' */ uint32 next_transmission_at = 5; - /* reason of transmission - unsigned integer where each bit indicates different */ - /* possible communication reason. Can be more than one */ - /* - bit 0: first message after sensor reset */ - /* - bit 1: user button triggered */ - /* - bit 2: user BLE triggered */ - /* - bit 3-7: number of retries -> incremented after each unsuccessful transmission. Max value 4. */ - /* Set to 0 after a successful transmission. */ - /* - bit 8: channel 1 lower threshold exceeded */ - /* - bit 9: channel 1 lower threshold returned */ - /* - bit 10: channel 1 higher threshold exceeded */ - /* - bit 11: channel 1 higher threshold returned */ - /* - bit 12: channel 1 differential threshold crossed */ - /* - bits 13-17: channel 2 thresholds (same as for channel 1) */ - /* - bits 18-22: channel 3 thresholds (same as for channel 1) */ - /* - bits 23-27: channel 4 or 5 or 6 thresholds (same as for channel 1) */ + /* Reason of transmission - unsigned integer where each bit indicates different possible communication reason. */ + /* Can be more than one: */ + /* - bit 0: first message after sensor reset */ + /* - bit 1: user button triggered */ + /* - bit 2: user BLE triggered */ + /* - bit 3-7: number of retries -> incremented after each unsuccessful transmission. Max value 31. */ + /* Set to 0 after a successful transmission. */ + /* - bit 8...19: rule 1...12 was met */ + /* - bit 20: triggered after the end of the limit */ uint32 transfer_reason = 6; - /* Signal strength level mapped from RSSI */ - /* - 0 : 113 dBm or less */ - /* - 1 : 111 dBm */ - /* - 2...30 : 109...-53 dBm */ - /* - 31 : -51 dBm or greater */ - /* - 99 : Not known or not detectable */ + /* Signal strength level mapped from RSSI: */ + /* - 0: RSSI < -110 dBm */ + /* - 1: -110 dBm <= RSSI < -109 dBm */ + /* - 2...61: -109 <= RSSI < -108 dBm ... -50 dBm <= RSSI < -49 dBm */ + /* - 62: -49 dBm <= RSSI < -48 dBm */ + /* - 63: RSSI >= -48 dBm */ + /* - 99: Not known or not detectable */ uint32 signal = 7; - /* Hash of the current configuration. Hash value changes each time a device receives a new configuration */ + /* Hash of the current configuration. Hash value changes each time a device receives a new configuration */ uint32 hash = 9; - /* Optional string up to 36 bytes long. Can be set to any user define value or hold device's IMEI */ + /* Optional string up to 36 bytes long. Can be set to any user define value or hold device's IMEI */ string cloud_token = 16; } \ No newline at end of file diff --git a/common/transport/coap/src/main/proto/efento/proto_rule.proto b/common/transport/coap/src/main/proto/efento/proto_rule.proto index 4079683a3f..12550426b1 100644 --- a/common/transport/coap/src/main/proto/efento/proto_rule.proto +++ b/common/transport/coap/src/main/proto/efento/proto_rule.proto @@ -50,11 +50,19 @@ option java_outer_classname = "ProtoRuleProtos"; /* - CO2_EQUIVALENT - [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon dioxide equivalent. */ /* - BREATH_VOC - [ppm] - Parts per million. Resolution 1ppm. Range [0:100000]. Breath VOC estimate. */ /* - PERCENTAGE - [%] - Percentage. Resolution 0.01%. Range [0.00:100.00]. */ +/* - VOLTAGE - [mV] - Milivolt. Resolution 0.1mV. Range [0.0:100000.0]. */ +/* - CURRENT - [mA] - Miliampere. Resolution 0.01mA. Range [0.00:10000.00]. */ /* - PULSE_CNT_ACC_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [0:1000000]. Accumulative pulse counter (minor). */ /* - PULSE_CNT_ACC_MAJOR - [kNB] - Number of kilopulses. Resolution 1 kilopulse. Range [0:1000000]. */ /* Accumulative pulse counter (major). */ /* - ELEC_METER_ACC_MINOR - [Wh] - Watt-hour. Resolution 1Wh. Range [0:1000000]. Accumulative electricity meter (minor). */ /* - ELEC_METER_ACC_MAJOR - [kWh] - Kilowatt-hour. Resolution 1kWh. Range [0:1000000]. Accumulative electricity meter (major). */ +/* - PULSE_CNT_ACC_WIDE_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [0:999999]. */ +/* Accumulative pulse counter wide range (minor). */ +/* - PULSE_CNT_ACC_WIDE_MAJOR - [MNB] - Number of megapulses. Resolution 1 megapulse. Range [0:999999]. */ +/* Accumulative pulse counter wide range (major). */ +/* - CURRENT_PRECISE - [mA] - Miliampere. Resolution 0.001mA. Range [-4 000.000:4 000.000]. */ +/* - OUTPUT_CONTROL - Not applicable */ /* Encoding R: used to set relative values in the Rules (e.g. differential threshold and hysteresis) */ /* - TEMPERATURE - [°C] - Celsius degree. Resolution 0.1°C. Range [0.1:4273.2]. */ @@ -88,11 +96,19 @@ option java_outer_classname = "ProtoRuleProtos"; /* - CO2_EQUIVALENT - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Carbon dioxide equivalent. */ /* - BREATH_VOC - [ppm] - Parts per million. Resolution 1ppm. Range [1:100000]. Breath VOC estimate. */ /* - PERCENTAGE - [%] - Percentage. Resolution 0.01%. Range [0.01:100.00]. */ +/* - VOLTAGE - [mV] - Milivolt. Resolution 0.1mV. Range [0.1:100000.0]. */ +/* - CURRENT - [mA] - Miliampere. Resolution 0.01mA. Range [0.01:10000.00]. */ /* - PULSE_CNT_ACC_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [1:1000000]. Accumulative pulse counter (minor). */ /* - PULSE_CNT_ACC_MAJOR - [kNB] - Number of kilopulses. Resolution 1 kilopulse. Range [1:1000000]. */ /* Accumulative pulse counter (major). */ /* - ELEC_METER_ACC_MINOR - [Wh] - Watt-hour. Resolution 1Wh. Range [1:1000000]. Accumulative electricity meter (minor). */ /* - ELEC_METER_ACC_MAJOR - [kWh] - Kilowatt-hour. Resolution 1kWh. Range [1:1000000]. Accumulative electricity meter (major). */ +/* - PULSE_CNT_ACC_WIDE_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [1:999999]. */ +/* Accumulative pulse counter wide range (minor). */ +/* - PULSE_CNT_ACC_WIDE_MAJOR - [MNB] - Number of megapulses. Resolution 1 megapulse. Range [1:999999]. */ +/* Accumulative pulse counter wide range (major). */ +/* - CURRENT_PRECISE - [mA] - Miliampere. Resolution 0.001mA. Range [0.001:8 000.000]. */ +/* - OUTPUT_CONTROL - Not applicable */ /* Condition to be checked by the device. If the condition is true, an action is triggered */ enum Condition { @@ -202,6 +218,10 @@ enum Action { /* To trigger the transmission with ACK */ ACTION_TRIGGER_TRANSMISSION_WITH_ACK = 3; + + /* To change BLE advertising period mode to fast (with lower user-configured advertising interval). */ + /* Once the rule is deactived avertising period mode returns to previously configured value. */ + ACTION_FAST_ADVERTISING_MODE = 4; } /* Type of a rule calendars. */ diff --git a/common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/efento/CoapEfentTransportResourceTest.java b/common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/efento/CoapEfentTransportResourceTest.java new file mode 100644 index 0000000000..5faaaee1d4 --- /dev/null +++ b/common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/efento/CoapEfentTransportResourceTest.java @@ -0,0 +1,144 @@ +/** + * Copyright © 2016-2024 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.coap.efento; + +import com.google.protobuf.ByteString; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos; +import org.thingsboard.server.gen.transport.coap.MeasurementsProtos; +import org.thingsboard.server.transport.coap.CoapTransportContext; + +import java.nio.ByteBuffer; +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class CoapEfentTransportResourceTest { + + private static CoapEfentoTransportResource coapEfentoTransportResource; + + @BeforeAll + static void setUp() { + var ctxMock = mock(CoapTransportContext.class); + coapEfentoTransportResource = new CoapEfentoTransportResource(ctxMock, "testName"); + } + + @Test + void checkContinuousSensor() { + long tsInSec = Instant.now().getEpochSecond(); + MeasurementsProtos.ProtoMeasurements measurements = MeasurementsProtos.ProtoMeasurements.newBuilder() + .setSerialNum(integerToByteString(1234)) + .setCloudToken("test_token") + .setMeasurementPeriodBase(180) + .setMeasurementPeriodFactor(1) + .setBatteryStatus(true) + .setSignal(0) + .setNextTransmissionAt(1000) + .setTransferReason(0) + .setHash(0) + .addAllChannels(List.of(MeasurementsProtos.ProtoChannel.newBuilder() + .setType(MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_TEMPERATURE) + .setTimestamp(Math.toIntExact(tsInSec)) + .addAllSampleOffsets(List.of(223, 224)) + .build(), + MeasurementsProtos.ProtoChannel.newBuilder() + .setType(MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_HUMIDITY) + .setTimestamp(Math.toIntExact(tsInSec)) + .addAllSampleOffsets(List.of(20, 30)) + .build() + )) + .build(); + List efentoMeasurements = coapEfentoTransportResource.getEfentoMeasurements(measurements, UUID.randomUUID()); + assertThat(efentoMeasurements).hasSize(2); + assertThat(efentoMeasurements.get(0).getTs()).isEqualTo(tsInSec * 1000); + assertThat(efentoMeasurements.get(0).getValues().getAsJsonObject().get("temperature_1").getAsDouble()).isEqualTo(22.3); + assertThat(efentoMeasurements.get(0).getValues().getAsJsonObject().get("humidity_2").getAsDouble()).isEqualTo(20); + assertThat(efentoMeasurements.get(1).getTs()).isEqualTo((tsInSec + 180) * 1000); + assertThat(efentoMeasurements.get(1).getValues().getAsJsonObject().get("temperature_1").getAsDouble()).isEqualTo(22.4); + assertThat(efentoMeasurements.get(1).getValues().getAsJsonObject().get("humidity_2").getAsDouble()).isEqualTo(30); + } + + @Test + void checkBinarySensor() { + long tsInSec = Instant.now().getEpochSecond(); + MeasurementsProtos.ProtoMeasurements measurements = MeasurementsProtos.ProtoMeasurements.newBuilder() + .setSerialNum(integerToByteString(1234)) + .setCloudToken("test_token") + .setMeasurementPeriodBase(180) + .setMeasurementPeriodFactor(1) + .setBatteryStatus(true) + .setSignal(0) + .setNextTransmissionAt(1000) + .setTransferReason(0) + .setHash(0) + .addChannels(MeasurementsProtos.ProtoChannel.newBuilder() + .setType(MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_OK_ALARM) + .setTimestamp(Math.toIntExact(tsInSec)) + .addAllSampleOffsets(List.of(1, 1)) + .build()) + .build(); + List efentoMeasurements = coapEfentoTransportResource.getEfentoMeasurements(measurements, UUID.randomUUID()); + assertThat(efentoMeasurements).hasSize(1); + assertThat(efentoMeasurements.get(0).getTs()).isEqualTo(tsInSec * 1000); + assertThat(efentoMeasurements.get(0).getValues().getAsJsonObject().get("ok_alarm_1").getAsString()).isEqualTo("ALARM"); + } + + @Test + void checkBinarySensorWhenValueIsVarying() { + long tsInSec = Instant.now().getEpochSecond(); + MeasurementsProtos.ProtoMeasurements measurements = MeasurementsProtos.ProtoMeasurements.newBuilder() + .setSerialNum(integerToByteString(1234)) + .setCloudToken("test_token") + .setMeasurementPeriodBase(180) + .setMeasurementPeriodFactor(1) + .setBatteryStatus(true) + .setSignal(0) + .setNextTransmissionAt(1000) + .setTransferReason(0) + .setHash(0) + .addChannels(MeasurementsProtos.ProtoChannel.newBuilder() + .setType(MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_OK_ALARM) + .setTimestamp(Math.toIntExact(tsInSec)) + .addAllSampleOffsets(List.of(1, -10)) + .build()) + .build(); + List efentoMeasurements = coapEfentoTransportResource.getEfentoMeasurements(measurements, UUID.randomUUID()); + assertThat(efentoMeasurements).hasSize(2); + assertThat(efentoMeasurements.get(0).getTs()).isEqualTo(tsInSec * 1000); + assertThat(efentoMeasurements.get(0).getValues().getAsJsonObject().get("ok_alarm_1").getAsString()).isEqualTo("ALARM"); + assertThat(efentoMeasurements.get(1).getTs()).isEqualTo((tsInSec + 9) * 1000); + assertThat(efentoMeasurements.get(1).getValues().getAsJsonObject().get("ok_alarm_1").getAsString()).isEqualTo("OK"); + } + + public static ByteString integerToByteString(Integer intValue) { + // Allocate a ByteBuffer with the size of an integer (4 bytes) + ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES); + + // Put the integer value into the ByteBuffer + buffer.putInt(intValue); + + // Convert the ByteBuffer to a byte array + byte[] byteArray = buffer.array(); + + // Create a ByteString from the byte array + return ByteString.copyFrom(byteArray); + } + +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/DeduplicationUtil.java b/common/util/src/main/java/org/thingsboard/common/util/DeduplicationUtil.java new file mode 100644 index 0000000000..fd25e8b924 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/DeduplicationUtil.java @@ -0,0 +1,44 @@ +/** + * Copyright © 2016-2024 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.common.util; + +import org.springframework.util.ConcurrentReferenceHashMap; + +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType.SOFT; + +public class DeduplicationUtil { + + private static final ConcurrentMap cache = new ConcurrentReferenceHashMap<>(16, SOFT); + + public static boolean alreadyProcessed(Object deduplicationKey, long deduplicationDuration) { + AtomicBoolean alreadyProcessed = new AtomicBoolean(false); + cache.compute(deduplicationKey, (key, lastProcessedTs) -> { + if (lastProcessedTs != null) { + long passed = System.currentTimeMillis() - lastProcessedTs; + if (passed <= deduplicationDuration) { + alreadyProcessed.set(true); + return lastProcessedTs; + } + } + return System.currentTimeMillis(); + }); + return alreadyProcessed.get(); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java index 870c0bc505..b01d1c4ea0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java @@ -15,11 +15,13 @@ */ package org.thingsboard.server.dao.sqlts.dictionary; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.hibernate.exception.ConstraintViolationException; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.dao.dictionary.KeyDictionaryDao; import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryCompositeKey; import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryEntry; @@ -34,14 +36,15 @@ import java.util.concurrent.locks.ReentrantLock; @Component @Slf4j @SqlDao +@RequiredArgsConstructor public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService implements KeyDictionaryDao { - private final ConcurrentMap keyDictionaryMap = new ConcurrentHashMap<>(); - protected static final ReentrantLock creationLock = new ReentrantLock(); + private final KeyDictionaryRepository keyDictionaryRepository; - @Autowired - private KeyDictionaryRepository keyDictionaryRepository; + private final ConcurrentMap keyDictionaryMap = new ConcurrentHashMap<>(); + private static final ReentrantLock creationLock = new ReentrantLock(); + @Transactional(propagation = Propagation.NOT_SUPPORTED) @Override public Integer getOrSaveKeyId(String strKey) { Integer keyId = keyDictionaryMap.get(strKey); diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java b/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java index ee21b36c53..4ab9d3c457 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java @@ -17,8 +17,8 @@ package org.thingsboard.monitoring.data; public class Latencies { - public static final String WS_UPDATE = "wsUpdate"; public static final String WS_CONNECT = "wsConnect"; + public static final String WS_SUBSCRIBE = "wsSubscribe"; public static final String LOG_IN = "logIn"; public static String request(String key) { diff --git a/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseMonitoringService.java b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseMonitoringService.java index 7df767caef..c667c5b7ac 100644 --- a/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseMonitoringService.java +++ b/monitoring/src/main/java/org/thingsboard/monitoring/service/BaseMonitoringService.java @@ -100,7 +100,10 @@ public abstract class BaseMonitoringService, T ext reporter.reportLatency(Latencies.LOG_IN, stopWatch.getTime()); try (WsClient wsClient = wsClientFactory.createClient(accessToken)) { + stopWatch.start(); wsClient.subscribeForTelemetry(devices, TransportHealthChecker.TEST_TELEMETRY_KEY).waitForReply(); + reporter.reportLatency(Latencies.WS_SUBSCRIBE, stopWatch.getTime()); + for (BaseHealthChecker healthChecker : healthCheckers) { check(healthChecker, wsClient); } diff --git a/pom.xml b/pom.xml index 5b6f30bef8..09cf0393b7 100755 --- a/pom.xml +++ b/pom.xml @@ -2097,10 +2097,6 @@ io.jsonwebtoken jjwt-impl - - com.fasterxml.jackson.dataformat - jackson-dataformat-xml - diff --git a/ui-ngx/src/app/core/services/mobile.service.ts b/ui-ngx/src/app/core/services/mobile.service.ts index 03772ce167..7b11b817b4 100644 --- a/ui-ngx/src/app/core/services/mobile.service.ts +++ b/ui-ngx/src/app/core/services/mobile.service.ts @@ -30,6 +30,7 @@ const dashboardLoadedHandler = 'tbMobileDashboardLoadedHandler'; const dashboardLayoutHandler = 'tbMobileDashboardLayoutHandler'; const navigationHandler = 'tbMobileNavigationHandler'; const mobileHandler = 'tbMobileHandler'; +const mobileReadyHandler = 'tbMobileReadyHandler'; // @dynamic @Injectable({ @@ -54,6 +55,7 @@ export class MobileService { this.mobileApp = isDefined(this.mobileChannel); if (this.mobileApp) { window.addEventListener('message', this.onWindowMessageListener); + this.mobileChannel.callHandler(mobileReadyHandler); } } diff --git a/ui-ngx/src/app/modules/common/modules-map.ts b/ui-ngx/src/app/modules/common/modules-map.ts index fcb4e0b411..c06863c6e8 100644 --- a/ui-ngx/src/app/modules/common/modules-map.ts +++ b/ui-ngx/src/app/modules/common/modules-map.ts @@ -99,6 +99,9 @@ import * as TbJsonPipe from '@shared/pipe/tbJson.pipe'; import * as TruncatePipe from '@shared/pipe/truncate.pipe'; import * as ImagePipe from '@shared/pipe/image.pipe'; +import * as EllipsisChipListDirective from '@shared/directives/ellipsis-chip-list.directive'; +import * as TruncateWithTooltipDirective from '@shared/directives/truncate-with-tooltip.directive'; + import * as coercion from '@shared/decorators/coercion'; import * as enumerable from '@shared/decorators/enumerable'; import * as TbInject from '@shared/decorators/tb-inject'; @@ -422,6 +425,9 @@ class ModulesMap implements IModulesMap { '@shared/pipe/truncate.pipe': TruncatePipe, '@shared/pipe/image.pipe': ImagePipe, + '@shared/directives/ellipsis-chip-list.directive': EllipsisChipListDirective, + '@shared/directives/truncate-with-tooltip.directive': TruncateWithTooltipDirective, + '@shared/decorators/coercion': coercion, '@shared/decorators/enumerable': enumerable, '@shared/decorators/tb-inject': TbInject, diff --git a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts index b5c94f9aeb..fa4e8bcc71 100644 --- a/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts +++ b/ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts @@ -20,12 +20,19 @@ import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { + ActionStatus, actionStatusTranslations, + ActionType, actionTypeTranslations, AuditLog, AuditLogMode } from '@shared/models/audit-log.models'; -import { EntityTypeResource, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { + AliasEntityType, + EntityType, + EntityTypeResource, + entityTypeTranslations +} from '@shared/models/entity-type.models'; import { AuditLogService } from '@core/http/audit-log.service'; import { TranslateService } from '@ngx-translate/core'; import { DatePipe } from '@angular/common'; @@ -82,7 +89,7 @@ export class AuditLogTableConfig extends EntityTableConfig('entityType', 'audit-log.entity-type', '20%', - (entity) => translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type)), + (entity) => this.getEntityTypeTranslation(entity.entityId.entityType)), new EntityTableColumn('entityName', 'audit-log.entity-name', '20%'), ); } @@ -95,9 +102,9 @@ export class AuditLogTableConfig extends EntityTableConfig('actionType', 'audit-log.type', '33%', - (entity) => translate.instant(actionTypeTranslations.get(entity.actionType))), + (entity) => this.getActionTypeTranslation(entity.actionType)), new EntityTableColumn('actionStatus', 'audit-log.status', '33%', - (entity) => translate.instant(actionStatusTranslations.get(entity.actionStatus))) + (entity) => this.getActionStatusTranslation(entity.actionStatus)) ); this.cellActionDescriptors.push( @@ -105,11 +112,32 @@ export class AuditLogTableConfig extends EntityTableConfig true, - onAction: ($event, entity) => this.showAuditLogDetails(entity) + onAction: (_, entity) => this.showAuditLogDetails(entity) } ); } + private getEntityTypeTranslation(entityType: EntityType | AliasEntityType): string { + if (entityTypeTranslations.has(entityType) && entityTypeTranslations.get(entityType).type) { + return this.translate.instant(entityTypeTranslations.get(entityType).type); + } + return entityType; + } + + private getActionTypeTranslation(actionType: ActionType): string { + if (actionTypeTranslations.has(actionType)) { + return this.translate.instant(actionTypeTranslations.get(actionType)); + } + return actionType; + } + + private getActionStatusTranslation(actionStatus: ActionStatus): string { + if (actionStatusTranslations.has(actionStatus)) { + return this.translate.instant(actionStatusTranslations.get(actionStatus)); + } + return actionStatus; + } + fetchAuditLogs(pageLink: TimePageLink): Observable> { switch (this.auditLogMode) { case AuditLogMode.TENANT: diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts index f7d37b167e..d4bfdd4694 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts @@ -109,12 +109,13 @@ export class BrokerConfigControlComponent implements ControlValueAccessor, Valid } writeValue(brokerConfig: BrokerConfig): void { - const brokerConfigState = { - ...brokerConfig, - version: brokerConfig.version || 5, - clientId: brokerConfig.clientId || 'tb_gw_' + generateSecret(5), - }; - this.brokerConfigFormGroup.reset(brokerConfigState, {emitEvent: false}); + const { + version = 5, + clientId = `tb_gw_${generateSecret(5)}`, + security = {}, + } = brokerConfig; + + this.brokerConfigFormGroup.reset({ ...brokerConfig, version, clientId, security }, { emitEvent: false }); this.cdr.markForCheck(); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html index 9f0566b566..7e1ab2bb62 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html @@ -18,7 +18,7 @@
-
+
{{mappingTypeTranslationsMap.get(mappingType) | translate}}
@@ -75,15 +75,22 @@ -
+ + +
+
- - +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html index 17f5185bc6..105a3c0e8e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html @@ -23,6 +23,16 @@ +
+
{{ 'gateway.hints.modbus-server' | translate }}
+
+ + + {{ 'gateway.enable' | translate }} + + +
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.scss index b70fe42401..3b7e7288c8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.scss @@ -16,9 +16,3 @@ :host { height: 100%; } - -:host ::ng-deep { - .mat-mdc-tab-body-content { - overflow: hidden !important; - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts index 1fca768980..aa0424da93 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts @@ -18,13 +18,15 @@ import { ChangeDetectionStrategy, Component, forwardRef, Input, OnDestroy, Templ import { ControlValueAccessor, FormBuilder, + FormControl, FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, + UntypedFormControl, ValidationErrors, Validator, } from '@angular/forms'; -import { ConnectorType, ModbusBasicConfig } from '@home/components/widget/lib/gateway/gateway-widget.models'; +import { ModbusBasicConfig } from '@home/components/widget/lib/gateway/gateway-widget.models'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; import { takeUntil } from 'rxjs/operators'; @@ -33,6 +35,7 @@ import { Subject } from 'rxjs'; import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive'; import { ModbusSlaveConfigComponent } from '../modbus-slave-config/modbus-slave-config.component'; import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master-table.component'; +import { isEqual } from '@core/utils'; @Component({ selector: 'tb-modbus-basic-config', @@ -66,6 +69,7 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat @Input() generalTabContent: TemplateRef; basicFormGroup: FormGroup; + enableSlaveControl: FormControl; onChange: (value: ModbusBasicConfig) => void; onTouched: () => void; @@ -77,13 +81,21 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat master: [], slave: [], }); + this.enableSlaveControl = new FormControl(false); this.basicFormGroup.valueChanges .pipe(takeUntil(this.destroy$)) - .subscribe(value => { - this.onChange(value); + .subscribe(({ master, slave }) => { + this.onChange({ master, slave: slave ?? {} }); this.onTouched(); }); + + this.enableSlaveControl.valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(enable => { + this.updateSlaveEnabling(enable); + this.basicFormGroup.get('slave').updateValueAndValidity({emitEvent: !!this.onChange}); + }); } ngOnDestroy(): void { @@ -106,11 +118,25 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat }; this.basicFormGroup.setValue(editedBase, {emitEvent: false}); + this.enableSlaveControl.setValue(!!basicConfig.slave && !isEqual(basicConfig.slave, {})); } - validate(): ValidationErrors | null { - return this.basicFormGroup.valid ? null : { - basicFormGroup: {valid: false} - }; + validate(basicFormControl: UntypedFormControl): ValidationErrors | null { + const { master, slave } = basicFormControl.value; + const isEmpty = !master?.slaves?.length && (isEqual(slave, {}) || !slave); + if (!this.basicFormGroup.valid || isEmpty) { + return { + basicFormGroup: {valid: false} + }; + } + return null; + } + + private updateSlaveEnabling(isEnabled: boolean): void { + if (isEnabled) { + this.basicFormGroup.get('slave').enable({emitEvent: false}); + } else { + this.basicFormGroup.get('slave').disable({emitEvent: false}); + } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html index 02d2d1491d..f53e3ee277 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html @@ -26,12 +26,16 @@ -
- - {{ keyControl.get('tag').value }}{{ '-' }}{{ keyControl.get('value').value }} - - {{ keyControl.get('tag').value }} +
+ {{ keyControl.get('tag').value }}{{ '-' }}{{ keyControl.get('value').value }}
+ +
+
{{ 'gateway.key' | translate }}: {{ keyControl.get('tag').value }}
+
{{ 'gateway.address' | translate }}: {{ keyControl.get('address').value }}
+
{{ 'gateway.type' | translate }}: {{ keyControl.get('type').value }}
+
+
@@ -98,7 +102,7 @@ name="value" formControlName="objectsCount" placeholder="{{ 'gateway.set' | translate }}" - [readonly]="!editableDataTypes.includes(keyControl.get('type').value)" + [readonly]="!ModbusEditableDataTypes.includes(keyControl.get('type').value)" />
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss index 0e9f9a432f..445ac6c0ae 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss @@ -20,10 +20,7 @@ max-width: 700px; .title-container { - max-width: 11vw; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap + width: 180px; } .key-panel { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts index 5b312f52c8..93ca232e38 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts @@ -27,6 +27,7 @@ import { import { TbPopoverComponent } from '@shared/components/popover.component'; import { ModbusDataType, + ModbusEditableDataTypes, ModbusFunctionCodeTranslationsMap, ModbusObjectCountByDataType, ModbusValue, @@ -40,6 +41,7 @@ import { generateSecret } from '@core/utils'; import { coerceBoolean } from '@shared/decorators/coercion'; import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; +import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive'; @Component({ selector: 'tb-modbus-data-keys-panel', @@ -50,6 +52,7 @@ import { Subject } from 'rxjs'; CommonModule, SharedModule, GatewayHelpLinkPipe, + TruncateWithTooltipDirective, ] }) export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy { @@ -72,14 +75,15 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy { functionCodesMap = new Map(); defaultFunctionCodes = []; - readonly editableDataTypes = [ModbusDataType.BYTES, ModbusDataType.BITS, ModbusDataType.STRING]; + readonly ModbusEditableDataTypes = ModbusEditableDataTypes; readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap; private destroy$ = new Subject(); private readonly defaultReadFunctionCodes = [3, 4]; - private readonly defaultWriteFunctionCodes = [5, 6, 15, 16]; - private readonly stringAttrUpdatesWriteFunctionCodes = [6, 16]; + private readonly bitsReadFunctionCodes = [1, 2]; + private readonly defaultWriteFunctionCodes = [6, 16]; + private readonly bitsWriteFunctionCodes = [5, 15]; constructor(private fb: UntypedFormBuilder) {} @@ -161,7 +165,7 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy { private observeKeyDataType(keyFormGroup: FormGroup): void { keyFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(dataType => { - if (!this.editableDataTypes.includes(dataType)) { + if (!this.ModbusEditableDataTypes.includes(dataType)) { keyFormGroup.get('objectsCount').patchValue(ModbusObjectCountByDataType[dataType], {emitEvent: false}); } this.updateFunctionCodes(keyFormGroup, dataType); @@ -177,23 +181,23 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy { } private getFunctionCodes(dataType: ModbusDataType): number[] { + const writeFunctionCodes = [ + ...(dataType === ModbusDataType.BITS ? this.bitsWriteFunctionCodes : []), ...this.defaultWriteFunctionCodes + ]; + if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) { - return dataType === ModbusDataType.STRING - ? this.stringAttrUpdatesWriteFunctionCodes - : this.defaultWriteFunctionCodes; + return writeFunctionCodes.sort((a, b) => a - b); } const functionCodes = [...this.defaultReadFunctionCodes]; if (dataType === ModbusDataType.BITS) { - const bitsFunctionCodes = [1, 2]; - functionCodes.push(...bitsFunctionCodes); - functionCodes.sort(); + functionCodes.push(...this.bitsReadFunctionCodes); } if (this.keysType === ModbusValueKey.RPC_REQUESTS) { - functionCodes.push(...this.defaultWriteFunctionCodes); + functionCodes.push(...writeFunctionCodes); } - return functionCodes; + return functionCodes.sort((a, b) => a - b); } private getDefaultFunctionCodes(): number[] { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html index e16aa0c51f..8f8aeaeef7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html @@ -16,9 +16,12 @@ -->
+
+
{{ 'gateway.hints.modbus-master' | translate }}
+
-
+
{{ 'gateway.servers-slaves' | translate}}
@@ -63,33 +66,40 @@ {{ 'gateway.name' | translate }} - - {{ mapping['name'] }} + + {{ slave['name'] }} {{ 'gateway.client-communication-type' | translate }} - - {{ ModbusProtocolLabelsMap.get(mapping['type']) }} + + {{ ModbusProtocolLabelsMap.get(slave['type']) }} - -
+ + +
+
- - +
- +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts index 2cb66f6062..4787523b40 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts @@ -34,18 +34,17 @@ import { ControlValueAccessor, FormArray, FormBuilder, - NG_VALIDATORS, NG_VALUE_ACCESSOR, UntypedFormGroup, - ValidationErrors, - Validator, } from '@angular/forms'; import { ModbusMasterConfig, ModbusProtocolLabelsMap, + ModbusSlaveInfo, + ModbusValues, SlaveConfig } from '@home/components/widget/lib/gateway/gateway-widget.models'; -import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; +import { isDefinedAndNotNull } from '@core/utils'; import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; import { ModbusSlaveDialogComponent } from '../modbus-slave-dialog/modbus-slave-dialog.component'; @@ -62,16 +61,11 @@ import { TbTableDatasource } from '@shared/components/table/table-datasource.abs useExisting: forwardRef(() => ModbusMasterTableComponent), multi: true }, - { - provide: NG_VALIDATORS, - useExisting: forwardRef(() => ModbusMasterTableComponent), - multi: true - } ], standalone: true, imports: [CommonModule, SharedModule] }) -export class ModbusMasterTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { +export class ModbusMasterTableComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy { @ViewChild('searchInput') searchInputField: ElementRef; @@ -138,12 +132,6 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat this.pushDataAsFormArrays(master.slaves); } - validate(): ValidationErrors | null { - return this.slaves.controls.length ? null : { - slavesFormGroup: {valid: false} - }; - } - enterFilterMode(): void { this.textSearchMode = true; this.cdr.detectChanges(); @@ -164,12 +152,12 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat } const withIndex = isDefinedAndNotNull(index); const value = withIndex ? this.slaves.at(index).value : {}; - this.dialog.open(ModbusSlaveDialogComponent, { + this.dialog.open(ModbusSlaveDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { value, - buttonTitle: withIndex ? 'action.add' : 'action.apply' + buttonTitle: withIndex ? 'action.apply' : 'action.add' } }).afterClosed() .pipe(take(1), takeUntil(this.destroy$)) @@ -185,7 +173,7 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat }); } - deleteMapping($event: Event, index: number): void { + deleteSlave($event: Event, index: number): void { if ($event) { $event.stopPropagation(); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.html new file mode 100644 index 0000000000..c2dead1abc --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.html @@ -0,0 +1,92 @@ + + +
+ + {{ 'gateway.key' | translate }} + + + warning + + +
+
+ + {{ 'gateway.rpc.type' | translate }} + + {{ type }} + + + + {{ 'gateway.rpc.functionCode' | translate }} + + {{ ModbusFunctionCodeTranslationsMap.get(code) | translate}} + + +
+
+ + {{ 'gateway.rpc.value' | translate }} + + + warning + + +
+
+ + {{ 'gateway.rpc.address' | translate }} + + + warning + + + + {{ 'gateway.rpc.objectsCount' | translate }} + + +
+
+ diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.ts new file mode 100644 index 0000000000..17c48cc595 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.ts @@ -0,0 +1,166 @@ +/// +/// Copyright © 2016-2024 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + ChangeDetectionStrategy, + Component, + forwardRef, + OnDestroy, +} from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + UntypedFormGroup, + ValidationErrors, + Validator, + Validators +} from '@angular/forms'; +import { SharedModule } from '@shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { + ModbusDataType, + ModbusEditableDataTypes, + ModbusFunctionCodeTranslationsMap, + ModbusObjectCountByDataType, + ModbusValue, + noLeadTrailSpacesRegex, +} from '@home/components/widget/lib/gateway/gateway-widget.models'; + +@Component({ + selector: 'tb-modbus-rpc-parameters', + templateUrl: './modbus-rpc-parameters.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ModbusRpcParametersComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => ModbusRpcParametersComponent), + multi: true + } + ], + standalone: true, + imports: [ + CommonModule, + SharedModule, + ], +}) +export class ModbusRpcParametersComponent implements ControlValueAccessor, Validator, OnDestroy { + + rpcParametersFormGroup: UntypedFormGroup; + functionCodes: Array; + + readonly ModbusEditableDataTypes = ModbusEditableDataTypes; + readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap; + + readonly modbusDataTypes = Object.values(ModbusDataType) as ModbusDataType[]; + readonly writeFunctionCodes = [5, 6, 15, 16]; + + private readonly defaultFunctionCodes = [3, 4, 6, 16]; + private readonly readFunctionCodes = [1, 2, 3, 4]; + private readonly bitsFunctionCodes = [...this.readFunctionCodes, ...this.writeFunctionCodes]; + + private onChange: (value: ModbusValue) => void; + private onTouched: () => void; + + private destroy$ = new Subject(); + + constructor(private fb: FormBuilder) { + this.rpcParametersFormGroup = this.fb.group({ + tag: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + type: [ModbusDataType.BYTES, [Validators.required]], + functionCode: [this.defaultFunctionCodes[0], [Validators.required]], + value: [{value: '', disabled: true}, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], + address: [null, [Validators.required]], + objectsCount: [1, [Validators.required]], + }); + + this.updateFunctionCodes(this.rpcParametersFormGroup.get('type').value); + this.observeValueChanges(); + this.observeKeyDataType(); + this.observeFunctionCode(); + } + + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + + registerOnChange(fn: (value: ModbusValue) => void): void { + this.onChange = fn; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + validate(): ValidationErrors | null { + return this.rpcParametersFormGroup.valid ? null : { + rpcParametersFormGroup: { valid: false } + }; + } + + writeValue(value: ModbusValue): void { + this.rpcParametersFormGroup.patchValue(value, {emitEvent: false}); + } + + private observeValueChanges(): void { + this.rpcParametersFormGroup.valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((value) => { + this.onChange(value); + this.onTouched(); + }); + } + + private observeKeyDataType(): void { + this.rpcParametersFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(dataType => { + if (!this.ModbusEditableDataTypes.includes(dataType)) { + this.rpcParametersFormGroup.get('objectsCount').patchValue(ModbusObjectCountByDataType[dataType], {emitEvent: false}); + } + this.updateFunctionCodes(dataType); + }); + } + + private observeFunctionCode(): void { + this.rpcParametersFormGroup.get('functionCode').valueChanges + .pipe(takeUntil(this.destroy$)) + .subscribe(code => this.updateValueEnabling(code)); + } + + private updateValueEnabling(code: number): void { + if (this.writeFunctionCodes.includes(code)) { + this.rpcParametersFormGroup.get('value').enable({emitEvent: false}); + } else { + this.rpcParametersFormGroup.get('value').setValue(null); + this.rpcParametersFormGroup.get('value').disable({emitEvent: false}); + } + } + + private updateFunctionCodes(dataType: ModbusDataType): void { + this.functionCodes = dataType === ModbusDataType.BITS ? this.bitsFunctionCodes : this.defaultFunctionCodes; + if (!this.functionCodes.includes(this.rpcParametersFormGroup.get('functionCode').value)) { + this.rpcParametersFormGroup.get('functionCode').patchValue(this.functionCodes[0], {emitEvent: false}); + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html index d37c8cc9ee..d1a16c33bc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html @@ -16,16 +16,6 @@ -->
-
-
{{ 'gateway.hints.modbus-server' | translate }}
-
- - - {{ 'gateway.enable' | translate }} - - -
-
gateway.server-slave-config
@@ -161,7 +151,7 @@
-
gateway.poll-period
+
gateway.poll-period
@@ -178,6 +168,13 @@
+
+ + + {{ 'gateway.send-data-TB' | translate }} + + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss deleted file mode 100644 index a464832202..0000000000 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright © 2016-2024 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. - */ -$server-config-header-height: 132px; - -:host { - .slave-content { - height: calc(100% - #{$server-config-header-height}); - overflow: auto; - } - - .slave-container { - display: inherit; - } -} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts index 2a8488a2d4..5f5b4d293c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts @@ -43,7 +43,7 @@ import { import { SharedModule } from '@shared/shared.module'; import { CommonModule } from '@angular/common'; import { Subject } from 'rxjs'; -import { startWith, takeUntil } from 'rxjs/operators'; +import { takeUntil } from 'rxjs/operators'; import { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe'; import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component'; import { ModbusValuesComponent, } from '../modbus-values/modbus-values.component'; @@ -73,7 +73,6 @@ import { isEqual } from '@core/utils'; ModbusSecurityConfigComponent, GatewayPortTooltipPipe, ], - styleUrls: ['./modbus-slave-config.component.scss'], }) export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validator, OnDestroy { @@ -90,6 +89,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat readonly ModbusProtocolType = ModbusProtocolType; readonly modbusBaudrates = ModbusBaudrates; + private isSlaveEnabled = false; private readonly serialSpecificControlKeys = ['serialPort', 'baudrate']; private readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host']; @@ -106,11 +106,11 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]], serialPort: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], method: [ModbusMethodType.SOCKET], - unitId: [0, [Validators.required]], + unitId: [null, [Validators.required]], baudrate: [this.modbusBaudrates[0]], deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], - pollPeriod: [5000], + pollPeriod: [5000, [Validators.required]], sendDataToThingsBoard: [false], byteOrder:[ModbusOrderType.BIG], security: [], @@ -126,14 +126,9 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat this.observeValueChanges(); this.observeTypeChange(); - this.observeFormEnable(); this.observeShowSecurity(); } - get isSlaveEnabled(): boolean { - return this.slaveConfigFormGroup.get('sendDataToThingsBoard').value; - } - get protocolType(): ModbusProtocolType { return this.slaveConfigFormGroup.get('type').value; } @@ -160,7 +155,11 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat writeValue(slaveConfig: ModbusSlave): void { this.showSecurityControl.patchValue(!!slaveConfig.security && !isEqual(slaveConfig.security, {})); this.updateSlaveConfig(slaveConfig); - this.updateFormEnableState(slaveConfig.sendDataToThingsBoard); + } + + setDisabledState(isDisabled: boolean): void { + this.isSlaveEnabled = !isDisabled; + this.updateFormEnableState(); } private observeValueChanges(): void { @@ -180,7 +179,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat this.slaveConfigFormGroup.get('type').valueChanges .pipe(takeUntil(this.destroy$)) .subscribe(type => { - this.updateFormEnableState(this.isSlaveEnabled); + this.updateFormEnableState(); this.updateMethodType(type); }); } @@ -196,22 +195,15 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat } } - private observeFormEnable(): void { - this.slaveConfigFormGroup.get('sendDataToThingsBoard').valueChanges - .pipe(startWith(this.isSlaveEnabled), takeUntil(this.destroy$)) - .subscribe(value => this.updateFormEnableState(value)); - } - - private updateFormEnableState(enabled: boolean): void { - if (enabled) { + private updateFormEnableState(): void { + if (this.isSlaveEnabled) { this.slaveConfigFormGroup.enable({emitEvent: false}); this.showSecurityControl.enable({emitEvent: false}); } else { this.slaveConfigFormGroup.disable({emitEvent: false}); this.showSecurityControl.disable({emitEvent: false}); - this.slaveConfigFormGroup.get('sendDataToThingsBoard').enable({emitEvent: false}); } - this.updateEnablingByProtocol(this.protocolType); + this.updateEnablingByProtocol(); this.updateSecurityEnable(this.showSecurityControl.value); } @@ -229,9 +221,10 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat } } - private updateEnablingByProtocol(type: ModbusProtocolType): void { - const enableKeys = type === ModbusProtocolType.Serial ? this.serialSpecificControlKeys : this.tcpUdpSpecificControlKeys; - const disableKeys = type === ModbusProtocolType.Serial ? this.tcpUdpSpecificControlKeys : this.serialSpecificControlKeys; + private updateEnablingByProtocol(): void { + const isSerial = this.protocolType === ModbusProtocolType.Serial; + const enableKeys = isSerial ? this.serialSpecificControlKeys : this.tcpUdpSpecificControlKeys; + const disableKeys = isSerial ? this.tcpUdpSpecificControlKeys : this.serialSpecificControlKeys; if (this.isSlaveEnabled) { enableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({ emitEvent: false })); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html index 1e47479f03..088b385623 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html @@ -28,7 +28,7 @@
-
gateway.name
+
gateway.name
@@ -306,7 +306,7 @@
-
gateway.poll-period
+
gateway.poll-period
@@ -314,7 +314,7 @@
-
gateway.connect-attempt-time
+
gateway.connect-attempt-time
@@ -322,7 +322,7 @@
-
gateway.connect-attempt-count
+
gateway.connect-attempt-count
@@ -330,7 +330,7 @@
-
gateway.wait-after-failed-attempts
+
gateway.wait-after-failed-attempts
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.scss index 8cea184957..8900741a93 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.scss @@ -18,4 +18,9 @@ width: 80vw; max-width: 900px; } + + .slave-name-label { + margin-right: 16px; + color: rgba(0, 0, 0, 0.87); + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts index a00a488aa0..e21a9921a7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts @@ -163,7 +163,7 @@ export class ModbusSlaveDialogComponent extends DialogComponent void; + private onChange: (value: MQTTBasicConfig) => void; private onTouched: () => void; private destroy$ = new Subject(); @@ -100,7 +101,7 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator this.basicFormGroup.valueChanges .pipe(takeUntil(this.destroy$)) .subscribe(value => { - this.onChange(value); + this.onChange(this.getMappedMQTTConfig(value)); this.onTouched(); }); } @@ -110,7 +111,7 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator this.destroy$.complete(); } - registerOnChange(fn: (value: string) => void): void { + registerOnChange(fn: (value: MQTTBasicConfig) => void): void { this.onChange = fn; } @@ -135,13 +136,30 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator this.basicFormGroup.setValue(editedBase, {emitEvent: false}); } + private getMappedMQTTConfig(basicConfig: MQTTBasicConfig): MQTTBasicConfig { + let { broker, workers, dataMapping, requestsMapping } = basicConfig || {}; + + if (isDefinedAndNotNull(workers.maxNumberOfWorkers) || isDefinedAndNotNull(workers.maxMessageNumberPerWorker)) { + broker = { + ...broker, + ...workers, + }; + } + + if ((requestsMapping as RequestMappingData[])?.length) { + requestsMapping = this.getRequestDataObject(requestsMapping as RequestMappingData[]); + } + + return { broker, workers, dataMapping, requestsMapping }; + } + validate(): ValidationErrors | null { return this.basicFormGroup.valid ? null : { basicFormGroup: {valid: false} }; } - private getRequestDataArray(value: Record): RequestMappingData[] { + private getRequestDataArray(value: Record): RequestMappingData[] { const mappingConfigs = []; if (isObject(value)) { @@ -157,4 +175,17 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator return mappingConfigs; } + + private getRequestDataObject(array: RequestMappingData[]): Record { + return array.reduce((result, { requestType, requestValue }) => { + result[requestType].push(requestValue); + return result; + }, { + connectRequests: [], + disconnectRequests: [], + attributeRequests: [], + attributeUpdates: [], + serverSideRpc: [], + }); + } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts index eddad255a3..ff50480b92 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts @@ -85,7 +85,7 @@ export class OpcServerConfigComponent implements ControlValueAccessor, Validator subCheckPeriodInMillis: [10, [Validators.required, Validators.min(10)]], showMap: [false, []], security: [SecurityPolicy.BASIC128, []], - identity: [{}, [Validators.required]] + identity: [] }); this.serverConfigFormGroup.valueChanges.pipe( @@ -116,16 +116,25 @@ export class OpcServerConfigComponent implements ControlValueAccessor, Validator } writeValue(serverConfig: ServerConfig): void { - const { timeoutInMillis, scanPeriodInMillis, enableSubscriptions, subCheckPeriodInMillis, showMap, security } = serverConfig; - const serverConfigState = { + const { + timeoutInMillis = 1000, + scanPeriodInMillis = 1000, + enableSubscriptions = true, + subCheckPeriodInMillis = 10, + showMap = false, + security = SecurityPolicy.BASIC128, + identity = {}, + } = serverConfig; + + this.serverConfigFormGroup.reset({ ...serverConfig, - timeoutInMillis: timeoutInMillis || 1000, - scanPeriodInMillis: scanPeriodInMillis || 1000, - enableSubscriptions: enableSubscriptions || true, - subCheckPeriodInMillis: subCheckPeriodInMillis || 10, - showMap: showMap || false, - security: security || SecurityPolicy.BASIC128, - }; - this.serverConfigFormGroup.reset(serverConfigState, {emitEvent: false}); + timeoutInMillis, + scanPeriodInMillis, + enableSubscriptions, + subCheckPeriodInMillis, + showMap, + security, + identity, + }, { emitEvent: false }); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts index e4c5360cdf..a91a7c1b9f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts @@ -51,6 +51,7 @@ import { EntityType } from '@shared/models/entity-type.models'; import { AddConnectorConfigData, ConnectorBaseConfig, + ConnectorBaseInfo, ConnectorConfigurationModes, ConnectorType, GatewayConnector, @@ -60,7 +61,7 @@ import { } from './gateway-widget.models'; import { MatDialog } from '@angular/material/dialog'; import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component'; -import { debounceTime, take, takeUntil, tap } from 'rxjs/operators'; +import { debounceTime, filter, take, takeUntil, tap } from 'rxjs/operators'; import { ErrorStateMatcher } from '@angular/material/core'; import { PageData } from '@shared/models/page/page-data'; @@ -133,6 +134,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie private basicConfigSub: Subscription; + private jsonConfigSub: Subscription; + private subscriptionOptions: WidgetSubscriptionOptions = { callbacks: { onDataUpdated: () => this.ctx.ngZone.run(() => { @@ -209,17 +212,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie } }); - this.connectorForm.get('configurationJson').valueChanges.pipe( - takeUntil(this.destroy$) - ).subscribe((config) => { - const basicConfig = this.connectorForm.get('basicConfig'); - const type = this.connectorForm.get('type').value; - const mode = this.connectorForm.get('mode').value; - if (!isEqual(config, basicConfig?.value) && this.allowBasicConfig.has(type) && mode === ConnectorConfigurationModes.ADVANCED) { - this.connectorForm.get('basicConfig').patchValue(config, {emitEvent: false}); - } - }); - this.dataSource.sort = this.sort; this.dataSource.sortingDataAccessor = (data: AttributeData, sortHeaderId: string) => { switch (sortHeaderId) { @@ -275,7 +267,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie } saveConnector(): void { - const value = this.connectorForm.get('type').value === ConnectorType.MQTT ? this.getMappedMQTTValue() : this.connectorForm.value; + const value = { ...this.connectorForm.value }; value.configuration = camelCase(value.name) + '.json'; delete value.basicConfig; if (value.type !== ConnectorType.GRPC) { @@ -336,20 +328,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie }); } - private getMappedMQTTValue(): GatewayConnector { - const value = this.connectorForm.value; - return { - ...value, - configurationJson: { - ...value.configurationJson, - broker: { - ...value.configurationJson.broker, - ...value.configurationJson.workers, - } - } - }; - } - private updateData(reload: boolean = false): void { this.pageLink.sortOrder.property = this.sort.active; this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; @@ -383,11 +361,28 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie } const sharedIndex = this.sharedAttributeData.findIndex(data => { const sharedData = data.value; - return sharedData.name === connectorData.name && sharedData.ts && sharedData.ts <= connectorData.ts; + const hasSameName = sharedData.name === connectorData.name; + const hasEmptyConfig = isEqual(sharedData.configurationJson, {}) && hasSameName; + const hasSameConfig = this.hasSameConfig(sharedData.configurationJson, connectorData.configurationJson); + const isRecentlyCreated = sharedData.ts && sharedData.ts <= connectorData.ts; + return hasSameName && isRecentlyCreated && (hasSameConfig || hasEmptyConfig); }); return sharedIndex !== -1; } + private hasSameConfig(sharedDataConfigJson: ConnectorBaseInfo, connectorDataConfigJson: ConnectorBaseInfo): boolean { + const { name, id, enableRemoteLogging, logLevel, ...sharedDataConfig } = sharedDataConfigJson; + const { + name: connectorName, + id: connectorId, + enableRemoteLogging: connectorEnableRemoteLogging, + logLevel: connectorLogLevel, + ...connectorConfig + } = connectorDataConfigJson; + + return isEqual(sharedDataConfig, connectorConfig); + } + private combineData(): void { this.dataSource.data = [...this.activeData, ...this.inactiveData, ...this.sharedAttributeData].filter((item, index, self) => index === self.findIndex((t) => t.key === item.key) @@ -684,6 +679,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie this.basicConfigSub.unsubscribe(); } this.basicConfigSub = this.connectorForm.get('basicConfig').valueChanges.pipe( + filter(() => !!this.initialConnector), takeUntil(this.destroy$) ).subscribe((config) => { const configJson = this.connectorForm.get('configurationJson'); @@ -696,6 +692,22 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie }); } + private createJsonConfigWatcher(): void { + if (this.jsonConfigSub) { + this.jsonConfigSub.unsubscribe(); + } + this.jsonConfigSub = this.connectorForm.get('configurationJson').valueChanges.pipe( + takeUntil(this.destroy$) + ).subscribe((config) => { + const basicConfig = this.connectorForm.get('basicConfig'); + const type = this.connectorForm.get('type').value; + const mode = this.connectorForm.get('mode').value; + if (!isEqual(config, basicConfig?.value) && this.allowBasicConfig.has(type) && mode === ConnectorConfigurationModes.ADVANCED) { + this.connectorForm.get('basicConfig').patchValue(config, {emitEvent: false}); + } + }); + } + private confirmConnectorChange(): Observable { if (this.initialConnector && this.connectorForm.dirty) { return this.dialogService.confirm( @@ -734,19 +746,18 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie case ConnectorType.MQTT: case ConnectorType.OPCUA: case ConnectorType.MODBUS: - this.connectorForm.get('type').patchValue(connector.type, {emitValue: false, onlySelf: true}); - this.connectorForm.get('basicConfig').setValue({}, {emitEvent: false}); - + this.connectorForm.get('mode').setValue(connector.mode || ConnectorConfigurationModes.BASIC, {emitEvent: false}); setTimeout(() => { - this.connectorForm.patchValue({...connector, mode: connector.mode || ConnectorConfigurationModes.BASIC}); - this.createBasicConfigWatcher(); + this.connectorForm.patchValue(connector, {emitEvent: false}); this.connectorForm.markAsPristine(); + this.createBasicConfigWatcher(); }); break; default: this.connectorForm.patchValue({...connector, mode: null}); this.connectorForm.markAsPristine(); } + this.createJsonConfigWatcher(); } private setClientData(data: PageData): void { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html index eb428bebdc..3e94ae96d7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html @@ -50,46 +50,6 @@ placeholder="${params}"/> - - - {{ 'gateway.rpc.tag' | translate }} - - -
- - {{ 'gateway.rpc.type' | translate }} - - - {{ type }} - - - - - {{ 'gateway.rpc.functionCode' | translate }} - - - {{ modbusCodesTranslate.get(code) | translate}} - - - -
- - {{ 'gateway.rpc.value' | translate }} - - -
- - {{ 'gateway.rpc.address' | translate }} - - - - {{ 'gateway.rpc.objectsCount' | translate }} - - -
-
{{ 'gateway.rpc.methodRPC' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts index 3027cafa8d..989f9daeed 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts @@ -35,8 +35,6 @@ import { ConnectorType, GatewayConnectorDefaultTypesTranslatesMap, HTTPMethods, - ModbusCodesTranslate, - ModbusCommandTypes, noLeadTrailSpacesRegex, RPCCommand, RPCTemplateConfig, @@ -53,7 +51,7 @@ import { } from '@shared/components/dialog/json-object-edit-dialog.component'; import { jsonRequired } from '@shared/components/json-object-edit.component'; import { deepClone } from '@core/utils'; -import { filter, takeUntil, tap } from "rxjs/operators"; +import { takeUntil, tap } from "rxjs/operators"; import { Subject } from "rxjs"; @Component({ @@ -80,9 +78,7 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C saveTemplate: EventEmitter = new EventEmitter(); commandForm: FormGroup; - codesArray: Array = [1, 2, 3, 4, 5, 6, 15, 16]; ConnectorType = ConnectorType; - modbusCommandTypes = Object.values(ModbusCommandTypes) as ModbusCommandTypes[]; bACnetRequestTypes = Object.values(BACnetRequestTypes) as BACnetRequestTypes[]; bACnetObjectTypes = Object.values(BACnetObjectTypes) as BACnetObjectTypes[]; bLEMethods = Object.values(BLEMethods) as BLEMethods[]; @@ -98,7 +94,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C SocketMethodProcessingsTranslates = SocketMethodProcessingsTranslates; SNMPMethodsTranslations = SNMPMethodsTranslations; gatewayConnectorDefaultTypesTranslates = GatewayConnectorDefaultTypesTranslatesMap; - modbusCodesTranslate = ModbusCodesTranslate; urlPattern = /^[-a-zA-Zd_$:{}?~+=\/.0-9-]*$/; numbersOnlyPattern = /^[0-9]*$/; @@ -156,26 +151,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C withResponse: [false, []], }); break; - case ConnectorType.MODBUS: - formGroup = this.fb.group({ - tag: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], - type: [null, [Validators.required]], - functionCode: [null, [Validators.required]], - value: [null, []], - address: [null, [Validators.required, Validators.min(0), Validators.pattern(this.numbersOnlyPattern)]], - objectsCount: [null, [Validators.required, Validators.min(0), Validators.pattern(this.numbersOnlyPattern)]] - }) - const valueForm = formGroup.get('value'); - formGroup.get('functionCode').valueChanges.subscribe(value => { - if (value > 4) { - valueForm.addValidators([Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]); - } else { - valueForm.clearValidators(); - valueForm.setValue(null); - } - valueForm.updateValueAndValidity(); - }) - break; case ConnectorType.BACNET: formGroup = this.fb.group({ method: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html index 8368698087..41dea0b656 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html @@ -41,8 +41,32 @@ - + + +
+
{{ 'gateway.rpc.title' | translate: {type: gatewayConnectorDefaultTypesTranslates.get(connectorType)} }}
+ +
+ + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss index f74097bbba..b8424dc068 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss @@ -40,6 +40,10 @@ } } + .rpc-parameters { + width: 100%; + } + .result-block { padding: 0 5px; display: flex; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts index 1f3327ec06..d6b20d0137 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts @@ -22,6 +22,7 @@ import { ContentType } from '@shared/models/constants'; import { jsonRequired } from '@shared/components/json-object-edit.component'; import { ConnectorType, + GatewayConnectorDefaultTypesTranslatesMap, RPCCommand, RPCTemplate, RPCTemplateConfig, @@ -71,6 +72,9 @@ export class GatewayServiceRPCComponent implements OnInit { public connectorType: ConnectorType; public templates: Array = []; + readonly ConnectorType = ConnectorType; + readonly gatewayConnectorDefaultTypesTranslates = GatewayConnectorDefaultTypesTranslatesMap; + private subscription: IWidgetSubscription; private subscriptionOptions: WidgetSubscriptionOptions = { callbacks: { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts index 8df3994ea2..12df05c8f3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts @@ -18,7 +18,7 @@ import { ResourcesService } from '@core/services/resources.service'; import { Observable } from 'rxjs'; import { ValueTypeData } from '@shared/models/constants'; -export const noLeadTrailSpacesRegex = /^(?! )[\S\s]*(? | RequestMappingData[]; + requestsMapping: Record | RequestMappingData[]; broker: BrokerConfig; - workers: WorkersConfig; + workers?: WorkersConfig; } export interface OPCBasicConfig { @@ -311,35 +319,15 @@ export interface RPCCommand { time: number; } - -export enum ModbusCommandTypes { - Bits = 'bits', - Bit = 'bit', - // eslint-disable-next-line id-blacklist - String = 'string', - Bytes = 'bytes', - Int8 = '8int', - Uint8 = '8uint', - Int16 = '16int', - Uint16 = '16uint', - Float16 = '16float', - Int32 = '32int', - Uint32 = '32uint', - Float32 = '32float', - Int64 = '64int', - Uint64 = '64uint', - Float64 = '64float' -} - -export const ModbusCodesTranslate = new Map([ - [1, 'gateway.rpc.read-coils'], - [2, 'gateway.rpc.read-discrete-inputs'], - [3, 'gateway.rpc.read-multiple-holding-registers'], - [4, 'gateway.rpc.read-input-registers'], - [5, 'gateway.rpc.write-single-coil'], - [6, 'gateway.rpc.write-single-holding-register'], - [15, 'gateway.rpc.write-multiple-coils'], - [16, 'gateway.rpc.write-multiple-holding-registers'] +export const ModbusFunctionCodeTranslationsMap = new Map([ + [1, 'gateway.function-codes.read-coils'], + [2, 'gateway.function-codes.read-discrete-inputs'], + [3, 'gateway.function-codes.read-multiple-holding-registers'], + [4, 'gateway.function-codes.read-input-registers'], + [5, 'gateway.function-codes.write-single-coil'], + [6, 'gateway.function-codes.write-single-holding-register'], + [15, 'gateway.function-codes.write-multiple-coils'], + [16, 'gateway.function-codes.write-multiple-holding-registers'] ]); export enum BACnetRequestTypes { @@ -862,6 +850,8 @@ export enum ModbusDataType { FLOAT64 = '64float' } +export const ModbusEditableDataTypes = [ModbusDataType.BYTES, ModbusDataType.BITS, ModbusDataType.STRING]; + export enum ModbusObjectCountByDataType { '8int' = 1, '8uint' = 1, @@ -920,19 +910,6 @@ export const ModbusKeysNoKeysTextTranslationsMap = new Map( - [ - [1, 'gateway.read-coils'], - [2, 'gateway.read-discrete-inputs'], - [3, 'gateway.read-multiple-holding-registers'], - [4, 'gateway.read-input-registers'], - [5, 'gateway.write-coil'], - [6, 'gateway.write-register'], - [15, 'gateway.write-coils'], - [16, 'gateway.write-registers'], - ] -); - export interface ModbusMasterConfig { slaves: SlaveConfig[]; } diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index 00cabe0599..da12eba322 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -141,6 +141,9 @@ import { import { TypeValuePanelComponent } from '@home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component'; +import { + ModbusRpcParametersComponent +} from '@home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component'; import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component'; @NgModule({ @@ -229,6 +232,7 @@ import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/sc KeyValueIsNotEmptyPipe, ModbusBasicConfigComponent, EllipsisChipListDirective, + ModbusRpcParametersComponent, ], exports: [ EntitiesTableWidgetComponent, diff --git a/ui-ngx/src/app/shared/directives/public-api.ts b/ui-ngx/src/app/shared/directives/public-api.ts new file mode 100644 index 0000000000..6f25320d90 --- /dev/null +++ b/ui-ngx/src/app/shared/directives/public-api.ts @@ -0,0 +1,18 @@ +/// +/// Copyright © 2016-2024 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. +/// + +export * from './truncate-with-tooltip.directive'; +export * from './ellipsis-chip-list.directive'; diff --git a/ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts b/ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts index 1281b86040..ad4e4cbbc1 100644 --- a/ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts +++ b/ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts @@ -105,8 +105,6 @@ export class TruncateWithTooltipDirective implements OnInit, AfterViewInit, OnDe private showTooltip(): void { this.tooltip.message = this.text; - - this.renderer.setAttribute(this.elementRef.nativeElement, 'matTooltip', this.text); this.tooltip.show(); } diff --git a/ui-ngx/src/app/shared/models/audit-log.models.ts b/ui-ngx/src/app/shared/models/audit-log.models.ts index 70e8a0be6b..a399a458c2 100644 --- a/ui-ngx/src/app/shared/models/audit-log.models.ts +++ b/ui-ngx/src/app/shared/models/audit-log.models.ts @@ -48,6 +48,7 @@ export enum ActionType { ALARM_ACK = 'ALARM_ACK', ALARM_CLEAR = 'ALARM_CLEAR', ALARM_ASSIGNED = 'ALARM_ASSIGNED', + ALARM_DELETE = 'ALARM_DELETE', ALARM_UNASSIGNED = 'ALARM_UNASSIGNED', ADDED_COMMENT = 'ADDED_COMMENT', UPDATED_COMMENT = 'UPDATED_COMMENT', @@ -91,6 +92,7 @@ export const actionTypeTranslations = new Map( [ActionType.RELATIONS_DELETED, 'audit-log.type-relations-delete'], [ActionType.ALARM_ACK, 'audit-log.type-alarm-ack'], [ActionType.ALARM_CLEAR, 'audit-log.type-alarm-clear'], + [ActionType.ALARM_DELETE, 'audit-log.type-alarm-delete'], [ActionType.ALARM_ASSIGNED, 'audit-log.type-alarm-assign'], [ActionType.ALARM_UNASSIGNED, 'audit-log.type-alarm-unassign'], [ActionType.ADDED_COMMENT, 'audit-log.type-added-comment'], diff --git a/ui-ngx/src/app/shared/public-api.ts b/ui-ngx/src/app/shared/public-api.ts index bd3d565da3..1f38b7f70c 100644 --- a/ui-ngx/src/app/shared/public-api.ts +++ b/ui-ngx/src/app/shared/public-api.ts @@ -19,3 +19,4 @@ export * from './decorators/public-api'; export * from './models/public-api'; export * from './pipe/public-api'; export * from './shared.module'; +export * from './directives/public-api'; 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 c799473714..1933a0f8ea 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -936,6 +936,7 @@ "type-relations-delete": "All relation deleted", "type-alarm-ack": "Acknowledged", "type-alarm-clear": "Cleared", + "type-alarm-delete": "Deleted", "type-alarm-assign": "Assigned", "type-alarm-unassign": "Unassigned", "type-added-comment": "Added comment", @@ -2923,6 +2924,16 @@ "from-device-request-settings": "Input request parsing", "from-device-request-settings-hint": "These fields support JSONPath expressions to extract a name from incoming message.", "function-code": "Function code", + "function-codes": { + "read-coils": "01 - Read Coils", + "read-discrete-inputs": "02 - Read Discrete Inputs", + "read-multiple-holding-registers": "03 - Read Multiple Holding Registers", + "read-input-registers": "04 - Read Input Registers", + "write-single-coil": "05 - Write Single Coil", + "write-single-holding-register": "06 - Write Single Holding Register", + "write-multiple-coils": "15 - Write Multiple Coils", + "write-multiple-holding-registers": "16 - Write Multiple Holding Registers" + }, "to-device-response-settings": "Output request processing", "to-device-response-settings-hint": "For these fields you can use the following variables and they will be replaced with actual values: ${deviceName}, ${attributeKey}, ${attributeValue}", "gateway": "Gateway", @@ -3129,14 +3140,6 @@ "template-name-duplicate": "Template with such name already exists, it will be updated.", "command": "Command", "params": "Params", - "read-coils": "01: Read Coils", - "read-discrete-inputs": "02: Read Discrete Inputs", - "read-multiple-holding-registers": "03: Read Multiple Holding Registers", - "read-input-registers": "04: Read Input Registers", - "write-single-coil": "05: Write Single Coil", - "write-single-holding-register": "06: Write Single Holding Register", - "write-multiple-coils": "15: Write Multiple Coils", - "write-multiple-holding-registers": "16: Write Multiple Holding Registers", "json-value-invalid": "JSON value has an invalid format" }, "rpc-methods": "RPC methods", @@ -3326,16 +3329,13 @@ "username": "Username", "username-required": "Username is required.", "unit-id-required": "Unit ID is required.", - "read-coils": "Read Coils", - "read-discrete-inputs": "Read Discrete Inputs", - "read-multiple-holding-registers": "Read Multiple Holding Register", - "read-input-registers": "Read Input Registers", "write-coil": "Write Coil", "write-coils": "Write Coils", "write-register": "Write Register", "write-registers": "Write Registers", "hints": { - "modbus-server": "Starting with version 3.0, Gateway can run as a Modbus slave.", + "modbus-master": "Configuration sections for connecting to Modbus servers and reading data from them.", + "modbus-server": "Configuration section for the Modbus server, storing data and sending updates to the platform when changes occur or at fixed intervals.", "remote-configuration": "Enables remote configuration and management of the gateway", "remote-shell": "Enables remote control of the operating system with the gateway from the Remote Shell widget", "host": "Hostname or IP address of platform server",