Browse Source

Merge with master

pull/11391/head
Igor Kulikov 2 years ago
parent
commit
241f28f254
  1. 68
      application/src/main/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverter.java
  2. 2
      application/src/main/java/org/thingsboard/server/controller/EdgeController.java
  3. 29
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  4. 17
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  5. 19
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java
  6. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  7. 24
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/DeviceEdgeProcessor.java
  8. 15
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/rule/RuleChainEdgeProcessor.java
  9. 12
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java
  10. 17
      application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java
  11. 4
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
  12. 36
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbLocalSubscriptionService.java
  13. 5
      application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java
  14. 2
      application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java
  15. 5
      application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java
  16. 3
      application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java
  17. 3
      application/src/main/java/org/thingsboard/server/service/subscription/TbLocalSubscriptionService.java
  18. 14
      application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
  19. 4
      application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java
  20. 5
      application/src/main/resources/thingsboard.yml
  21. 66
      application/src/test/java/org/thingsboard/server/controller/EdgeControllerTest.java
  22. 10
      application/src/test/java/org/thingsboard/server/edge/AbstractEdgeTest.java
  23. 52
      application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java
  24. 4
      application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java
  25. 6
      application/src/test/java/org/thingsboard/server/edge/TelemetryEdgeTest.java
  26. 20
      application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java
  27. 6
      application/src/test/java/org/thingsboard/server/service/edge/rpc/processor/device/AbstractDeviceProcessorTest.java
  28. 33
      application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java
  29. 2
      common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java
  30. 3
      common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java
  31. 3
      common/message/src/main/java/org/thingsboard/server/common/msg/edge/FromEdgeSyncResponse.java
  32. 6
      common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimitsException.java
  33. 4
      common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java
  34. 1
      common/proto/src/main/proto/queue.proto
  35. 2
      common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java
  36. 277
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/CoapEfentoTransportResource.java
  37. 13
      common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/efento/utils/CoapEfentoUtils.java
  38. 90
      common/transport/coap/src/main/proto/efento/proto_config.proto
  39. 25
      common/transport/coap/src/main/proto/efento/proto_device_info.proto
  40. 227
      common/transport/coap/src/main/proto/efento/proto_measurement_types.proto
  41. 139
      common/transport/coap/src/main/proto/efento/proto_measurements.proto
  42. 20
      common/transport/coap/src/main/proto/efento/proto_rule.proto
  43. 144
      common/transport/coap/src/test/java/org/thingsboard/server/transport/coap/efento/CoapEfentTransportResourceTest.java
  44. 44
      common/util/src/main/java/org/thingsboard/common/util/DeduplicationUtil.java
  45. 13
      dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java
  46. 2
      monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java
  47. 3
      monitoring/src/main/java/org/thingsboard/monitoring/service/BaseMonitoringService.java
  48. 4
      pom.xml
  49. 2
      ui-ngx/src/app/core/services/mobile.service.ts
  50. 6
      ui-ngx/src/app/modules/common/modules-map.ts
  51. 38
      ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts
  52. 13
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/broker-config-control/broker-config-control.component.ts
  53. 20
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html
  54. 10
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.html
  55. 6
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.scss
  56. 40
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-basic-config/modbus-basic-config.component.ts
  57. 16
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.html
  58. 5
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.scss
  59. 28
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-data-keys-panel/modbus-data-keys-panel.component.ts
  60. 37
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.html
  61. 26
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-master-table/modbus-master-table.component.ts
  62. 92
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.html
  63. 166
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-rpc-parameters/modbus-rpc-parameters.component.ts
  64. 19
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.html
  65. 27
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss
  66. 41
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.ts
  67. 10
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.html
  68. 5
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.scss
  69. 10
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-dialog/modbus-slave-dialog.component.ts
  70. 7
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.scss
  71. 39
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts
  72. 31
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/opc-server-config/opc-server-config.component.ts
  73. 77
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-connectors.component.ts
  74. 40
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html
  75. 27
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts
  76. 28
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html
  77. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.scss
  78. 4
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.ts
  79. 69
      ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-widget.models.ts
  80. 4
      ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts
  81. 18
      ui-ngx/src/app/shared/directives/public-api.ts
  82. 2
      ui-ngx/src/app/shared/directives/truncate-with-tooltip.directive.ts
  83. 2
      ui-ngx/src/app/shared/models/audit-log.models.ts
  84. 1
      ui-ngx/src/app/shared/public-api.ts
  85. 26
      ui-ngx/src/assets/locale/locale.constant-en_US.json

68
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<MediaType> 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<MediaType> 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;
}
}

2
application/src/main/java/org/thingsboard/server/controller/EdgeController.java

@ -490,7 +490,7 @@ public class EdgeController extends BaseController {
if (fromEdgeSyncResponse.isSuccess()) { if (fromEdgeSyncResponse.isSuccess()) {
response.setResult(new ResponseEntity<>(HttpStatus.OK)); response.setResult(new ResponseEntity<>(HttpStatus.OK));
} else { } else {
response.setErrorResult(new ThingsboardException("Edge is not connected", ThingsboardErrorCode.GENERAL)); response.setErrorResult(new ThingsboardException(fromEdgeSyncResponse.getError(), ThingsboardErrorCode.GENERAL));
} }
} }

29
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) { private void startSyncProcess(TenantId tenantId, EdgeId edgeId, UUID requestId) {
EdgeGrpcSession session = sessions.get(edgeId); EdgeGrpcSession session = sessions.get(edgeId);
if (session != null) { if (session != null) {
boolean success = false; if (!session.isSyncCompleted()) {
if (session.isConnected()) { clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, false, "Sync process is active at the moment"));
session.startSyncProcess(true); } else {
success = true; 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 @Override
public void processSyncRequest(ToEdgeSyncRequest request, Consumer<FromEdgeSyncResponse> responseConsumer) { public void processSyncRequest(ToEdgeSyncRequest request, Consumer<FromEdgeSyncResponse> responseConsumer) {
log.trace("[{}][{}] Processing sync edge request [{}]", request.getTenantId(), request.getId(), request.getEdgeId());
UUID requestId = request.getId(); UUID requestId = request.getId();
localSyncEdgeRequests.put(requestId, responseConsumer); EdgeGrpcSession session = sessions.get(request.getEdgeId());
clusterService.pushEdgeSyncRequestToCore(request); if (session != null && !session.isSyncCompleted()) {
scheduleSyncRequestTimeout(request, requestId); 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) { private void scheduleSyncRequestTimeout(ToEdgeSyncRequest request, UUID requestId) {
@ -312,7 +321,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
Consumer<FromEdgeSyncResponse> consumer = localSyncEdgeRequests.remove(requestId); Consumer<FromEdgeSyncResponse> consumer = localSyncEdgeRequests.remove(requestId);
if (consumer != null) { if (consumer != null) {
log.trace("[{}] timeout for processing sync edge request.", requestId); 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); }, 20, TimeUnit.SECONDS);
} }

17
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -247,6 +247,7 @@ public final class EdgeGrpcSession implements Closeable {
} }
}, ctx.getGrpcCallbackExecutorService()); }, ctx.getGrpcCallbackExecutorService());
} else { } else {
log.info("[{}][{}] sync process completed", this.tenantId, edge.getId());
DownlinkMsg syncCompleteDownlinkMsg = DownlinkMsg.newBuilder() DownlinkMsg syncCompleteDownlinkMsg = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.setSyncCompletedMsg(SyncCompletedMsg.newBuilder().build()) .setSyncCompletedMsg(SyncCompletedMsg.newBuilder().build())
@ -325,7 +326,11 @@ public final class EdgeGrpcSession implements Closeable {
} }
private void sendDownlinkMsg(ResponseMsg downlinkMsg) { 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()) { if (isConnected()) {
downlinkMsgLock.lock(); downlinkMsgLock.lock();
try { try {
@ -337,7 +342,7 @@ public final class EdgeGrpcSession implements Closeable {
} finally { } finally {
downlinkMsgLock.unlock(); 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; DownlinkMsg downlinkMsg = null;
try { try {
switch (edgeEvent.getAction()) { 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); 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 -> case ATTRIBUTES_UPDATED, POST_ATTRIBUTES, ATTRIBUTES_DELETED, TIMESERIES_UPDATED ->
downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edge, edgeEvent); downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edge, edgeEvent);

19
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java

@ -15,6 +15,7 @@
*/ */
package org.thingsboard.server.service.edge.rpc; package org.thingsboard.server.service.edge.rpc;
import lombok.Getter;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
@ -53,6 +54,7 @@ public class EdgeSyncCursor {
private final List<EdgeEventFetcher> fetchers = new LinkedList<>(); private final List<EdgeEventFetcher> fetchers = new LinkedList<>();
@Getter
private int currentIdx = 0; private int currentIdx = 0;
public EdgeSyncCursor(EdgeContextComponent ctx, Edge edge, boolean fullSync) { public EdgeSyncCursor(EdgeContextComponent ctx, Edge edge, boolean fullSync) {
@ -62,12 +64,12 @@ public class EdgeSyncCursor {
fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService())); fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService()));
fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService())); fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService()));
fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService())); fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService()));
Customer publicCustomer = ctx.getCustomerService().findOrCreatePublicCustomer(edge.getTenantId()); }
fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId())); Customer publicCustomer = ctx.getCustomerService().findOrCreatePublicCustomer(edge.getTenantId());
if (edge.getCustomerId() != null && !EntityId.NULL_UUID.equals(edge.getCustomerId().getId())) { fetchers.add(new CustomerEdgeEventFetcher(publicCustomer.getId()));
fetchers.add(new CustomerEdgeEventFetcher(edge.getCustomerId())); if (edge.getCustomerId() != null && !EntityId.NULL_UUID.equals(edge.getCustomerId().getId())) {
fetchers.add(new CustomerUsersEdgeEventFetcher(ctx.getUserService(), edge.getCustomerId())); fetchers.add(new CustomerEdgeEventFetcher(edge.getCustomerId()));
} fetchers.add(new CustomerUsersEdgeEventFetcher(ctx.getUserService(), edge.getCustomerId()));
} }
fetchers.add(new DashboardsEdgeEventFetcher(ctx.getDashboardService())); fetchers.add(new DashboardsEdgeEventFetcher(ctx.getDashboardService()));
fetchers.add(new DefaultProfilesEdgeEventFetcher(ctx.getDeviceProfileService(), ctx.getAssetProfileService())); fetchers.add(new DefaultProfilesEdgeEventFetcher(ctx.getDeviceProfileService(), ctx.getAssetProfileService()));
@ -102,9 +104,4 @@ public class EdgeSyncCursor {
currentIdx++; currentIdx++;
return edgeEventFetcher; return edgeEventFetcher;
} }
public int getCurrentIdx() {
return currentIdx;
}
} }

2
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, private boolean doSaveIfEdgeIsOffline(EdgeEventType type,
EdgeEventActionType action) { EdgeEventActionType action) {
return switch (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; true;
default -> switch (type) { default -> switch (type) {
case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE, WIDGETS_BUNDLE, WIDGET_TYPE, case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE, WIDGETS_BUNDLE, WIDGET_TYPE,

24
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.FromDeviceRpcResponse;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponseActorMsg; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponseActorMsg;
import org.thingsboard.server.dao.exception.DataValidationException; 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.DeviceCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg; import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg;
import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; 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_CREATED_RPC_MESSAGE:
case ENTITY_UPDATED_RPC_MESSAGE: case ENTITY_UPDATED_RPC_MESSAGE:
saveOrUpdateDevice(tenantId, deviceId, deviceUpdateMsg, edge); 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: case ENTITY_DELETED_RPC_MESSAGE:
Device deviceToDelete = deviceService.findDeviceById(tenantId, deviceId); Device deviceToDelete = deviceService.findDeviceById(tenantId, deviceId);
if (deviceToDelete != null) { if (deviceToDelete != null) {
@ -232,6 +231,12 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements
DownlinkMsg.Builder builder = DownlinkMsg.newBuilder() DownlinkMsg.Builder builder = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addDeviceUpdateMsg(deviceUpdateMsg); .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)) { if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) {
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId()); DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId());
deviceProfile = checkIfDeviceProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, deviceProfile, edgeVersion); deviceProfile = checkIfDeviceProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, deviceProfile, edgeVersion);
@ -269,22 +274,7 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements
deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion))
.constructDeviceRpcCallMsg(edgeEvent.getEntityId(), edgeEvent.getBody())) .constructDeviceRpcCallMsg(edgeEvent.getEntityId(), edgeEvent.getBody()))
.build(); .build();
case CREDENTIALS_REQUEST:
return convertCredentialsRequestEventToDownlink(edgeEvent);
} }
return downlinkMsg; 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();
}
} }

15
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) RuleChainUpdateMsg ruleChainUpdateMsg = ((RuleChainMsgConstructor)
ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)) ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion))
.constructRuleChainUpdatedMsg(msgType, ruleChain, isRoot); .constructRuleChainUpdatedMsg(msgType, ruleChain, isRoot);
downlinkMsg = DownlinkMsg.newBuilder()
DownlinkMsg.Builder builder = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addRuleChainUpdateMsg(ruleChainUpdateMsg) .addRuleChainUpdateMsg(ruleChainUpdateMsg);
.build();
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() case DELETED, UNASSIGNED_FROM_EDGE -> downlinkMsg = DownlinkMsg.newBuilder()

12
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); User user = userService.findUserById(edgeEvent.getTenantId(), userId);
if (user != null) { if (user != null) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction()); UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
downlinkMsg = DownlinkMsg.newBuilder() DownlinkMsg.Builder builder = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addUserUpdateMsg(((UserMsgConstructor) userMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructUserUpdatedMsg(msgType, user)) .addUserUpdateMsg(((UserMsgConstructor) userMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructUserUpdatedMsg(msgType, user));
.build(); 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() case DELETED -> downlinkMsg = DownlinkMsg.newBuilder()

17
application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java

@ -82,8 +82,6 @@ import java.util.UUID;
@Slf4j @Slf4j
public class DefaultEdgeRequestsService implements EdgeRequestsService { public class DefaultEdgeRequestsService implements EdgeRequestsService {
private static final int DEFAULT_PAGE_SIZE = 1000;
@Autowired @Autowired
private EdgeEventService edgeEventService; private EdgeEventService edgeEventService;
@ -142,8 +140,10 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
private ListenableFuture<Void> processEntityAttributesAndAddToEdgeQueue(TenantId tenantId, EntityId entityId, Edge edge, private ListenableFuture<Void> processEntityAttributesAndAddToEdgeQueue(TenantId tenantId, EntityId entityId, Edge edge,
EdgeEventType entityType, String scope, List<AttributeKvEntry> ssAttributes, EdgeEventType entityType, String scope, List<AttributeKvEntry> ssAttributes,
AttributesRequestMsg attributesRequestMsg) { AttributesRequestMsg attributesRequestMsg) {
Map<String, Object> entityData = null;
ObjectNode attributes = null;
ListenableFuture<Void> future;
try { try {
ListenableFuture<Void> future;
if (ssAttributes == null || ssAttributes.isEmpty()) { if (ssAttributes == null || ssAttributes.isEmpty()) {
log.trace("[{}][{}] No attributes found for entity {} [{}]", tenantId, log.trace("[{}][{}] No attributes found for entity {} [{}]", tenantId,
edge.getName(), edge.getName(),
@ -151,8 +151,8 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
entityId.getId()); entityId.getId());
future = Futures.immediateFuture(null); future = Futures.immediateFuture(null);
} else { } else {
Map<String, Object> entityData = new HashMap<>(); entityData = new HashMap<>();
ObjectNode attributes = JacksonUtil.newObjectNode(); attributes = JacksonUtil.newObjectNode();
for (AttributeKvEntry attr : ssAttributes) { for (AttributeKvEntry attr : ssAttributes) {
if (DefaultDeviceStateService.PERSISTENT_ATTRIBUTES.contains(attr.getKey()) if (DefaultDeviceStateService.PERSISTENT_ATTRIBUTES.contains(attr.getKey())
&& !DefaultDeviceStateService.INACTIVITY_TIMEOUT.equals(attr.getKey())) { && !DefaultDeviceStateService.INACTIVITY_TIMEOUT.equals(attr.getKey())) {
@ -170,7 +170,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
attributes.put(attr.getKey(), attr.getValueAsString()); attributes.put(attr.getKey(), attr.getValueAsString());
} }
} }
if (attributes.size() > 0) { if (!attributes.isEmpty()) {
entityData.put("kv", attributes); entityData.put("kv", attributes);
entityData.put("scope", scope); entityData.put("scope", scope);
JsonNode body = JacksonUtil.valueToTree(entityData); 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); return Futures.transformAsync(future, v -> processLatestTimeseriesAndAddToEdgeQueue(tenantId, entityId, edge, entityType), dbCallbackExecutorService);
} catch (Exception e) { } 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); log.error(errMsg, e);
return Futures.immediateFailedFuture(new RuntimeException(errMsg, e)); return Futures.immediateFailedFuture(new RuntimeException(errMsg, e));
} }
} }
private ListenableFuture<Void> processLatestTimeseriesAndAddToEdgeQueue(TenantId tenantId, EntityId entityId, Edge edge, private ListenableFuture<Void> processLatestTimeseriesAndAddToEdgeQueue(TenantId tenantId, EntityId entityId, Edge edge,
EdgeEventType entityType) { EdgeEventType entityType) {
ListenableFuture<List<TsKvEntry>> getAllLatestFuture = timeseriesService.findAllLatest(tenantId, entityId); ListenableFuture<List<TsKvEntry>> getAllLatestFuture = timeseriesService.findAllLatest(tenantId, entityId);

4
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.EntityKey;
import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.EntityKeyType;
import org.thingsboard.server.common.data.query.TsValue; 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.alarm.AlarmService;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.entity.EntityService; 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) { private void handleWsCmdRuntimeException(String sessionId, RuntimeException e, EntityDataCmd cmd) {
log.debug("[{}] Failed to process ws cmd: {}", sessionId, cmd, e); log.debug("[{}] Failed to process ws cmd: {}", sessionId, cmd, e);
if (e instanceof TbRateLimitsException) {
return;
}
wsService.close(sessionId, CloseStatus.SERVICE_RESTARTED); wsService.close(sessionId, CloseStatus.SERVICE_RESTARTED);
} }

36
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.PostConstruct;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener; import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.common.util.DeduplicationUtil;
import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.EntityType; 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.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry; 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.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; 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.attributes.AttributesService;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.gen.transport.TransportProtos; 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.discovery.event.ClusterTopologyChangeEvent;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.ws.WebSocketService; 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.NotificationRequestUpdate;
import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate; import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate; import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate;
@ -88,13 +94,20 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
private final TbClusterService clusterService; private final TbClusterService clusterService;
private final SubscriptionManagerService subscriptionManagerService; private final SubscriptionManagerService subscriptionManagerService;
private final WebSocketService webSocketService; private final WebSocketService webSocketService;
private final RateLimitService rateLimitService;
private ExecutorService tsCallBackExecutor; private ExecutorService tsCallBackExecutor;
private ScheduledExecutorService staleSessionCleanupExecutor; 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, public DefaultTbLocalSubscriptionService(AttributesService attrService, TimeseriesService tsService, TbServiceInfoProvider serviceInfoProvider,
PartitionService partitionService, TbClusterService clusterService, PartitionService partitionService, TbClusterService clusterService,
@Lazy SubscriptionManagerService subscriptionManagerService, @Lazy WebSocketService webSocketService) { @Lazy SubscriptionManagerService subscriptionManagerService, @Lazy WebSocketService webSocketService,
RateLimitService rateLimitService) {
this.attrService = attrService; this.attrService = attrService;
this.tsService = tsService; this.tsService = tsService;
this.serviceInfoProvider = serviceInfoProvider; this.serviceInfoProvider = serviceInfoProvider;
@ -102,6 +115,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
this.clusterService = clusterService; this.clusterService = clusterService;
this.subscriptionManagerService = subscriptionManagerService; this.subscriptionManagerService = subscriptionManagerService;
this.webSocketService = webSocketService; this.webSocketService = webSocketService;
this.rateLimitService = rateLimitService;
} }
private String serviceId; private String serviceId;
@ -185,9 +199,18 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
} }
@Override @Override
public void addSubscription(TbSubscription<?> subscription) { public void addSubscription(TbSubscription<?> subscription, WebSocketSessionRef sessionRef) {
TenantId tenantId = subscription.getTenantId(); TenantId tenantId = subscription.getTenantId();
EntityId entityId = subscription.getEntityId(); 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); log.debug("[{}][{}] Register subscription: {}", tenantId, entityId, subscription);
SubscriptionModificationResult result; SubscriptionModificationResult result;
subsLock.lock(); subsLock.lock();
@ -584,4 +607,13 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
subscriptionsBySessionId.keySet().forEach(webSocketService::cleanupIfStale); 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);
}
} }

5
application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java

@ -132,7 +132,7 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
int subIdx = sessionRef.getSessionSubIdSeq().incrementAndGet(); int subIdx = sessionRef.getSessionSubIdSeq().incrementAndGet();
subToEntityIdMap.put(subIdx, entityData.getEntityId()); subToEntityIdMap.put(subIdx, entityData.getEntityId());
localSubscriptionService.addSubscription( localSubscriptionService.addSubscription(
createTsSub(entityData, subIdx, false, startTs, endTs, keyStates, resultToLatestValues)); createTsSub(entityData, subIdx, false, startTs, endTs, keyStates, resultToLatestValues), sessionRef);
}); });
} }
@ -140,7 +140,7 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
Map<EntityKeyType, List<EntityKey>> keysByType = getEntityKeyByTypeMap(keys); Map<EntityKeyType, List<EntityKey>> keysByType = getEntityKeyByTypeMap(keys);
for (EntityData entityData : data.getData()) { for (EntityData entityData : data.getData()) {
List<TbSubscription> entitySubscriptions = addSubscriptions(entityData, keysByType, latestValues, startTs, endTs); List<TbSubscription> 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<T extends AbstractDataQuery<? extends
abstract void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate, EntityKeyType keyType, boolean resultToLatestValues); abstract void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate, EntityKeyType keyType, boolean resultToLatestValues);
protected abstract Aggregation getCurrentAggregation(); protected abstract Aggregation getCurrentAggregation();
} }

2
application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractSubCtx.java

@ -152,7 +152,7 @@ public abstract class TbAbstractSubCtx<T extends EntityCountQuery> {
.scope(TbAttributeSubscriptionScope.SERVER_SCOPE) .scope(TbAttributeSubscriptionScope.SERVER_SCOPE)
.build(); .build();
subToDynamicValueKeySet.add(subIdx); subToDynamicValueKeySet.add(subIdx);
localSubscriptionService.addSubscription(sub); localSubscriptionService.addSubscription(sub, sessionRef);
} }
} catch (InterruptedException | ExecutionException e) { } catch (InterruptedException | ExecutionException e) {
log.info("[{}][{}][{}] Failed to resolve dynamic values: {}", tenantId, customerId, userId, dynamicValues.keySet()); log.info("[{}][{}][{}] Failed to resolve dynamic values: {}", tenantId, customerId, userId, dynamicValues.keySet());

5
application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java

@ -177,7 +177,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx<AlarmDataQuery> {
.updateProcessor((sub, update) -> sendWsMsg(sub.getSessionId(), update)) .updateProcessor((sub, update) -> sendWsMsg(sub.getSessionId(), update))
.ts(startTs) .ts(startTs)
.build(); .build();
localSubscriptionService.addSubscription(subscription); localSubscriptionService.addSubscription(subscription, sessionRef);
} }
@Override @Override
@ -342,7 +342,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx<AlarmDataQuery> {
newSubsList.forEach(entity -> createAlarmSubscriptionForEntity(query.getPageLink(), startTs, entity)); newSubsList.forEach(entity -> createAlarmSubscriptionForEntity(query.getPageLink(), startTs, entity));
} }
subIdsToCancel.forEach(subId -> localSubscriptionService.cancelSubscription(getSessionId(), subId)); subIdsToCancel.forEach(subId -> localSubscriptionService.cancelSubscription(getSessionId(), subId));
subsToAdd.forEach(localSubscriptionService::addSubscription); subsToAdd.forEach(subscription -> localSubscriptionService.addSubscription(subscription, sessionRef));
} }
private void resetInvocationCounter() { private void resetInvocationCounter() {
@ -361,4 +361,5 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx<AlarmDataQuery> {
EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder); EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder);
return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters()); return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters());
} }
} }

3
application/src/main/java/org/thingsboard/server/service/subscription/TbEntityDataSubCtx.java

@ -226,7 +226,7 @@ public class TbEntityDataSubCtx extends TbAbstractDataSubCtx<EntityDataQuery> {
} }
} }
subIdsToCancel.forEach(subId -> localSubscriptionService.cancelSubscription(getSessionId(), subId)); 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)); sendWsMsg(new EntityDataUpdate(cmdId, data, null, maxEntitiesPerDataSubscription));
} }
@ -239,4 +239,5 @@ public class TbEntityDataSubCtx extends TbAbstractDataSubCtx<EntityDataQuery> {
protected EntityDataQuery buildEntityDataQuery() { protected EntityDataQuery buildEntityDataQuery() {
return query; return query;
} }
} }

3
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.common.msg.queue.TbCallback;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.discovery.event.ClusterTopologyChangeEvent; 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.NotificationRequestUpdate;
import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate; import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate;
@ -29,7 +30,7 @@ import java.util.List;
public interface TbLocalSubscriptionService { public interface TbLocalSubscriptionService {
void addSubscription(TbSubscription<?> subscription); void addSubscription(TbSubscription<?> subscription, WebSocketSessionRef sessionRef);
void onSubEventCallback(TransportProtos.TbEntitySubEventCallbackProto subEventCallback, TbCallback callback); void onSubEventCallback(TransportProtos.TbEntitySubEventCallbackProto subEventCallback, TbCallback callback);

14
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.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; 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.attributes.AttributesService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
@ -223,9 +224,10 @@ public class DefaultWebSocketService implements WebSocketService {
try { try {
Optional.ofNullable(cmdsHandlers.get(cmd.getType())) Optional.ofNullable(cmdsHandlers.get(cmd.getType()))
.ifPresent(cmdHandler -> cmdHandler.handle(sessionRef, cmd)); .ifPresent(cmdHandler -> cmdHandler.handle(sessionRef, cmd));
} catch (TbRateLimitsException e) {
log.debug("{} Failed to handle WS cmd: {}", sessionRef, cmd, e);
} catch (Exception e) { } catch (Exception e) {
log.error("[sessionId: {}, tenantId: {}, userId: {}] Failed to handle WS cmd: {}", sessionId, log.error("{} Failed to handle WS cmd: {}", sessionRef, cmd, e);
sessionRef.getSecurityCtx().getTenantId(), sessionRef.getSecurityCtx().getId(), cmd, e);
} }
} }
} }
@ -467,7 +469,7 @@ public class DefaultWebSocketService implements WebSocketService {
subLock.lock(); subLock.lock();
try { try {
oldSubService.addSubscription(sub); oldSubService.addSubscription(sub, sessionRef);
sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData)); sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData));
} finally { } finally {
subLock.unlock(); subLock.unlock();
@ -580,7 +582,7 @@ public class DefaultWebSocketService implements WebSocketService {
subLock.lock(); subLock.lock();
try { try {
oldSubService.addSubscription(sub); oldSubService.addSubscription(sub, sessionRef);
sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData)); sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData));
} finally { } finally {
subLock.unlock(); subLock.unlock();
@ -677,7 +679,7 @@ public class DefaultWebSocketService implements WebSocketService {
subLock.lock(); subLock.lock();
try { try {
oldSubService.addSubscription(sub); oldSubService.addSubscription(sub, sessionRef);
sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data)); sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data));
} finally { } finally {
subLock.unlock(); subLock.unlock();
@ -732,7 +734,7 @@ public class DefaultWebSocketService implements WebSocketService {
subLock.lock(); subLock.lock();
try { try {
oldSubService.addSubscription(sub); oldSubService.addSubscription(sub, sessionRef);
sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data)); sendUpdate(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data));
} finally { } finally {
subLock.unlock(); subLock.unlock();

4
application/src/main/java/org/thingsboard/server/service/ws/notification/DefaultNotificationCommandsHandler.java

@ -81,7 +81,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
.limit(cmd.getLimit()) .limit(cmd.getLimit())
.notificationTypes(cmd.getTypes()) .notificationTypes(cmd.getTypes())
.build(); .build();
localSubscriptionService.addSubscription(subscription); localSubscriptionService.addSubscription(subscription, sessionRef);
fetchUnreadNotifications(subscription); fetchUnreadNotifications(subscription);
sendUpdate(sessionRef.getSessionId(), subscription.createFullUpdate()); sendUpdate(sessionRef.getSessionId(), subscription.createFullUpdate());
@ -99,7 +99,7 @@ public class DefaultNotificationCommandsHandler implements NotificationCommandsH
.entityId(securityCtx.getId()) .entityId(securityCtx.getId())
.updateProcessor(this::handleNotificationsCountSubscriptionUpdate) .updateProcessor(this::handleNotificationsCountSubscriptionUpdate)
.build(); .build();
localSubscriptionService.addSubscription(subscription); localSubscriptionService.addSubscription(subscription, sessionRef);
fetchUnreadNotificationsCount(subscription); fetchUnreadNotificationsCount(subscription);
sendUpdate(sessionRef.getSessionId(), subscription.createUpdate()); sendUpdate(sessionRef.getSessionId(), subscription.createUpdate());

5
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}" max_queue_messages_per_session: "${TB_SERVER_WS_DEFAULT_QUEUE_MESSAGES_PER_SESSION:1000}"
# Maximum time between WS session opening and sending auth command # Maximum time between WS session opening and sending auth command
auth_timeout_ms: "${TB_SERVER_WS_AUTH_TIMEOUT_MS:10000}" 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: rest:
server_side_rpc: server_side_rpc:
# Minimum value of the server-side RPC timeout. May override value provided in the REST API call. # Minimum value of the server-side RPC timeout. May override value provided in the REST API call.

66
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.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.CustomerId; 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.EdgeId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.data.rule.RuleChain; 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.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.model.JwtSettings; import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.dao.edge.EdgeDao; import org.thingsboard.server.dao.edge.EdgeDao;
import org.thingsboard.server.dao.exception.DataValidationException; 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.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; 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.DeviceProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg; import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg;
import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg; 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.RuleChainUpdateMsg;
import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg; import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg;
import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg; import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg;
@ -93,6 +99,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
@ -887,23 +894,24 @@ public class EdgeControllerTest extends AbstractControllerTest {
edgeImitator.ignoreType(UserCredentialsUpdateMsg.class); edgeImitator.ignoreType(UserCredentialsUpdateMsg.class);
edgeImitator.ignoreType(OAuth2UpdateMsg.class); edgeImitator.ignoreType(OAuth2UpdateMsg.class);
edgeImitator.expectMessageAmount(24); edgeImitator.expectMessageAmount(27);
edgeImitator.connect(); edgeImitator.connect();
waitForMessages(edgeImitator); waitForMessages(edgeImitator);
verifyFetchersMsgs(edgeImitator); verifyFetchersMsgs(edgeImitator, savedDevice);
// verify queue msgs // verify queue msgs
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); 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(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(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(popAssetMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Asset 1"));
Assert.assertTrue(edgeImitator.getDownlinkMsgs().isEmpty()); Assert.assertTrue(edgeImitator.getDownlinkMsgs().isEmpty());
edgeImitator.expectMessageAmount(20); edgeImitator.expectMessageAmount(22);
doPost("/api/edge/sync/" + edge.getId()); doPost("/api/edge/sync/" + edge.getId());
waitForMessages(edgeImitator); waitForMessages(edgeImitator);
verifyFetchersMsgs(edgeImitator); verifyFetchersMsgs(edgeImitator, savedDevice);
Assert.assertTrue(edgeImitator.getDownlinkMsgs().isEmpty()); Assert.assertTrue(edgeImitator.getDownlinkMsgs().isEmpty());
edgeImitator.allowIgnoredTypes(); edgeImitator.allowIgnoredTypes();
@ -920,6 +928,23 @@ public class EdgeControllerTest extends AbstractControllerTest {
.andExpect(status().isOk()); .andExpect(status().isOk());
} }
private RuleChainId getEdgeRootRuleChainId(EdgeImitator edgeImitator) {
try {
EdgeId edgeId = new EdgeId(new UUID(edgeImitator.getConfiguration().getEdgeIdMSB(), edgeImitator.getConfiguration().getEdgeIdLSB()));
List<RuleChain> edgeRuleChains = doGetTypedWithPageLink("/api/edge/" + edgeId.getId() + "/ruleChains?",
new TypeReference<PageData<RuleChain>>() {
}, 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 { private void simulateEdgeActivation(Edge edge) throws Exception {
ObjectNode attributes = JacksonUtil.newObjectNode(); ObjectNode attributes = JacksonUtil.newObjectNode();
attributes.put("active", true); 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(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(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(), "general"));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "mail")); Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "mail"));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "connectivity")); 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(popCustomerMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Public"));
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default")); 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(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(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(popAssetMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Asset 1"));
Assert.assertTrue(popTenantMsg(edgeImitator.getDownlinkMsgs(), tenantId)); Assert.assertTrue(popTenantMsg(edgeImitator.getDownlinkMsgs(), tenantId));
@ -1002,6 +1029,21 @@ public class EdgeControllerTest extends AbstractControllerTest {
return false; return false;
} }
private boolean popRuleChainMetadataMsg(List<AbstractMessage> 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<AbstractMessage> messages, String key) { private boolean popAdminSettingsMsg(List<AbstractMessage> messages, String key) {
for (AbstractMessage message : messages) { for (AbstractMessage message : messages) {
if (message instanceof AdminSettingsUpdateMsg adminSettingsUpdateMsg) { if (message instanceof AdminSettingsUpdateMsg adminSettingsUpdateMsg) {
@ -1046,6 +1088,20 @@ public class EdgeControllerTest extends AbstractControllerTest {
return false; return false;
} }
private boolean popDeviceCredentialsMsg(List<AbstractMessage> 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<AbstractMessage> messages, UpdateMsgType msgType, String name) { private boolean popAssetProfileMsg(List<AbstractMessage> messages, UpdateMsgType msgType, String name) {
for (AbstractMessage message : messages) { for (AbstractMessage message : messages) {
if (message instanceof AssetProfileUpdateMsg assetProfileUpdateMsg) { if (message instanceof AssetProfileUpdateMsg assetProfileUpdateMsg) {

10
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.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg; import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg; 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.DeviceProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg; import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.EdgeConfiguration; 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.TenantUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg; 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 org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
import java.util.ArrayList; import java.util.ArrayList;
@ -143,7 +145,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret()); edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret());
edgeImitator.ignoreType(OAuth2UpdateMsg.class); edgeImitator.ignoreType(OAuth2UpdateMsg.class);
edgeImitator.expectMessageAmount(21); edgeImitator.expectMessageAmount(24);
edgeImitator.connect(); edgeImitator.connect();
requestEdgeRuleChainMetadata(); requestEdgeRuleChainMetadata();
@ -250,7 +252,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
UUID ruleChainUUID = validateRuleChains(); UUID ruleChainUUID = validateRuleChains();
// 1 from request message // 1 from request message
validateMsgsCnt(RuleChainMetadataUpdateMsg.class, 1); validateMsgsCnt(RuleChainMetadataUpdateMsg.class, 2);
validateRuleChainMetadataUpdates(ruleChainUUID); validateRuleChainMetadataUpdates(ruleChainUUID);
// 4 messages ('general', 'mail', 'connectivity', 'jwt') // 4 messages ('general', 'mail', 'connectivity', 'jwt')
@ -275,6 +277,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
validateMsgsCnt(DeviceUpdateMsg.class, 1); validateMsgsCnt(DeviceUpdateMsg.class, 1);
validateDevices(); validateDevices();
validateMsgsCnt(DeviceCredentialsUpdateMsg.class, 1);
// 1 from asset fetcher // 1 from asset fetcher
validateMsgsCnt(AssetUpdateMsg.class, 1); validateMsgsCnt(AssetUpdateMsg.class, 1);
validateAssets(); validateAssets();
@ -287,6 +291,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
validateMsgsCnt(UserUpdateMsg.class, 1); validateMsgsCnt(UserUpdateMsg.class, 1);
validateUsers(); validateUsers();
validateMsgsCnt(UserCredentialsUpdateMsg.class, 1);
// 1 from tenant fetcher // 1 from tenant fetcher
validateMsgsCnt(TenantUpdateMsg.class, 1); validateMsgsCnt(TenantUpdateMsg.class, 1);
validateTenant(); validateTenant();

52
application/src/test/java/org/thingsboard/server/edge/DeviceEdgeTest.java

@ -150,25 +150,25 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
+ "/edge/" + edge.getUuidId(), Edge.class); + "/edge/" + edge.getUuidId(), Edge.class);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(2);
doPost("/api/customer/" + savedCustomer.getUuidId() doPost("/api/customer/" + savedCustomer.getUuidId()
+ "/device/" + savedDevice.getUuidId(), Device.class); + "/device/" + savedDevice.getUuidId(), Device.class);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage(); deviceUpdateMsgOpt = edgeImitator.findMessageByType(DeviceUpdateMsg.class);
Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); Assert.assertTrue(deviceUpdateMsgOpt.isPresent());
deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; deviceUpdateMsg = deviceUpdateMsgOpt.get();
deviceFromMsg = JacksonUtil.fromString(deviceUpdateMsg.getEntity(), Device.class, true); deviceFromMsg = JacksonUtil.fromString(deviceUpdateMsg.getEntity(), Device.class, true);
Assert.assertNotNull(deviceFromMsg); Assert.assertNotNull(deviceFromMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType());
Assert.assertEquals(savedCustomer.getId(), deviceFromMsg.getCustomerId()); Assert.assertEquals(savedCustomer.getId(), deviceFromMsg.getCustomerId());
// unassign device #2 from customer // unassign device #2 from customer
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(2);
doDelete("/api/customer/device/" + savedDevice.getUuidId(), Device.class); doDelete("/api/customer/device/" + savedDevice.getUuidId(), Device.class);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage(); deviceUpdateMsgOpt = edgeImitator.findMessageByType(DeviceUpdateMsg.class);
Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); Assert.assertTrue(deviceUpdateMsgOpt.isPresent());
deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; deviceUpdateMsg = deviceUpdateMsgOpt.get();
deviceFromMsg = JacksonUtil.fromString(deviceUpdateMsg.getEntity(), Device.class, true); deviceFromMsg = JacksonUtil.fromString(deviceUpdateMsg.getEntity(), Device.class, true);
Assert.assertNotNull(deviceFromMsg); Assert.assertNotNull(deviceFromMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType());
@ -243,7 +243,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
// update device // update device
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(2);
savedDevice.setFirmwareId(firmwareOtaPackageInfo.getId()); savedDevice.setFirmwareId(firmwareOtaPackageInfo.getId());
savedDevice.setSoftwareId(softwareOtaPackageInfo.getId()); savedDevice.setSoftwareId(softwareOtaPackageInfo.getId());
@ -256,9 +256,9 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
savedDevice = doPost("/api/device", savedDevice, Device.class); savedDevice = doPost("/api/device", savedDevice, Device.class);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Optional<DeviceUpdateMsg> deviceUpdateMsgOpt = edgeImitator.findMessageByType(DeviceUpdateMsg.class);
Assert.assertTrue(latestMessage instanceof DeviceUpdateMsg); Assert.assertTrue(deviceUpdateMsgOpt.isPresent());
DeviceUpdateMsg deviceUpdateMsg = (DeviceUpdateMsg) latestMessage; DeviceUpdateMsg deviceUpdateMsg = deviceUpdateMsgOpt.get();
Device deviceMsg = JacksonUtil.fromString(deviceUpdateMsg.getEntity(), Device.class, true); Device deviceMsg = JacksonUtil.fromString(deviceUpdateMsg.getEntity(), Device.class, true);
Assert.assertNotNull(deviceMsg); Assert.assertNotNull(deviceMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType()); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, deviceUpdateMsg.getMsgType());
@ -504,7 +504,7 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build()); uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build());
edgeImitator.expectResponsesAmount(1); edgeImitator.expectResponsesAmount(1);
edgeImitator.expectMessageAmount(2); edgeImitator.expectMessageAmount(1);
testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
@ -526,18 +526,6 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
Device device = doGet("/api/device/" + newDeviceId, Device.class); Device device = doGet("/api/device/" + newDeviceId, Device.class);
Assert.assertNotNull(device); Assert.assertNotNull(device);
Assert.assertNotEquals(deviceOnCloudName, device.getName()); Assert.assertNotEquals(deviceOnCloudName, device.getName());
Optional<DeviceCredentialsRequestMsg> 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 @Test
@ -553,22 +541,10 @@ public class DeviceEdgeTest extends AbstractEdgeTest {
uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build()); uplinkMsgBuilder.addDeviceUpdateMsg(deviceUpdateMsgBuilder.build());
edgeImitator.expectResponsesAmount(1); edgeImitator.expectResponsesAmount(1);
edgeImitator.expectMessageAmount(1);
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses()); Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Device device = doGet("/api/device/" + deviceMsg.getId().getId(), Device.class);
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);
Assert.assertNotNull(device); Assert.assertNotNull(device);
Assert.assertEquals("Edge Device 2", device.getName()); Assert.assertEquals("Edge Device 2", device.getName());
} }

4
application/src/test/java/org/thingsboard/server/edge/RuleChainEdgeTest.java

@ -193,7 +193,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
ruleChain.setType(RuleChainType.EDGE); ruleChain.setType(RuleChainType.EDGE);
RuleChain savedRuleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class); RuleChain savedRuleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class);
edgeImitator.expectMessageAmount(2); edgeImitator.expectMessageAmount(4);
doPost("/api/edge/" + edge.getUuidId() doPost("/api/edge/" + edge.getUuidId()
+ "/ruleChain/" + savedRuleChain.getUuidId(), RuleChain.class); + "/ruleChain/" + savedRuleChain.getUuidId(), RuleChain.class);
RuleChainMetaData metaData = createRuleChainMetadata(savedRuleChain); RuleChainMetaData metaData = createRuleChainMetadata(savedRuleChain);
@ -201,7 +201,7 @@ public class RuleChainEdgeTest extends AbstractEdgeTest {
// set new rule chain as root // set new rule chain as root
RuleChainId currentRootRuleChainId = edge.getRootRuleChainId(); RuleChainId currentRootRuleChainId = edge.getRootRuleChainId();
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(2);
doPost("/api/edge/" + edge.getUuidId() doPost("/api/edge/" + edge.getUuidId()
+ "/" + savedRuleChain.getUuidId() + "/root", Edge.class); + "/" + savedRuleChain.getUuidId() + "/root", Edge.class);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());

6
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.common.data.id.EntityId;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.AttributeDeleteMsg; 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.DeviceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.EntityDataProto; import org.thingsboard.server.gen.edge.v1.EntityDataProto;
import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UplinkMsg;
@ -183,7 +184,7 @@ public class TelemetryEdgeTest extends AbstractEdgeTest {
edgeImitator.setRandomFailuresOnTimeseriesDownlink(true); edgeImitator.setRandomFailuresOnTimeseriesDownlink(true);
// imitator will generate failure in 100% of timeseries cases // imitator will generate failure in 100% of timeseries cases
edgeImitator.setFailureProbability(100); edgeImitator.setFailureProbability(100);
edgeImitator.expectMessageAmount(numberOfMsgsToSend); edgeImitator.expectMessageAmount(numberOfMsgsToSend * 2);
for (int idx = 1; idx <= numberOfMsgsToSend; idx++) { for (int idx = 1; idx <= numberOfMsgsToSend; idx++) {
String timeseriesData = "{\"data\":{\"idx\":" + idx + "},\"ts\":" + System.currentTimeMillis() + "}"; String timeseriesData = "{\"data\":{\"idx\":" + idx + "},\"ts\":" + System.currentTimeMillis() + "}";
JsonNode timeseriesEntityData = JacksonUtil.toJsonNode(timeseriesData); JsonNode timeseriesEntityData = JacksonUtil.toJsonNode(timeseriesData);
@ -204,6 +205,9 @@ public class TelemetryEdgeTest extends AbstractEdgeTest {
List<DeviceUpdateMsg> deviceUpdateMsgs = edgeImitator.findAllMessagesByType(DeviceUpdateMsg.class); List<DeviceUpdateMsg> deviceUpdateMsgs = edgeImitator.findAllMessagesByType(DeviceUpdateMsg.class);
Assert.assertEquals(numberOfMsgsToSend, deviceUpdateMsgs.size()); Assert.assertEquals(numberOfMsgsToSend, deviceUpdateMsgs.size());
List<DeviceCredentialsUpdateMsg> deviceCredentialsUpdateMsgs = edgeImitator.findAllMessagesByType(DeviceCredentialsUpdateMsg.class);
Assert.assertEquals(numberOfMsgsToSend, deviceCredentialsUpdateMsgs.size());
edgeImitator.setRandomFailuresOnTimeseriesDownlink(false); edgeImitator.setRandomFailuresOnTimeseriesDownlink(false);
} }

20
application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java

@ -71,13 +71,13 @@ public class UserEdgeTest extends AbstractEdgeTest {
Assert.assertTrue(userCredentialsUpdateMsgOpt.isPresent()); Assert.assertTrue(userCredentialsUpdateMsgOpt.isPresent());
// update user // update user
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(2);
savedTenantAdmin.setLastName("Borisov"); savedTenantAdmin.setLastName("Borisov");
savedTenantAdmin = doPost("/api/user", savedTenantAdmin, User.class); savedTenantAdmin = doPost("/api/user", savedTenantAdmin, User.class);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage(); userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class);
Assert.assertTrue(latestMessage instanceof UserUpdateMsg); Assert.assertTrue(userUpdateMsgOpt.isPresent());
userUpdateMsg = (UserUpdateMsg) latestMessage; userUpdateMsg = userUpdateMsgOpt.get();
userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true);
Assert.assertNotNull(userMsg); Assert.assertNotNull(userMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType());
@ -92,7 +92,7 @@ public class UserEdgeTest extends AbstractEdgeTest {
changePasswordRequest.setNewPassword("newTenant"); changePasswordRequest.setNewPassword("newTenant");
doPost("/api/auth/changePassword", changePasswordRequest); doPost("/api/auth/changePassword", changePasswordRequest);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage(); AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg);
UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage;
UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true); UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true);
@ -155,13 +155,13 @@ public class UserEdgeTest extends AbstractEdgeTest {
Assert.assertEquals(savedCustomerUser.getLastName(), userMsg.getLastName()); Assert.assertEquals(savedCustomerUser.getLastName(), userMsg.getLastName());
// update user // update user
edgeImitator.expectMessageAmount(1); edgeImitator.expectMessageAmount(2);
savedCustomerUser.setLastName("Addams"); savedCustomerUser.setLastName("Addams");
savedCustomerUser = doPost("/api/user", savedCustomerUser, User.class); savedCustomerUser = doPost("/api/user", savedCustomerUser, User.class);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage(); userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class);
Assert.assertTrue(latestMessage instanceof UserUpdateMsg); Assert.assertTrue(userUpdateMsgOpt.isPresent());
userUpdateMsg = (UserUpdateMsg) latestMessage; userUpdateMsg = userUpdateMsgOpt.get();
userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true);
Assert.assertNotNull(userMsg); Assert.assertNotNull(userMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType());
@ -176,7 +176,7 @@ public class UserEdgeTest extends AbstractEdgeTest {
changePasswordRequest.setNewPassword("newCustomer"); changePasswordRequest.setNewPassword("newCustomer");
doPost("/api/auth/changePassword", changePasswordRequest); doPost("/api/auth/changePassword", changePasswordRequest);
Assert.assertTrue(edgeImitator.waitForMessages()); Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage(); AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg);
UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage;
UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true); UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true);

6
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.EdgeId;
import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId; 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.DeviceProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessorTest; import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessorTest;
@ -61,6 +62,9 @@ public abstract class AbstractDeviceProcessorTest extends BaseEdgeProcessorTest
deviceProfile.setProfileData(deviceProfileData); deviceProfile.setProfileData(deviceProfileData);
deviceProfile.setTransportType(DeviceTransportType.DEFAULT); deviceProfile.setTransportType(DeviceTransportType.DEFAULT);
DeviceCredentials deviceCredentials = new DeviceCredentials();
deviceCredentials.setDeviceId(deviceId);
Device device = new Device(); Device device = new Device();
device.setDeviceProfileId(deviceProfileId); device.setDeviceProfileId(deviceProfileId);
device.setId(deviceId); device.setId(deviceId);
@ -71,9 +75,9 @@ public abstract class AbstractDeviceProcessorTest extends BaseEdgeProcessorTest
edgeEvent.setTenantId(tenantId); edgeEvent.setTenantId(tenantId);
edgeEvent.setAction(EdgeEventActionType.ADDED); edgeEvent.setAction(EdgeEventActionType.ADDED);
willReturn(device).given(deviceService).findDeviceById(tenantId, deviceId); willReturn(device).given(deviceService).findDeviceById(tenantId, deviceId);
willReturn(deviceProfile).given(deviceProfileService).findDeviceProfileById(tenantId, deviceProfileId); willReturn(deviceProfile).given(deviceProfileService).findDeviceProfileById(tenantId, deviceProfileId);
willReturn(deviceCredentials).given(deviceCredentialsService).findDeviceCredentialsByDeviceId(tenantId, deviceId);
} }
protected void updateDeviceProfileDefaultFields(long expectedDashboardIdMSB, long expectedDashboardIdLSB, protected void updateDeviceProfileDefaultFields(long expectedDashboardIdMSB, long expectedDashboardIdLSB,

33
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 lombok.extern.slf4j.Slf4j;
import org.junit.Test; 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.util.ClassUtils;
import org.springframework.web.client.RestTemplate; 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 @Slf4j
public class RestTemplateConvertersTest { public class RestTemplateConvertersTest {
@Test @Test
public void testJacksonXmlConverter() { public void testMappingJackson2HttpMessageConverterIsUsedInsteadOfMappingJackson2XmlHttpMessageConverter() {
ClassLoader classLoader = RestTemplate.class.getClassLoader(); ClassLoader classLoader = RestTemplate.class.getClassLoader();
boolean jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader); 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!"); assertThat(jackson2XmlPresent).isTrue();
//If this xml mapper will be present in classpath then we will get "Unsupported Media Type" in RestTemplate
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) {}
} }

2
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), DELETED_COMMENT(ActionType.DELETED_COMMENT),
ASSIGNED_TO_EDGE(ActionType.ASSIGNED_TO_EDGE), ASSIGNED_TO_EDGE(ActionType.ASSIGNED_TO_EDGE),
UNASSIGNED_FROM_EDGE(ActionType.UNASSIGNED_FROM_EDGE), UNASSIGNED_FROM_EDGE(ActionType.UNASSIGNED_FROM_EDGE),
CREDENTIALS_REQUEST(null), CREDENTIALS_REQUEST(null), // deprecated
ENTITY_MERGE_REQUEST(null); // deprecated ENTITY_MERGE_REQUEST(null); // deprecated
private final ActionType actionType; private final ActionType actionType;

3
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_DEVICE("transport messages per device", false),
TRANSPORT_MESSAGES_PER_GATEWAY("transport messages per gateway", false), TRANSPORT_MESSAGES_PER_GATEWAY("transport messages per gateway", false),
TRANSPORT_MESSAGES_PER_GATEWAY_DEVICE("transport messages per gateway device", 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<DefaultTenantProfileConfiguration, String> configExtractor; private Function<DefaultTenantProfileConfiguration, String> configExtractor;
@Getter @Getter

3
common/message/src/main/java/org/thingsboard/server/common/msg/edge/FromEdgeSyncResponse.java

@ -25,12 +25,13 @@ import java.util.UUID;
@Data @Data
public class FromEdgeSyncResponse implements EdgeSessionMsg { public class FromEdgeSyncResponse implements EdgeSessionMsg {
private static final long serialVersionUID = -6360890886315347486L; private static final long serialVersionUID = -6360890556315667486L;
private final UUID id; private final UUID id;
private final TenantId tenantId; private final TenantId tenantId;
private final EdgeId edgeId; private final EdgeId edgeId;
private final boolean success; private final boolean success;
private final String error;
@Override @Override
public MsgType getMsgType() { public MsgType getMsgType() {

6
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!"); super(entityType.name() + " rate limits reached!");
this.entityType = entityType; this.entityType = entityType;
} }
public TbRateLimitsException(String message) {
super(message);
this.entityType = null;
}
} }

4
common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java

@ -162,6 +162,7 @@ public class ProtoUtils {
.setEdgeIdMSB(response.getEdgeId().getId().getMostSignificantBits()) .setEdgeIdMSB(response.getEdgeId().getId().getMostSignificantBits())
.setEdgeIdLSB(response.getEdgeId().getId().getLeastSignificantBits()) .setEdgeIdLSB(response.getEdgeId().getId().getLeastSignificantBits())
.setSuccess(response.isSuccess()) .setSuccess(response.isSuccess())
.setError(response.getError())
.build(); .build();
} }
@ -170,7 +171,8 @@ public class ProtoUtils {
new UUID(proto.getResponseIdMSB(), proto.getResponseIdLSB()), new UUID(proto.getResponseIdMSB(), proto.getResponseIdLSB()),
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())), TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())),
new EdgeId(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())), new EdgeId(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())),
proto.getSuccess() proto.getSuccess(),
proto.getError()
); );
} }

1
common/proto/src/main/proto/queue.proto

@ -1157,6 +1157,7 @@ message FromEdgeSyncResponseMsgProto {
int64 edgeIdMSB = 5; int64 edgeIdMSB = 5;
int64 edgeIdLSB = 6; int64 edgeIdLSB = 6;
bool success = 7; bool success = 7;
string error = 8;
} }
message DeviceEdgeUpdateMsgProto { message DeviceEdgeUpdateMsgProto {

2
common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java

@ -124,7 +124,7 @@ class ProtoUtilsTest {
@Test @Test
void protoFromEdgeSyncResponseSerialization() { 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); assertThat(ProtoUtils.fromProto(ProtoUtils.toProto(msg))).as("deserialized").isEqualTo(msg);
} }

277
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.TransportProtos;
import org.thingsboard.server.gen.transport.coap.ConfigProtos; import org.thingsboard.server.gen.transport.coap.ConfigProtos;
import org.thingsboard.server.gen.transport.coap.DeviceInfoProtos; 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;
import org.thingsboard.server.gen.transport.coap.MeasurementsProtos.ProtoChannel;
import org.thingsboard.server.transport.coap.AbstractCoapTransportResource; import org.thingsboard.server.transport.coap.AbstractCoapTransportResource;
import org.thingsboard.server.transport.coap.CoapTransportContext; import org.thingsboard.server.transport.coap.CoapTransportContext;
import org.thingsboard.server.transport.coap.callback.CoapDeviceAuthCallback; 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.nio.ByteBuffer;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static com.google.gson.JsonParser.parseString; import static com.google.gson.JsonParser.parseString;
import static org.thingsboard.server.transport.coap.CoapTransportService.CONFIGURATION; 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.CURRENT_TIMESTAMP;
import static org.thingsboard.server.transport.coap.CoapTransportService.DEVICE_INFO; 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.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 @Slf4j
public class CoapEfentoTransportResource extends AbstractCoapTransportResource { public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
@ -220,127 +222,186 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
} }
} }
private List<EfentoTelemetry> getEfentoMeasurements(MeasurementsProtos.ProtoMeasurements protoMeasurements, UUID sessionId) { List<EfentoTelemetry> getEfentoMeasurements(MeasurementsProtos.ProtoMeasurements protoMeasurements, UUID sessionId) {
String serialNumber = CoapEfentoUtils.convertByteArrayToString(protoMeasurements.getSerialNum().toByteArray()); String serialNumber = CoapEfentoUtils.convertByteArrayToString(protoMeasurements.getSerialNum().toByteArray());
boolean batteryStatus = protoMeasurements.getBatteryStatus(); boolean batteryStatus = protoMeasurements.getBatteryStatus();
int measurementPeriodBase = protoMeasurements.getMeasurementPeriodBase(); int measurementPeriodBase = protoMeasurements.getMeasurementPeriodBase();
int measurementPeriodFactor = protoMeasurements.getMeasurementPeriodFactor(); int measurementPeriodFactor = protoMeasurements.getMeasurementPeriodFactor();
int signal = protoMeasurements.getSignal(); int signal = protoMeasurements.getSignal();
List<MeasurementsProtos.ProtoChannel> channelsList = protoMeasurements.getChannelsList(); long nextTransmissionAtMillis = TimeUnit.SECONDS.toMillis(protoMeasurements.getNextTransmissionAt());
List<ProtoChannel> channelsList = protoMeasurements.getChannelsList();
if (CollectionUtils.isEmpty(channelsList)) {
throw new IllegalStateException("[" + sessionId + "]: Failed to get Efento measurements, reason: channels list is empty!");
}
Map<Long, JsonObject> valuesMap = new TreeMap<>(); Map<Long, JsonObject> valuesMap = new TreeMap<>();
if (!CollectionUtils.isEmpty(channelsList)) { for (int channel = 0; channel < channelsList.size(); channel++) {
int channel = 0; ProtoChannel protoChannel = channelsList.get(channel);
JsonObject values; List<Integer> sampleOffsetsList = protoChannel.getSampleOffsetsList();
for (MeasurementsProtos.ProtoChannel protoChannel : channelsList) { if (CollectionUtils.isEmpty(sampleOffsetsList)) {
channel++; log.trace("[{}][{}] sampleOffsetsList list is empty!", sessionId, protoChannel.getType().name());
boolean isBinarySensor = false; continue;
MeasurementTypeProtos.MeasurementType measurementType = protoChannel.getType(); }
String measurementTypeName = measurementType.name(); boolean isBinarySensor = isBinarySensor(protoChannel.getType());
if (measurementType.equals(MeasurementTypeProtos.MeasurementType.OK_ALARM) int channelPeriodFactor = (measurementPeriodFactor == 0 ? (isBinarySensor ? 14 : 1) : measurementPeriodFactor);
|| measurementType.equals(MeasurementTypeProtos.MeasurementType.FLOODING)) { int measurementPeriod = measurementPeriodBase * channelPeriodFactor;
isBinarySensor = true; long measurementPeriodMillis = TimeUnit.SECONDS.toMillis(measurementPeriod);
} long startTimestampMillis = TimeUnit.SECONDS.toMillis(protoChannel.getTimestamp());
if (measurementPeriodFactor == 0 && isBinarySensor) {
measurementPeriodFactor = 14; for (int i = 0; i < sampleOffsetsList.size(); i++) {
} else { int sampleOffset = sampleOffsetsList.get(i);
measurementPeriodFactor = 1; if (isSensorError(sampleOffset)) {
log.warn("[{}],[{}] Sensor error value! Ignoring.", sessionId, sampleOffset);
continue;
} }
int measurementPeriod = measurementPeriodBase * measurementPeriodFactor;
long measurementPeriodMillis = TimeUnit.SECONDS.toMillis(measurementPeriod); JsonObject values;
long nextTransmissionAtMillis = TimeUnit.SECONDS.toMillis(protoMeasurements.getNextTransmissionAt()); if (isBinarySensor) {
int startPoint = protoChannel.getStartPoint(); boolean currentIsOk = sampleOffset < 0;
int startTimestamp = protoChannel.getTimestamp(); Integer previousSampleOffset = i > 0 ? sampleOffsetsList.get(i - 1) : null;
long startTimestampMillis = TimeUnit.SECONDS.toMillis(startTimestamp); if (previousSampleOffset != null) { //compare with previous value
List<Integer> sampleOffsetsList = protoChannel.getSampleOffsetsList(); boolean previousIsOk = previousSampleOffset < 0;
if (!CollectionUtils.isEmpty(sampleOffsetsList)) { if (currentIsOk == previousIsOk) {
int sampleOfssetsListSize = sampleOffsetsList.size(); break;
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;
}
} }
} }
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 { } 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<EfentoTelemetry> efentoMeasurements = new ArrayList<>(); if (CollectionUtils.isEmpty(valuesMap)) {
for (Long ts : valuesMap.keySet()) {
EfentoTelemetry measurement = new EfentoTelemetry(ts, valuesMap.get(ts));
efentoMeasurements.add(measurement);
}
return efentoMeasurements;
} else {
throw new IllegalStateException("[" + sessionId + "]: Failed to collect Efento measurements, reason, values map is empty!"); 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) { private EfentoTelemetry getEfentoDeviceInfo(DeviceInfoProtos.ProtoDeviceInfo protoDeviceInfo) {

13
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; package org.thingsboard.server.transport.coap.efento.utils;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.MeasurementType;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.TimeZone; 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 class CoapEfentoUtils {
public static String convertByteArrayToString(byte[] a) { public static String convertByteArrayToString(byte[] a) {
@ -50,4 +55,12 @@ public class CoapEfentoUtils {
return values; 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;
}
} }

90
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"; option java_outer_classname = "ConfigProtos";
/* Message containing optional channels control parameters */ /* Message containing optional channels control parameters */
message ProtoChannelControl { message ProtoOutputControlState {
/* Channel index */ /* Channel index */
uint32 channel_index = 1; uint32 channel_index = 1;
/* Control parameters. Maximal number equals 4. This field is channel specific: */ /* Channel state ON/OFF. Range (1 - OFF; 2 - ON) */
/* IO_control channel: */ uint32 channel_state = 2;
/* - 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;
} }
/* Message containing request data for accesing calibration parameters */ /* Message containing request data for accesing calibration parameters */
@ -58,6 +45,39 @@ message ProtoCalibrationParameters {
repeated int32 parameters = 3; 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 */ /* 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. */ /* 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 */ /* 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 */ /* 65535 - disable transfer limit function */
uint32 transfer_limit_timer = 10; uint32 transfer_limit_timer = 10;
/* Data (measurements) server IP address */ /* For firmware >= 6.07.00: */
/* IP of the data server as string in form x.x.x.x. For example: 18.184.24.239 */ /* 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; string data_server_ip = 11;
/* Data (measurements) server port */ /* Data (measurements) server port */
/* Range: [1:65535] */ /* Range: [1:65535] */
uint32 data_server_port = 12; uint32 data_server_port = 12;
/* Update server IP address */ /* For firmware >= 6.07.00: */
/* IP of data server as string in form x.x.x.x. For example: 18.184.24.239 */ /* 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; string update_server_ip = 13;
/* Update server port for UDP transmission */ /* Update server port for UDP transmission */
@ -144,7 +174,8 @@ message ProtoConfig {
/* 0xFFFFFFFF or 1000000 - automatic selection */ /* 0xFFFFFFFF or 1000000 - automatic selection */
uint32 plmn_selection = 17; 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 */ /* This field is only sent by server */
uint32 disable_modem_request = 18; uint32 disable_modem_request = 18;
@ -263,9 +294,8 @@ message ProtoConfig {
/* Calendar configuration. Up to 6 calendars are supported */ /* Calendar configuration. Up to 6 calendars are supported */
repeated ProtoCalendar calendars = 47; repeated ProtoCalendar calendars = 47;
/* Control parameters for channels. Maximal number of requests equals 6 */ /* DEPRECATED - Used for backward compatibility */
/* This field is only sent by server */ reserved 48;
repeated ProtoChannelControl channels_control_request = 48;
/* Set/get calibration parameters for single channel. */ /* Set/get calibration parameters for single channel. */
ProtoCalibrationParameters calibration_parameters_request = 49; ProtoCalibrationParameters calibration_parameters_request = 49;
@ -295,7 +325,7 @@ message ProtoConfig {
reserved 52, 53; reserved 52, 53;
/* Encryption key configuration. Sensor sends in this field two last bytes of SHA256 hash calculated from its current */ /* 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. */ /* Max length: 16 bytes. */
/* 0x7F - encryption key disabled. */ /* 0x7F - encryption key disabled. */
bytes encryption_key = 54; bytes encryption_key = 54;
@ -309,4 +339,14 @@ message ProtoConfig {
/* String with special character 0x7F (DEL) only indicates that automatic password is turn on */ /* 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) */ /* Password can only be set to custom value if apn_user_name has been configured (is not automatic) */
string apn_password = 56; 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;
} }

25
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[32] - Rx_time - [0.1s] - Range: [0:2147483647]. Unknown value: -1 */
/* parameters[33] - Tx_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; 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 message ProtoUpdateInfo
@ -115,16 +120,16 @@ message ProtoUpdateInfo
uint32 timestamp = 1; uint32 timestamp = 1;
/* Update status, possible values: */ /* Update status, possible values: */
/* - 0x1 - No update yet */ /* - 1 - No update yet */
/* - 0x2 - No error */ /* - 2 - No error */
/* - 0x3 - UDP socekt error */ /* - 3 - UDP socekt error */
/* - 0x4 - Hash error */ /* - 4 - Hash error */
/* - 0x5 - Missing packet error */ /* - 5 - Missing packet error */
/* - 0x6 - Invalid data error */ /* - 6 - Invalid data error */
/* - 0x7 - Sending timeout error */ /* - 7 - Sending timeout error */
/* - 0x8 - No SW to update error */ /* - 8 - No SW to update error */
/* - 0x9 - Sending unexpected error */ /* - 9 - Sending unexpected error */
/* - 0x10 - Unexpected error */ /* - 10 - Unexpected error */
uint32 status = 2; uint32 status = 2;
} }

227
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"; option java_outer_classname = "MeasurementTypeProtos";
enum MeasurementType { enum MeasurementType {
NO_SENSOR = 0; /* [] - No sensor on the channel */
MEASUREMENT_TYPE_NO_SENSOR = 0;
/* [°C] - Celsius degree. Resolution 0.1°C. Range [-273.2-4000.0]. Type: Continuous */
TEMPERATURE = 1; /* [°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 */
HUMIDITY = 2; /* [% 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]. Type: Continuous */
ATMOSPHERIC_PRESSURE = 3; /* [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]Type: Continuous */
DIFFERENTIAL_PRESSURE = 4; /* [Pa] - Pascal. Resolution 1Pa. Range [-10000:10000]. Differential pressure. Type: Continuous */
MEASUREMENT_TYPE_DIFFERENTIAL_PRESSURE = 4;
/* Sign indicates state: (+) ALARM, (-) OK. Type: Binary */
OK_ALARM = 5; /* Sign indicates state: (+) ALARM, (-) OK. Type: Binary */
MEASUREMENT_TYPE_OK_ALARM = 5;
/* [IAQ] - Iaq index. Resolution 1IAQ. Range [0-500]. Sensor return also calibration status */
/* as offset to measured value: */ /* [IAQ] - IAQ index. Resolution 1IAQ. Range [0:500]. To get IAQ index the value should be divided by 3. */
/* - offset 3000: Sensor not stabilized (always returns 25 IAQ value) */ /* Sensor return also calibration status as metadata (is the remainder when the absolute value is divided by 3): */
/* - offset 2000: Calibration required (sensor returns not accurate values) */ /* - 0: Calibration required (sensor returns not accurate values) */
/* - offset 1000: Calibration on-going (sensor returns not accurate values) */ /* - 1: Calibration on-going (sensor returns not accurate values) */
/* - offset 0: Calibration done (best accuracy of IAQ sensor) */ /* - 2: Calibration done (best accuracy of IAQ sensor) */
/* Type: Continuous */ /* Type: Continuous */
IAQ = 6; MEASUREMENT_TYPE_IAQ = 6;
/* Sign indicates water presence: (+) water not detected, (-) water detected. Type: Binary */ /* Sign indicates water presence: (+) water not detected, (-) water detected. Type: Binary */
FLOODING = 7; MEASUREMENT_TYPE_FLOODING = 7;
/* [NB] Number of pulses. Resolution 1 pulse. Range [0-16711679]. Type: Continuous */ /* [NB] Number of pulses. Resolution 1 pulse. Range [0:8000000]. Type: Continuous */
PULSE_CNT = 8; MEASUREMENT_TYPE_PULSE_CNT = 8;
/* [Wh] - Watthour; Resolution 1Wh. Range [0-16711679]. Number of Watthours in a single period. Type: Continuous */ /* [Wh] - Watthour; Resolution 1Wh. Range [0:8000000]. Number of Watthours in a single period. Type: Continuous */
ELECTRICITY_METER = 9; MEASUREMENT_TYPE_ELECTRICITY_METER = 9;
/* [l] - Liter. Resolution 1l. Range [0-16711679]. Number of litres in a single period. Type: Continuous */ /* [l] - Liter. Resolution 1l. Range [0:8000000]. Number of litres in a single period. Type: Continuous */
WATER_METER = 10; MEASUREMENT_TYPE_WATER_METER = 10;
/* [kPa] - Kilopascal (1kPa = 1000Pa); Resolution 1kPa. Range [-1000-0]. Soil moisture (tension). Type: Continuous */ /* [kPa] - Kilopascal (1kPa = 1000Pa); Resolution 1kPa. Range [-1000:0]. Soil moisture (tension). Type: Continuous */
SOIL_MOISTURE = 11; MEASUREMENT_TYPE_SOIL_MOISTURE = 11;
/* [ppm] - Parts per million. Resolution 1ppm. Range [0-1000000]. Carbon monoxide concentration. Type: Continuous */ /* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon monoxide concentration. Type: Continuous */
CO_GAS = 12; MEASUREMENT_TYPE_CO_GAS = 12;
/* [ppm] - Parts per million. Resolution 0.01ppm. Range [0-1000000.00]. Nitrogen dioxide concentration. Type: Continuous*/ /* [ppm] - Parts per million. Resolution 1.0ppm. Range [0:1000000]. Nitrogen dioxide concentration. Type: Continuous */
NO2_GAS = 13; MEASUREMENT_TYPE_NO2_GAS = 13;
/* [ppm] - Parts per million. Resolution 1ppm. Range [0-1000000]. Hydrogen sulfide concentration. Type: Continuous */ /* [ppm] - Parts per million. Resolution 0.01ppm. Range [0.00:80000.00]. Hydrogen sulfide concentration. Type: Continuous */
H2S_GAS = 14; MEASUREMENT_TYPE_H2S_GAS = 14;
/* [lx] - Illuminance. Resolution 0.1lx. Range [0-100000.0]. Type: Continuous */ /* [lx] - Lux. Resolution 0.1lx. Range [0.0:100000.0]. Illuminance. Type: Continuous */
AMBIENT_LIGHT = 15; MEASUREMENT_TYPE_AMBIENT_LIGHT = 15;
/* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0-1000]. */ /* [µ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 */ /* Particles with an aerodynamic diameter less than 1 micrometer. Type: Continuous */
PM_1_0 = 16; // µg/m^3 MEASUREMENT_TYPE_PM_1_0 = 16;
/* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0-1000]. */ /* [µ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 */ /* Particles with an aerodynamic diameter less than 2.5 micrometers. Type: Continuous */
PM_2_5 = 17; // µg/m^3 MEASUREMENT_TYPE_PM_2_5 = 17;
/* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0-1000]. */ /* [µ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 */ /* Particles with an aerodynamic diameter less than 10 micrometers. Type: Continuous */
PM_10_0 = 18; // µg/m^3 MEASUREMENT_TYPE_PM_10_0 = 18;
/* [dB] - Decibels. Resolution 0.1 dB. Range: [0-130.0]. Noise level. Type: Continuous */ /* [dB] - Decibels. Resolution 0.1 dB. Range: [0.0:200.0]. Noise level. Type: Continuous */
NOISE_LEVEL = 19; // 0.1 dB MEASUREMENT_TYPE_NOISE_LEVEL = 19;
/* [ppm] - Parts per million. Resolution 1ppm. Range [0-1000000]. Ammonia concentration. Type: Continuous */ /* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Ammonia concentration. Type: Continuous */
NH3_GAS = 20; MEASUREMENT_TYPE_NH3_GAS = 20;
/* [ppm] - Parts per million. Resolution 1ppm. Range [0-1000000]. Methane concentration. Type: Continuous */ /* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Methane concentration. Type: Continuous */
CH4_GAS = 21; 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;
} }

139
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_package = "org.thingsboard.server.gen.transport.coap";
option java_outer_classname = "MeasurementsProtos"; option java_outer_classname = "MeasurementsProtos";
message ProtoChannel{ message ProtoChannel {
/* Type of channel */
/* Type of channel */
MeasurementType type = 1; 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; int32 timestamp = 2;
/* Only used for 'Continuous' sensor types. Value used as the starting point for calculating the values of all */ /* Only used for 'Continuous' sensor types. Value used as the starting point for calculating the values of all */
/* measurements in the package. */ /* measurements in the package. */
/* Format defined by 'MeasurementType' field */ /* Format defined by 'MeasurementType' field */
sint32 start_point = 4; sint32 start_point = 4;
/* 'Continuous' sensor types */ /* 'Continuous' sensor types */
/* Value of the offset from the 'start_point' for each measurement in the package. The oldest sample first ([0]). */ /* 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. */ /* 'sample_offsets' format defined by 'MeasurementType' field. */
/* Example: MeasurementType = 1 (temperature), start_point = 100, sample_offsets[0] = 15, sample_offsets[1] = 20 */ /* If the 'sample_offset' has a value from the range [8355840: 8388607], it should be interpreted as a sensor error code. */
/* 1st sample in the package temperature value = 11.5 °C, 2nd sample in the package temperature value = 12 °C */ /* In that case value of the 'start_point' field should not be added to this 'sample_offset'. See ES6-264 for error codes. */
/* Calculating timestamps of the measurements: timestamp = 1606391700, measurement_period_base = 60, */ /* Example: MeasurementType = 1 (temperature), start_point = 100, sample_offsets[0] = 15, sample_offsets[1] = 20, */
/* measurement_period_factor = 1. Timestamp of the 1st sample = 1606391700, timestamp of the 2nd sample = 1606391760 */ /* sample_offset[2] = 8388605 */
/* 'Binary' sensor types: */ /* 1st sample in the package temperature value = 11.5 °C, 2nd sample in the package temperature value = 12 °C */
/* Absolute value of the 'sample_offsets' field indicates the offset in seconds from 'timestamp' field. */ /* 3rd sample in the package has no temperature value. It has information about failure of MCP9808 (temperature) sensor. */
/* Sign (- or +) indicates the state of measurements depend of sensor type. */ /* Calculating timestamps of the measurements: timestamp = 1606391700, measurement_period_base = 60, */
/* Value of this field equals to '1' or '-1' indicates the state at the 'timestamp'. Other values */ /* measurement_period_factor = 1. Timestamp of the 1st sample = 1606391700, timestamp of the 2nd sample = 1606391760, */
/* indicate the state of the relay at the time (in seconds) equal to 'timestamp' + value. */ /* timestamp of the 3rd sample 1606391820 */
/* 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 : */ /* 'Binary' sensor types: */
/* state at the time equal to 'timestamp' + 2 s, etc.). The first and the last sample define the time range of the */ /* Absolute value of the 'sample_offsets' field indicates the offset in seconds from 'timestamp' field. */
/* measurements. Only state changes in the time range are included in the 'sample_offsets' field */ /* Sign (- or +) indicates the state of measurements depending on the sensor type. */
/* Examples: if 'timestamp' value is 1553518060 and 'sample_offsets' equals '1', it means that at 1553518060 the state */ /* Value of this field equals to '1' or '-1' indicates the state at the 'timestamp'. Other values */
/* was high, if 'timestamp' value is 1553518060 and 'sample_offsets' equals '-9', it means at 1553518068 the state was low */ /* 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]; repeated sint32 sample_offsets = 5 [packed=true];
/* Deprecated - configuration is sent to endpoint 'c' */ /* Deprecated - configuration is sent to endpoint 'c' */
//int32 lo_threshold = 6; /* int32 lo_threshold = 6; */
reserved 6;
/* Deprecated - configuration is sent to endpoint 'c' */ /* Deprecated - configuration is sent to endpoint 'c' */
//int32 hi_threshold = 7; /* int32 hi_threshold = 7; */
reserved 7;
/* Deprecated - configurationis sent to endpoint 'c' */ /* Deprecated - configurations sent to endpoint 'c' */
//int32 diff_threshold = 8; /* int32 diff_threshold = 8; */
} reserved 8;
}
message ProtoMeasurements { message ProtoMeasurements {
/* serial number of the device */ /* Serial number of the device */
bytes serial_num = 1; bytes serial_num = 1;
/* true - battery ok, false - battery low */ /* Battery status: true - battery ok, false - battery low */
bool battery_status = 2; bool battery_status = 2;
/* 'Measurement_period_base' and 'measurement_period_factor' define how often the measurements are taken. */ /* '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 'Continuous' type take measurement each Measurement_period_base * measurement_period_factor. */
/* Sensors of 'Binary' type take measurement each Measurement_period_base. */ /* 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 */ /* 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. */ /* 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 */ /* 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. */ /* not sent (equal to 0), then the default value '1' shall be used for period calculation. */
/* measurement period base in seconds */ /* measurement period base in seconds */
uint32 measurement_period_base = 3; uint32 measurement_period_base = 3;
/* Measurement period factor */ /* Measurement period factor */
uint32 measurement_period_factor = 8; uint32 measurement_period_factor = 8;
repeated ProtoChannel channels = 4; repeated ProtoChannel channels = 4;
/* Timestamp of the next scheduled transmission. If the device will not send data until this time, */ /* Timestamp of the next scheduled transmission. If the device will not send data until this time, */
/* it should be considered as 'lost' */ /* it should be considered as 'lost' */
uint32 next_transmission_at = 5; uint32 next_transmission_at = 5;
/* reason of transmission - unsigned integer where each bit indicates different */ /* Reason of transmission - unsigned integer where each bit indicates different possible communication reason. */
/* possible communication reason. Can be more than one */ /* Can be more than one: */
/* - bit 0: first message after sensor reset */ /* - bit 0: first message after sensor reset */
/* - bit 1: user button triggered */ /* - bit 1: user button triggered */
/* - bit 2: user BLE triggered */ /* - bit 2: user BLE triggered */
/* - bit 3-7: number of retries -> incremented after each unsuccessful transmission. Max value 4. */ /* - bit 3-7: number of retries -> incremented after each unsuccessful transmission. Max value 31. */
/* Set to 0 after a successful transmission. */ /* Set to 0 after a successful transmission. */
/* - bit 8: channel 1 lower threshold exceeded */ /* - bit 8...19: rule 1...12 was met */
/* - bit 9: channel 1 lower threshold returned */ /* - bit 20: triggered after the end of the limit */
/* - 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) */
uint32 transfer_reason = 6; uint32 transfer_reason = 6;
/* Signal strength level mapped from RSSI */ /* Signal strength level mapped from RSSI: */
/* - 0 : 113 dBm or less */ /* - 0: RSSI < -110 dBm */
/* - 1 : 111 dBm */ /* - 1: -110 dBm <= RSSI < -109 dBm */
/* - 2...30 : 109...-53 dBm */ /* - 2...61: -109 <= RSSI < -108 dBm ... -50 dBm <= RSSI < -49 dBm */
/* - 31 : -51 dBm or greater */ /* - 62: -49 dBm <= RSSI < -48 dBm */
/* - 99 : Not known or not detectable */ /* - 63: RSSI >= -48 dBm */
/* - 99: Not known or not detectable */
uint32 signal = 7; 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; 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; string cloud_token = 16;
} }

20
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. */ /* - 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. */ /* - BREATH_VOC - [ppm] - Parts per million. Resolution 1ppm. Range [0:100000]. Breath VOC estimate. */
/* - PERCENTAGE - [%] - Percentage. Resolution 0.01%. Range [0.00:100.00]. */ /* - 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_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]. */ /* - PULSE_CNT_ACC_MAJOR - [kNB] - Number of kilopulses. Resolution 1 kilopulse. Range [0:1000000]. */
/* Accumulative pulse counter (major). */ /* Accumulative pulse counter (major). */
/* - ELEC_METER_ACC_MINOR - [Wh] - Watt-hour. Resolution 1Wh. Range [0:1000000]. Accumulative electricity meter (minor). */ /* - 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). */ /* - 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) */ /* 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]. */ /* - 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. */ /* - 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. */ /* - BREATH_VOC - [ppm] - Parts per million. Resolution 1ppm. Range [1:100000]. Breath VOC estimate. */
/* - PERCENTAGE - [%] - Percentage. Resolution 0.01%. Range [0.01:100.00]. */ /* - 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_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]. */ /* - PULSE_CNT_ACC_MAJOR - [kNB] - Number of kilopulses. Resolution 1 kilopulse. Range [1:1000000]. */
/* Accumulative pulse counter (major). */ /* Accumulative pulse counter (major). */
/* - ELEC_METER_ACC_MINOR - [Wh] - Watt-hour. Resolution 1Wh. Range [1:1000000]. Accumulative electricity meter (minor). */ /* - 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). */ /* - 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 */ /* Condition to be checked by the device. If the condition is true, an action is triggered */
enum Condition { enum Condition {
@ -202,6 +218,10 @@ enum Action {
/* To trigger the transmission with ACK */ /* To trigger the transmission with ACK */
ACTION_TRIGGER_TRANSMISSION_WITH_ACK = 3; 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. */ /* Type of a rule calendars. */

144
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<CoapEfentoTransportResource.EfentoTelemetry> 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<CoapEfentoTransportResource.EfentoTelemetry> 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<CoapEfentoTransportResource.EfentoTelemetry> 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);
}
}

44
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<Object, Long> 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();
}
}

13
dao/src/main/java/org/thingsboard/server/dao/sqlts/dictionary/JpaKeyDictionaryDao.java

@ -15,11 +15,13 @@
*/ */
package org.thingsboard.server.dao.sqlts.dictionary; package org.thingsboard.server.dao.sqlts.dictionary;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.hibernate.exception.ConstraintViolationException; import org.hibernate.exception.ConstraintViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Component; 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.dictionary.KeyDictionaryDao;
import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryCompositeKey; import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryCompositeKey;
import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryEntry; import org.thingsboard.server.dao.model.sqlts.dictionary.KeyDictionaryEntry;
@ -34,14 +36,15 @@ import java.util.concurrent.locks.ReentrantLock;
@Component @Component
@Slf4j @Slf4j
@SqlDao @SqlDao
@RequiredArgsConstructor
public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService implements KeyDictionaryDao { public class JpaKeyDictionaryDao extends JpaAbstractDaoListeningExecutorService implements KeyDictionaryDao {
private final ConcurrentMap<String, Integer> keyDictionaryMap = new ConcurrentHashMap<>(); private final KeyDictionaryRepository keyDictionaryRepository;
protected static final ReentrantLock creationLock = new ReentrantLock();
@Autowired private final ConcurrentMap<String, Integer> keyDictionaryMap = new ConcurrentHashMap<>();
private KeyDictionaryRepository keyDictionaryRepository; private static final ReentrantLock creationLock = new ReentrantLock();
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@Override @Override
public Integer getOrSaveKeyId(String strKey) { public Integer getOrSaveKeyId(String strKey) {
Integer keyId = keyDictionaryMap.get(strKey); Integer keyId = keyDictionaryMap.get(strKey);

2
monitoring/src/main/java/org/thingsboard/monitoring/data/Latencies.java

@ -17,8 +17,8 @@ package org.thingsboard.monitoring.data;
public class Latencies { public class Latencies {
public static final String WS_UPDATE = "wsUpdate";
public static final String WS_CONNECT = "wsConnect"; public static final String WS_CONNECT = "wsConnect";
public static final String WS_SUBSCRIBE = "wsSubscribe";
public static final String LOG_IN = "logIn"; public static final String LOG_IN = "logIn";
public static String request(String key) { public static String request(String key) {

3
monitoring/src/main/java/org/thingsboard/monitoring/service/BaseMonitoringService.java

@ -100,7 +100,10 @@ public abstract class BaseMonitoringService<C extends MonitoringConfig<T>, T ext
reporter.reportLatency(Latencies.LOG_IN, stopWatch.getTime()); reporter.reportLatency(Latencies.LOG_IN, stopWatch.getTime());
try (WsClient wsClient = wsClientFactory.createClient(accessToken)) { try (WsClient wsClient = wsClientFactory.createClient(accessToken)) {
stopWatch.start();
wsClient.subscribeForTelemetry(devices, TransportHealthChecker.TEST_TELEMETRY_KEY).waitForReply(); wsClient.subscribeForTelemetry(devices, TransportHealthChecker.TEST_TELEMETRY_KEY).waitForReply();
reporter.reportLatency(Latencies.WS_SUBSCRIBE, stopWatch.getTime());
for (BaseHealthChecker<C, T> healthChecker : healthCheckers) { for (BaseHealthChecker<C, T> healthChecker : healthCheckers) {
check(healthChecker, wsClient); check(healthChecker, wsClient);
} }

4
pom.xml

@ -2097,10 +2097,6 @@
<groupId>io.jsonwebtoken</groupId> <groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId> <artifactId>jjwt-impl</artifactId>
</exclusion> </exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<dependency> <dependency>

2
ui-ngx/src/app/core/services/mobile.service.ts

@ -30,6 +30,7 @@ const dashboardLoadedHandler = 'tbMobileDashboardLoadedHandler';
const dashboardLayoutHandler = 'tbMobileDashboardLayoutHandler'; const dashboardLayoutHandler = 'tbMobileDashboardLayoutHandler';
const navigationHandler = 'tbMobileNavigationHandler'; const navigationHandler = 'tbMobileNavigationHandler';
const mobileHandler = 'tbMobileHandler'; const mobileHandler = 'tbMobileHandler';
const mobileReadyHandler = 'tbMobileReadyHandler';
// @dynamic // @dynamic
@Injectable({ @Injectable({
@ -54,6 +55,7 @@ export class MobileService {
this.mobileApp = isDefined(this.mobileChannel); this.mobileApp = isDefined(this.mobileChannel);
if (this.mobileApp) { if (this.mobileApp) {
window.addEventListener('message', this.onWindowMessageListener); window.addEventListener('message', this.onWindowMessageListener);
this.mobileChannel.callHandler(mobileReadyHandler);
} }
} }

6
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 TruncatePipe from '@shared/pipe/truncate.pipe';
import * as ImagePipe from '@shared/pipe/image.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 coercion from '@shared/decorators/coercion';
import * as enumerable from '@shared/decorators/enumerable'; import * as enumerable from '@shared/decorators/enumerable';
import * as TbInject from '@shared/decorators/tb-inject'; import * as TbInject from '@shared/decorators/tb-inject';
@ -422,6 +425,9 @@ class ModulesMap implements IModulesMap {
'@shared/pipe/truncate.pipe': TruncatePipe, '@shared/pipe/truncate.pipe': TruncatePipe,
'@shared/pipe/image.pipe': ImagePipe, '@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/coercion': coercion,
'@shared/decorators/enumerable': enumerable, '@shared/decorators/enumerable': enumerable,
'@shared/decorators/tb-inject': TbInject, '@shared/decorators/tb-inject': TbInject,

38
ui-ngx/src/app/modules/home/components/audit-log/audit-log-table-config.ts

@ -20,12 +20,19 @@ import {
EntityTableConfig EntityTableConfig
} from '@home/models/entity/entities-table-config.models'; } from '@home/models/entity/entities-table-config.models';
import { import {
ActionStatus,
actionStatusTranslations, actionStatusTranslations,
ActionType,
actionTypeTranslations, actionTypeTranslations,
AuditLog, AuditLog,
AuditLogMode AuditLogMode
} from '@shared/models/audit-log.models'; } 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 { AuditLogService } from '@core/http/audit-log.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
@ -82,7 +89,7 @@ export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLin
if (this.auditLogMode !== AuditLogMode.ENTITY) { if (this.auditLogMode !== AuditLogMode.ENTITY) {
this.columns.push( this.columns.push(
new EntityTableColumn<AuditLog>('entityType', 'audit-log.entity-type', '20%', new EntityTableColumn<AuditLog>('entityType', 'audit-log.entity-type', '20%',
(entity) => translate.instant(entityTypeTranslations.get(entity.entityId.entityType).type)), (entity) => this.getEntityTypeTranslation(entity.entityId.entityType)),
new EntityTableColumn<AuditLog>('entityName', 'audit-log.entity-name', '20%'), new EntityTableColumn<AuditLog>('entityName', 'audit-log.entity-name', '20%'),
); );
} }
@ -95,9 +102,9 @@ export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLin
this.columns.push( this.columns.push(
new EntityTableColumn<AuditLog>('actionType', 'audit-log.type', '33%', new EntityTableColumn<AuditLog>('actionType', 'audit-log.type', '33%',
(entity) => translate.instant(actionTypeTranslations.get(entity.actionType))), (entity) => this.getActionTypeTranslation(entity.actionType)),
new EntityTableColumn<AuditLog>('actionStatus', 'audit-log.status', '33%', new EntityTableColumn<AuditLog>('actionStatus', 'audit-log.status', '33%',
(entity) => translate.instant(actionStatusTranslations.get(entity.actionStatus))) (entity) => this.getActionStatusTranslation(entity.actionStatus))
); );
this.cellActionDescriptors.push( this.cellActionDescriptors.push(
@ -105,11 +112,32 @@ export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLin
name: this.translate.instant('audit-log.details'), name: this.translate.instant('audit-log.details'),
icon: 'more_horiz', icon: 'more_horiz',
isEnabled: () => true, isEnabled: () => 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<PageData<AuditLog>> { fetchAuditLogs(pageLink: TimePageLink): Observable<PageData<AuditLog>> {
switch (this.auditLogMode) { switch (this.auditLogMode) {
case AuditLogMode.TENANT: case AuditLogMode.TENANT:

13
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 { writeValue(brokerConfig: BrokerConfig): void {
const brokerConfigState = { const {
...brokerConfig, version = 5,
version: brokerConfig.version || 5, clientId = `tb_gw_${generateSecret(5)}`,
clientId: brokerConfig.clientId || 'tb_gw_' + generateSecret(5), security = {},
}; } = brokerConfig;
this.brokerConfigFormGroup.reset(brokerConfigState, {emitEvent: false});
this.brokerConfigFormGroup.reset({ ...brokerConfig, version, clientId, security }, { emitEvent: false });
this.cdr.markForCheck(); this.cdr.markForCheck();
} }

20
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component.html

@ -18,7 +18,7 @@
<div class="tb-mapping-table tb-absolute-fill"> <div class="tb-mapping-table tb-absolute-fill">
<div fxFlex fxLayout="column" class="tb-mapping-table-content"> <div fxFlex fxLayout="column" class="tb-mapping-table-content">
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode"> <mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools" *ngIf="(dataSource.isEmpty() | async) === false">
<div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container"> <div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container">
<span class="tb-mapping-table-title">{{mappingTypeTranslationsMap.get(mappingType) | translate}}</span> <span class="tb-mapping-table-title">{{mappingTypeTranslationsMap.get(mappingType) | translate}}</span>
</div> </div>
@ -75,15 +75,22 @@
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let mapping; let i = index" <mat-cell *matCellDef="let mapping; let i = index"
[ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px'}"> [ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px'}">
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end"> <ng-template #rowActions>
<button mat-icon-button <button mat-icon-button
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
(click)="manageMapping($event, i)"> (click)="manageMapping($event, i)">
<tb-icon>edit</tb-icon> <tb-icon>edit</tb-icon>
</button> </button>
<button mat-icon-button <button mat-icon-button
matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above"
(click)="deleteMapping($event, i)"> (click)="deleteMapping($event, i)">
<tb-icon>delete</tb-icon> <tb-icon>delete</tb-icon>
</button> </button>
</ng-template>
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<ng-container [ngTemplateOutlet]="rowActions"></ng-container>
</div> </div>
<div fxHide fxShow.lt-lg fxFlex fxLayout="row" fxLayoutAlign="end"> <div fxHide fxShow.lt-lg fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-icon-button <button mat-icon-button
@ -92,14 +99,7 @@
<mat-icon class="material-icons">more_vert</mat-icon> <mat-icon class="material-icons">more_vert</mat-icon>
</button> </button>
<mat-menu #cellActionsMenu="matMenu" xPosition="before"> <mat-menu #cellActionsMenu="matMenu" xPosition="before">
<button mat-icon-button <ng-container [ngTemplateOutlet]="rowActions"></ng-container>
(click)="manageMapping($event, i)">
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button
(click)="deleteMapping($event, i)">
<tb-icon>delete</tb-icon>
</button>
</mat-menu> </mat-menu>
</div> </div>
</mat-cell> </mat-cell>

10
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 @@
<tb-modbus-master-table formControlName="master"></tb-modbus-master-table> <tb-modbus-master-table formControlName="master"></tb-modbus-master-table>
</mat-tab> </mat-tab>
<mat-tab label="{{ 'gateway.server-config' | translate }}"> <mat-tab label="{{ 'gateway.server-config' | translate }}">
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-form-hint tb-primary-fill tb-flex center">{{ 'gateway.hints.modbus-server' | translate }}</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" [formControl]="enableSlaveControl">
<mat-label>
{{ 'gateway.enable' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</div>
<tb-modbus-slave-config formControlName="slave"></tb-modbus-slave-config> <tb-modbus-slave-config formControlName="slave"></tb-modbus-slave-config>
</mat-tab> </mat-tab>
</mat-tab-group> </mat-tab-group>

6
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 { :host {
height: 100%; height: 100%;
} }
:host ::ng-deep {
.mat-mdc-tab-body-content {
overflow: hidden !important;
}
}

40
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 { import {
ControlValueAccessor, ControlValueAccessor,
FormBuilder, FormBuilder,
FormControl,
FormGroup, FormGroup,
NG_VALIDATORS, NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
UntypedFormControl,
ValidationErrors, ValidationErrors,
Validator, Validator,
} from '@angular/forms'; } 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 { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@ -33,6 +35,7 @@ import { Subject } from 'rxjs';
import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive'; import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive';
import { ModbusSlaveConfigComponent } from '../modbus-slave-config/modbus-slave-config.component'; import { ModbusSlaveConfigComponent } from '../modbus-slave-config/modbus-slave-config.component';
import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master-table.component'; import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master-table.component';
import { isEqual } from '@core/utils';
@Component({ @Component({
selector: 'tb-modbus-basic-config', selector: 'tb-modbus-basic-config',
@ -66,6 +69,7 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat
@Input() generalTabContent: TemplateRef<any>; @Input() generalTabContent: TemplateRef<any>;
basicFormGroup: FormGroup; basicFormGroup: FormGroup;
enableSlaveControl: FormControl<boolean>;
onChange: (value: ModbusBasicConfig) => void; onChange: (value: ModbusBasicConfig) => void;
onTouched: () => void; onTouched: () => void;
@ -77,13 +81,21 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat
master: [], master: [],
slave: [], slave: [],
}); });
this.enableSlaveControl = new FormControl(false);
this.basicFormGroup.valueChanges this.basicFormGroup.valueChanges
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe(value => { .subscribe(({ master, slave }) => {
this.onChange(value); this.onChange({ master, slave: slave ?? {} });
this.onTouched(); this.onTouched();
}); });
this.enableSlaveControl.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(enable => {
this.updateSlaveEnabling(enable);
this.basicFormGroup.get('slave').updateValueAndValidity({emitEvent: !!this.onChange});
});
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@ -106,11 +118,25 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat
}; };
this.basicFormGroup.setValue(editedBase, {emitEvent: false}); this.basicFormGroup.setValue(editedBase, {emitEvent: false});
this.enableSlaveControl.setValue(!!basicConfig.slave && !isEqual(basicConfig.slave, {}));
} }
validate(): ValidationErrors | null { validate(basicFormControl: UntypedFormControl): ValidationErrors | null {
return this.basicFormGroup.valid ? null : { const { master, slave } = basicFormControl.value;
basicFormGroup: {valid: false} 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});
}
} }
} }

16
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 @@
<mat-expansion-panel class="tb-settings" [expanded]="last"> <mat-expansion-panel class="tb-settings" [expanded]="last">
<mat-expansion-panel-header fxLayout="row wrap"> <mat-expansion-panel-header fxLayout="row wrap">
<mat-panel-title> <mat-panel-title>
<div class="title-container"> <div *ngIf="isMaster else tagName" class="title-container" tbTruncateWithTooltip>
<span *ngIf="isMaster else tagName"> {{ keyControl.get('tag').value }}{{ '-' }}{{ keyControl.get('value').value }}
{{ keyControl.get('tag').value }}{{ '-' }}{{ keyControl.get('value').value }}
</span>
<ng-template #tagName>{{ keyControl.get('tag').value }}</ng-template>
</div> </div>
<ng-template #tagName>
<div class="tb-flex">
<div class="title-container" tbTruncateWithTooltip>{{ 'gateway.key' | translate }}: {{ keyControl.get('tag').value }}</div>
<div class="title-container">{{ 'gateway.address' | translate }}: {{ keyControl.get('address').value }}</div>
<div class="title-container">{{ 'gateway.type' | translate }}: {{ keyControl.get('type').value }}</div>
</div>
</ng-template>
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
@ -98,7 +102,7 @@
name="value" name="value"
formControlName="objectsCount" formControlName="objectsCount"
placeholder="{{ 'gateway.set' | translate }}" placeholder="{{ 'gateway.set' | translate }}"
[readonly]="!editableDataTypes.includes(keyControl.get('type').value)" [readonly]="!ModbusEditableDataTypes.includes(keyControl.get('type').value)"
/> />
</mat-form-field> </mat-form-field>
</div> </div>

5
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; max-width: 700px;
.title-container { .title-container {
max-width: 11vw; width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
} }
.key-panel { .key-panel {

28
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 { TbPopoverComponent } from '@shared/components/popover.component';
import { import {
ModbusDataType, ModbusDataType,
ModbusEditableDataTypes,
ModbusFunctionCodeTranslationsMap, ModbusFunctionCodeTranslationsMap,
ModbusObjectCountByDataType, ModbusObjectCountByDataType,
ModbusValue, ModbusValue,
@ -40,6 +41,7 @@ import { generateSecret } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion'; import { coerceBoolean } from '@shared/decorators/coercion';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
@Component({ @Component({
selector: 'tb-modbus-data-keys-panel', selector: 'tb-modbus-data-keys-panel',
@ -50,6 +52,7 @@ import { Subject } from 'rxjs';
CommonModule, CommonModule,
SharedModule, SharedModule,
GatewayHelpLinkPipe, GatewayHelpLinkPipe,
TruncateWithTooltipDirective,
] ]
}) })
export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy { export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
@ -72,14 +75,15 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
functionCodesMap = new Map(); functionCodesMap = new Map();
defaultFunctionCodes = []; defaultFunctionCodes = [];
readonly editableDataTypes = [ModbusDataType.BYTES, ModbusDataType.BITS, ModbusDataType.STRING]; readonly ModbusEditableDataTypes = ModbusEditableDataTypes;
readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap; readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
private readonly defaultReadFunctionCodes = [3, 4]; private readonly defaultReadFunctionCodes = [3, 4];
private readonly defaultWriteFunctionCodes = [5, 6, 15, 16]; private readonly bitsReadFunctionCodes = [1, 2];
private readonly stringAttrUpdatesWriteFunctionCodes = [6, 16]; private readonly defaultWriteFunctionCodes = [6, 16];
private readonly bitsWriteFunctionCodes = [5, 15];
constructor(private fb: UntypedFormBuilder) {} constructor(private fb: UntypedFormBuilder) {}
@ -161,7 +165,7 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
private observeKeyDataType(keyFormGroup: FormGroup): void { private observeKeyDataType(keyFormGroup: FormGroup): void {
keyFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(dataType => { 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}); keyFormGroup.get('objectsCount').patchValue(ModbusObjectCountByDataType[dataType], {emitEvent: false});
} }
this.updateFunctionCodes(keyFormGroup, dataType); this.updateFunctionCodes(keyFormGroup, dataType);
@ -177,23 +181,23 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
} }
private getFunctionCodes(dataType: ModbusDataType): number[] { private getFunctionCodes(dataType: ModbusDataType): number[] {
const writeFunctionCodes = [
...(dataType === ModbusDataType.BITS ? this.bitsWriteFunctionCodes : []), ...this.defaultWriteFunctionCodes
];
if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) { if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) {
return dataType === ModbusDataType.STRING return writeFunctionCodes.sort((a, b) => a - b);
? this.stringAttrUpdatesWriteFunctionCodes
: this.defaultWriteFunctionCodes;
} }
const functionCodes = [...this.defaultReadFunctionCodes]; const functionCodes = [...this.defaultReadFunctionCodes];
if (dataType === ModbusDataType.BITS) { if (dataType === ModbusDataType.BITS) {
const bitsFunctionCodes = [1, 2]; functionCodes.push(...this.bitsReadFunctionCodes);
functionCodes.push(...bitsFunctionCodes);
functionCodes.sort();
} }
if (this.keysType === ModbusValueKey.RPC_REQUESTS) { 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[] { private getDefaultFunctionCodes(): number[] {

37
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 @@
--> -->
<div class="tb-master-table tb-absolute-fill"> <div class="tb-master-table tb-absolute-fill">
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-form-hint tb-primary-fill tb-flex center">{{ 'gateway.hints.modbus-master' | translate }}</div>
</div>
<div fxFlex fxLayout="column" class="tb-master-table-content"> <div fxFlex fxLayout="column" class="tb-master-table-content">
<mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode"> <mat-toolbar class="mat-mdc-table-toolbar" [fxShow]="!textSearchMode">
<div class="mat-toolbar-tools"> <div class="mat-toolbar-tools" *ngIf="(dataSource.isEmpty() | async) === false">
<div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container"> <div fxLayout="row" fxLayoutAlign="start center" fxLayout.xs="column" fxLayoutAlign.xs="center start" class="title-container">
<span class="tb-master-table-title">{{ 'gateway.servers-slaves' | translate}}</span> <span class="tb-master-table-title">{{ 'gateway.servers-slaves' | translate}}</span>
</div> </div>
@ -63,33 +66,40 @@
<mat-header-cell *matHeaderCellDef class="table-value-column"> <mat-header-cell *matHeaderCellDef class="table-value-column">
{{ 'gateway.name' | translate }} {{ 'gateway.name' | translate }}
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let mapping" class="table-value-column"> <mat-cell *matCellDef="let slave" class="table-value-column">
{{ mapping['name'] }} {{ slave['name'] }}
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container [matColumnDef]="'type'"> <ng-container [matColumnDef]="'type'">
<mat-header-cell *matHeaderCellDef class="table-value-column"> <mat-header-cell *matHeaderCellDef class="table-value-column">
{{ 'gateway.client-communication-type' | translate }} {{ 'gateway.client-communication-type' | translate }}
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let mapping" class="table-value-column"> <mat-cell *matCellDef="let slave" class="table-value-column">
{{ ModbusProtocolLabelsMap.get(mapping['type']) }} {{ ModbusProtocolLabelsMap.get(slave['type']) }}
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<ng-container matColumnDef="actions" stickyEnd> <ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef <mat-header-cell *matHeaderCellDef
[ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px', textAlign: 'center'}"> [ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px', textAlign: 'center'}">
</mat-header-cell> </mat-header-cell>
<mat-cell *matCellDef="let mapping; let i = index" <mat-cell *matCellDef="let slave; let i = index"
[ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px'}"> [ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px'}">
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end"> <ng-template #rowActions>
<button mat-icon-button <button mat-icon-button
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
(click)="manageSlave($event, i)"> (click)="manageSlave($event, i)">
<tb-icon>edit</tb-icon> <tb-icon>edit</tb-icon>
</button> </button>
<button mat-icon-button <button mat-icon-button
(click)="deleteMapping($event, i)"> matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above"
(click)="deleteSlave($event, i)">
<tb-icon>delete</tb-icon> <tb-icon>delete</tb-icon>
</button> </button>
</ng-template>
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<ng-container [ngTemplateOutlet]="rowActions"></ng-container>
</div> </div>
<div fxHide fxShow.lt-lg fxFlex fxLayout="row" fxLayoutAlign="end"> <div fxHide fxShow.lt-lg fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-icon-button <button mat-icon-button
@ -98,20 +108,13 @@
<mat-icon class="material-icons">more_vert</mat-icon> <mat-icon class="material-icons">more_vert</mat-icon>
</button> </button>
<mat-menu #cellActionsMenu="matMenu" xPosition="before"> <mat-menu #cellActionsMenu="matMenu" xPosition="before">
<button mat-icon-button <ng-container [ngTemplateOutlet]="rowActions"></ng-container>
(click)="manageSlave($event, i)">
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button
(click)="deleteMapping($event, i)">
<tb-icon>delete</tb-icon>
</button>
</mat-menu> </mat-menu>
</div> </div>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="['name', 'type', 'actions']; sticky: true"></mat-header-row> <mat-header-row [ngClass]="{'mat-row-select': true}" *matHeaderRowDef="['name', 'type', 'actions']; sticky: true"></mat-header-row>
<mat-row *matRowDef="let mapping; columns: ['name', 'type', 'actions']"></mat-row> <mat-row *matRowDef="let slave; columns: ['name', 'type', 'actions']"></mat-row>
</table> </table>
<section [fxShow]="!textSearchMode && (dataSource.isEmpty() | async)" fxLayoutAlign="center center" <section [fxShow]="!textSearchMode && (dataSource.isEmpty() | async)" fxLayoutAlign="center center"
class="mat-headline-5 tb-absolute-fill tb-add-new"> class="mat-headline-5 tb-absolute-fill tb-add-new">

26
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, ControlValueAccessor,
FormArray, FormArray,
FormBuilder, FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR, NG_VALUE_ACCESSOR,
UntypedFormGroup, UntypedFormGroup,
ValidationErrors,
Validator,
} from '@angular/forms'; } from '@angular/forms';
import { import {
ModbusMasterConfig, ModbusMasterConfig,
ModbusProtocolLabelsMap, ModbusProtocolLabelsMap,
ModbusSlaveInfo,
ModbusValues,
SlaveConfig SlaveConfig
} from '@home/components/widget/lib/gateway/gateway-widget.models'; } 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 { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ModbusSlaveDialogComponent } from '../modbus-slave-dialog/modbus-slave-dialog.component'; 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), useExisting: forwardRef(() => ModbusMasterTableComponent),
multi: true multi: true
}, },
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ModbusMasterTableComponent),
multi: true
}
], ],
standalone: true, standalone: true,
imports: [CommonModule, SharedModule] imports: [CommonModule, SharedModule]
}) })
export class ModbusMasterTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy { export class ModbusMasterTableComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy {
@ViewChild('searchInput') searchInputField: ElementRef; @ViewChild('searchInput') searchInputField: ElementRef;
@ -138,12 +132,6 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat
this.pushDataAsFormArrays(master.slaves); this.pushDataAsFormArrays(master.slaves);
} }
validate(): ValidationErrors | null {
return this.slaves.controls.length ? null : {
slavesFormGroup: {valid: false}
};
}
enterFilterMode(): void { enterFilterMode(): void {
this.textSearchMode = true; this.textSearchMode = true;
this.cdr.detectChanges(); this.cdr.detectChanges();
@ -164,12 +152,12 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat
} }
const withIndex = isDefinedAndNotNull(index); const withIndex = isDefinedAndNotNull(index);
const value = withIndex ? this.slaves.at(index).value : {}; const value = withIndex ? this.slaves.at(index).value : {};
this.dialog.open<ModbusSlaveDialogComponent, any, any>(ModbusSlaveDialogComponent, { this.dialog.open<ModbusSlaveDialogComponent, ModbusSlaveInfo, ModbusValues>(ModbusSlaveDialogComponent, {
disableClose: true, disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: { data: {
value, value,
buttonTitle: withIndex ? 'action.add' : 'action.apply' buttonTitle: withIndex ? 'action.apply' : 'action.add'
} }
}).afterClosed() }).afterClosed()
.pipe(take(1), takeUntil(this.destroy$)) .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) { if ($event) {
$event.stopPropagation(); $event.stopPropagation();
} }

92
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 @@
<!--
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.
-->
<ng-container [formGroup]="rpcParametersFormGroup">
<div fxFlex fxLayout="row">
<mat-form-field fxFlex="100">
<mat-label>{{ 'gateway.key' | translate }}</mat-label>
<input matInput name="value" formControlName="tag" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.key-required') | translate"
*ngIf="rpcParametersFormGroup.get('tag').hasError('required') &&
rpcParametersFormGroup.get('tag').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayoutGap="10px">
<mat-form-field fxFlex="50" class="mat-block">
<mat-label>{{ 'gateway.rpc.type' | translate }}</mat-label>
<mat-select formControlName="type">
<mat-option *ngFor="let type of modbusDataTypes" [value]="type">{{ type }}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="50" class="mat-block">
<mat-label>{{ 'gateway.rpc.functionCode' | translate }}</mat-label>
<mat-select formControlName="functionCode">
<mat-option *ngFor="let code of functionCodes" [value]="code">{{ ModbusFunctionCodeTranslationsMap.get(code) | translate}}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div fxFlex fxLayout="row">
<mat-form-field fxFlex="100" *ngIf="writeFunctionCodes.includes(rpcParametersFormGroup.get('functionCode').value)">
<mat-label>{{ 'gateway.rpc.value' | translate }}</mat-label>
<input matInput name="value" formControlName="value" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.value-required') | translate"
*ngIf="rpcParametersFormGroup.get('value').hasError('required') &&
rpcParametersFormGroup.get('value').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
</div>
<div fxFlex fxLayout="row" fxLayoutGap="10px">
<mat-form-field fxFlex="50">
<mat-label>{{ 'gateway.rpc.address' | translate }}</mat-label>
<input matInput type="number" min="0" max="50000" name="value" formControlName="address" placeholder="{{ 'gateway.set' | translate }}"/>
<mat-icon matSuffix
matTooltipPosition="above"
matTooltipClass="tb-error-tooltip"
[matTooltip]="('gateway.address-required') | translate"
*ngIf="rpcParametersFormGroup.get('address').hasError('required') &&
rpcParametersFormGroup.get('address').touched"
class="tb-error">
warning
</mat-icon>
</mat-form-field>
<mat-form-field fxFlex="50">
<mat-label>{{ 'gateway.rpc.objectsCount' | translate }}</mat-label>
<input
matInput
type="number"
min="1"
max="50000"
name="value"
formControlName="objectsCount"
placeholder="{{ 'gateway.set' | translate }}"
[readonly]="!ModbusEditableDataTypes.includes(rpcParametersFormGroup.get('type').value)"
/>
</mat-form-field>
</div>
</ng-container>

166
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<number>;
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<void>();
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});
}
}
}

19
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 @@
--> -->
<div [formGroup]="slaveConfigFormGroup" class="slave-container"> <div [formGroup]="slaveConfigFormGroup" class="slave-container">
<div class="tb-form-panel no-border no-padding padding-top">
<div class="tb-form-hint tb-primary-fill tb-flex center">{{ 'gateway.hints.modbus-server' | translate }}</div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="sendDataToThingsBoard">
<mat-label>
{{ 'gateway.enable' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
</div>
<div class="slave-content tb-form-panel no-border no-padding padding-top" > <div class="slave-content tb-form-panel no-border no-padding padding-top" >
<div class="tb-flex row space-between align-center no-gap fill-width"> <div class="tb-flex row space-between align-center no-gap fill-width">
<div class="fixed-title-width" translate>gateway.server-slave-config</div> <div class="fixed-title-width" translate>gateway.server-slave-config</div>
@ -161,7 +151,7 @@
</div> </div>
</div> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width" translate>gateway.poll-period</div> <div class="fixed-title-width tb-required" translate>gateway.poll-period</div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="pollPeriod" placeholder="{{ 'gateway.set' | translate }}"/> <input matInput type="number" min="0" name="value" formControlName="pollPeriod" placeholder="{{ 'gateway.set' | translate }}"/>
@ -178,6 +168,13 @@
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div class="tb-form-row" fxLayoutAlign="space-between center">
<mat-slide-toggle class="mat-slide" formControlName="sendDataToThingsBoard">
<mat-label>
{{ 'gateway.send-data-TB' | translate }}
</mat-label>
</mat-slide-toggle>
</div>
<div class="tb-form-panel stroked"> <div class="tb-form-panel stroked">
<mat-expansion-panel class="tb-settings"> <mat-expansion-panel class="tb-settings">
<mat-expansion-panel-header> <mat-expansion-panel-header>

27
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-slave-config/modbus-slave-config.component.scss

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

41
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 { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Subject } from 'rxjs'; 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 { GatewayPortTooltipPipe } from '@home/components/widget/lib/gateway/pipes/gateway-port-tooltip.pipe';
import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component'; import { ModbusSecurityConfigComponent } from '../modbus-security-config/modbus-security-config.component';
import { ModbusValuesComponent, } from '../modbus-values/modbus-values.component'; import { ModbusValuesComponent, } from '../modbus-values/modbus-values.component';
@ -73,7 +73,6 @@ import { isEqual } from '@core/utils';
ModbusSecurityConfigComponent, ModbusSecurityConfigComponent,
GatewayPortTooltipPipe, GatewayPortTooltipPipe,
], ],
styleUrls: ['./modbus-slave-config.component.scss'],
}) })
export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validator, OnDestroy { export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validator, OnDestroy {
@ -90,6 +89,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
readonly ModbusProtocolType = ModbusProtocolType; readonly ModbusProtocolType = ModbusProtocolType;
readonly modbusBaudrates = ModbusBaudrates; readonly modbusBaudrates = ModbusBaudrates;
private isSlaveEnabled = false;
private readonly serialSpecificControlKeys = ['serialPort', 'baudrate']; private readonly serialSpecificControlKeys = ['serialPort', 'baudrate'];
private readonly tcpUdpSpecificControlKeys = ['port', 'security', 'host']; 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)]], port: [null, [Validators.required, Validators.min(PortLimits.MIN), Validators.max(PortLimits.MAX)]],
serialPort: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], serialPort: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
method: [ModbusMethodType.SOCKET], method: [ModbusMethodType.SOCKET],
unitId: [0, [Validators.required]], unitId: [null, [Validators.required]],
baudrate: [this.modbusBaudrates[0]], baudrate: [this.modbusBaudrates[0]],
deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
pollPeriod: [5000], pollPeriod: [5000, [Validators.required]],
sendDataToThingsBoard: [false], sendDataToThingsBoard: [false],
byteOrder:[ModbusOrderType.BIG], byteOrder:[ModbusOrderType.BIG],
security: [], security: [],
@ -126,14 +126,9 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
this.observeValueChanges(); this.observeValueChanges();
this.observeTypeChange(); this.observeTypeChange();
this.observeFormEnable();
this.observeShowSecurity(); this.observeShowSecurity();
} }
get isSlaveEnabled(): boolean {
return this.slaveConfigFormGroup.get('sendDataToThingsBoard').value;
}
get protocolType(): ModbusProtocolType { get protocolType(): ModbusProtocolType {
return this.slaveConfigFormGroup.get('type').value; return this.slaveConfigFormGroup.get('type').value;
} }
@ -160,7 +155,11 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
writeValue(slaveConfig: ModbusSlave): void { writeValue(slaveConfig: ModbusSlave): void {
this.showSecurityControl.patchValue(!!slaveConfig.security && !isEqual(slaveConfig.security, {})); this.showSecurityControl.patchValue(!!slaveConfig.security && !isEqual(slaveConfig.security, {}));
this.updateSlaveConfig(slaveConfig); this.updateSlaveConfig(slaveConfig);
this.updateFormEnableState(slaveConfig.sendDataToThingsBoard); }
setDisabledState(isDisabled: boolean): void {
this.isSlaveEnabled = !isDisabled;
this.updateFormEnableState();
} }
private observeValueChanges(): void { private observeValueChanges(): void {
@ -180,7 +179,7 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
this.slaveConfigFormGroup.get('type').valueChanges this.slaveConfigFormGroup.get('type').valueChanges
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe(type => { .subscribe(type => {
this.updateFormEnableState(this.isSlaveEnabled); this.updateFormEnableState();
this.updateMethodType(type); this.updateMethodType(type);
}); });
} }
@ -196,22 +195,15 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
} }
} }
private observeFormEnable(): void { private updateFormEnableState(): void {
this.slaveConfigFormGroup.get('sendDataToThingsBoard').valueChanges if (this.isSlaveEnabled) {
.pipe(startWith(this.isSlaveEnabled), takeUntil(this.destroy$))
.subscribe(value => this.updateFormEnableState(value));
}
private updateFormEnableState(enabled: boolean): void {
if (enabled) {
this.slaveConfigFormGroup.enable({emitEvent: false}); this.slaveConfigFormGroup.enable({emitEvent: false});
this.showSecurityControl.enable({emitEvent: false}); this.showSecurityControl.enable({emitEvent: false});
} else { } else {
this.slaveConfigFormGroup.disable({emitEvent: false}); this.slaveConfigFormGroup.disable({emitEvent: false});
this.showSecurityControl.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); this.updateSecurityEnable(this.showSecurityControl.value);
} }
@ -229,9 +221,10 @@ export class ModbusSlaveConfigComponent implements ControlValueAccessor, Validat
} }
} }
private updateEnablingByProtocol(type: ModbusProtocolType): void { private updateEnablingByProtocol(): void {
const enableKeys = type === ModbusProtocolType.Serial ? this.serialSpecificControlKeys : this.tcpUdpSpecificControlKeys; const isSerial = this.protocolType === ModbusProtocolType.Serial;
const disableKeys = type === ModbusProtocolType.Serial ? this.tcpUdpSpecificControlKeys : this.serialSpecificControlKeys; const enableKeys = isSerial ? this.serialSpecificControlKeys : this.tcpUdpSpecificControlKeys;
const disableKeys = isSerial ? this.tcpUdpSpecificControlKeys : this.serialSpecificControlKeys;
if (this.isSlaveEnabled) { if (this.isSlaveEnabled) {
enableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({ emitEvent: false })); enableKeys.forEach(key => this.slaveConfigFormGroup.get(key)?.enable({ emitEvent: false }));

10
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 @@
</mat-toolbar> </mat-toolbar>
<div mat-dialog-content [formGroup]="slaveConfigFormGroup" class="tb-form-panel"> <div mat-dialog-content [formGroup]="slaveConfigFormGroup" class="tb-form-panel">
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width tb-required" translate>gateway.name</div> <div class="fixed-title-width slave-name-label tb-required" translate>gateway.name</div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/> <input matInput name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/>
@ -306,7 +306,7 @@
</mat-slide-toggle> </mat-slide-toggle>
</div> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width-230" translate>gateway.poll-period</div> <div class="fixed-title-width-230 tb-required" translate>gateway.poll-period</div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="pollPeriod" placeholder="{{ 'gateway.set' | translate }}"/> <input matInput type="number" min="0" name="value" formControlName="pollPeriod" placeholder="{{ 'gateway.set' | translate }}"/>
@ -314,7 +314,7 @@
</div> </div>
</div> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width-230" translate>gateway.connect-attempt-time</div> <div class="fixed-title-width-230 tb-required" translate>gateway.connect-attempt-time</div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="connectAttemptTimeMs" placeholder="{{ 'gateway.set' | translate }}"/> <input matInput type="number" min="0" name="value" formControlName="connectAttemptTimeMs" placeholder="{{ 'gateway.set' | translate }}"/>
@ -322,7 +322,7 @@
</div> </div>
</div> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width-230" translate>gateway.connect-attempt-count</div> <div class="fixed-title-width-230 tb-required" translate>gateway.connect-attempt-count</div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="connectAttemptCount" placeholder="{{ 'gateway.set' | translate }}"/> <input matInput type="number" min="0" name="value" formControlName="connectAttemptCount" placeholder="{{ 'gateway.set' | translate }}"/>
@ -330,7 +330,7 @@
</div> </div>
</div> </div>
<div class="tb-form-row column-xs" fxLayoutAlign="space-between center"> <div class="tb-form-row column-xs" fxLayoutAlign="space-between center">
<div class="fixed-title-width-230" translate>gateway.wait-after-failed-attempts</div> <div class="fixed-title-width-230 tb-required" translate>gateway.wait-after-failed-attempts</div>
<div class="tb-flex no-gap"> <div class="tb-flex no-gap">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic"> <mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput type="number" min="0" name="value" formControlName="waitAfterFailedAttemptsMs" placeholder="{{ 'gateway.set' | translate }}"/> <input matInput type="number" min="0" name="value" formControlName="waitAfterFailedAttemptsMs" placeholder="{{ 'gateway.set' | translate }}"/>

5
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; width: 80vw;
max-width: 900px; max-width: 900px;
} }
.slave-name-label {
margin-right: 16px;
color: rgba(0, 0, 0, 0.87);
}
} }

10
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<ModbusSlaveDialo
bytesize: [ModbusByteSizes[0]], bytesize: [ModbusByteSizes[0]],
parity: [ModbusParity.None], parity: [ModbusParity.None],
strict: [true], strict: [true],
unitId: [0, [Validators.required]], unitId: [null, [Validators.required]],
deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
sendDataOnlyOnChange: [false], sendDataOnlyOnChange: [false],
@ -173,10 +173,10 @@ export class ModbusSlaveDialogComponent extends DialogComponent<ModbusSlaveDialo
retries: [true], retries: [true],
retryOnEmpty: [true], retryOnEmpty: [true],
retryOnInvalid: [true], retryOnInvalid: [true],
pollPeriod: [5000], pollPeriod: [5000, [Validators.required]],
connectAttemptTimeMs: [5000], connectAttemptTimeMs: [5000, [Validators.required]],
connectAttemptCount: [5], connectAttemptCount: [5, [Validators.required]],
waitAfterFailedAttemptsMs: [300000], waitAfterFailedAttemptsMs: [300000, [Validators.required]],
values: [{}], values: [{}],
security: [{}], security: [{}],
}); });

7
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/modbus/modbus-values/modbus-values.component.scss

@ -13,10 +13,9 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
:host {
.mat-mdc-tab-body-wrapper { :host ::ng-deep .mat-mdc-tab-body-wrapper {
min-height: 320px; min-height: 320px;
}
} }
::ng-deep .mdc-evolution-chip-set__chips { ::ng-deep .mdc-evolution-chip-set__chips {

39
ui-ngx/src/app/modules/home/components/widget/lib/gateway/connectors-configuration/mqtt-basic-config/mqtt-basic-config.component.ts

@ -47,6 +47,7 @@ import {
import { import {
MappingTableComponent MappingTableComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component'; } from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component';
import { isDefinedAndNotNull } from '@core/utils';
@Component({ @Component({
selector: 'tb-mqtt-basic-config', selector: 'tb-mqtt-basic-config',
@ -84,7 +85,7 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator
mappingTypes = MappingType; mappingTypes = MappingType;
basicFormGroup: FormGroup; basicFormGroup: FormGroup;
private onChange: (value: string) => void; private onChange: (value: MQTTBasicConfig) => void;
private onTouched: () => void; private onTouched: () => void;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
@ -100,7 +101,7 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator
this.basicFormGroup.valueChanges this.basicFormGroup.valueChanges
.pipe(takeUntil(this.destroy$)) .pipe(takeUntil(this.destroy$))
.subscribe(value => { .subscribe(value => {
this.onChange(value); this.onChange(this.getMappedMQTTConfig(value));
this.onTouched(); this.onTouched();
}); });
} }
@ -110,7 +111,7 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator
this.destroy$.complete(); this.destroy$.complete();
} }
registerOnChange(fn: (value: string) => void): void { registerOnChange(fn: (value: MQTTBasicConfig) => void): void {
this.onChange = fn; this.onChange = fn;
} }
@ -135,13 +136,30 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator
this.basicFormGroup.setValue(editedBase, {emitEvent: false}); 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 { validate(): ValidationErrors | null {
return this.basicFormGroup.valid ? null : { return this.basicFormGroup.valid ? null : {
basicFormGroup: {valid: false} basicFormGroup: {valid: false}
}; };
} }
private getRequestDataArray(value: Record<RequestType, RequestMappingData>): RequestMappingData[] { private getRequestDataArray(value: Record<RequestType, RequestMappingData[]>): RequestMappingData[] {
const mappingConfigs = []; const mappingConfigs = [];
if (isObject(value)) { if (isObject(value)) {
@ -157,4 +175,17 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator
return mappingConfigs; return mappingConfigs;
} }
private getRequestDataObject(array: RequestMappingData[]): Record<RequestType, RequestMappingData[]> {
return array.reduce((result, { requestType, requestValue }) => {
result[requestType].push(requestValue);
return result;
}, {
connectRequests: [],
disconnectRequests: [],
attributeRequests: [],
attributeUpdates: [],
serverSideRpc: [],
});
}
} }

31
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)]], subCheckPeriodInMillis: [10, [Validators.required, Validators.min(10)]],
showMap: [false, []], showMap: [false, []],
security: [SecurityPolicy.BASIC128, []], security: [SecurityPolicy.BASIC128, []],
identity: [{}, [Validators.required]] identity: []
}); });
this.serverConfigFormGroup.valueChanges.pipe( this.serverConfigFormGroup.valueChanges.pipe(
@ -116,16 +116,25 @@ export class OpcServerConfigComponent implements ControlValueAccessor, Validator
} }
writeValue(serverConfig: ServerConfig): void { writeValue(serverConfig: ServerConfig): void {
const { timeoutInMillis, scanPeriodInMillis, enableSubscriptions, subCheckPeriodInMillis, showMap, security } = serverConfig; const {
const serverConfigState = { timeoutInMillis = 1000,
scanPeriodInMillis = 1000,
enableSubscriptions = true,
subCheckPeriodInMillis = 10,
showMap = false,
security = SecurityPolicy.BASIC128,
identity = {},
} = serverConfig;
this.serverConfigFormGroup.reset({
...serverConfig, ...serverConfig,
timeoutInMillis: timeoutInMillis || 1000, timeoutInMillis,
scanPeriodInMillis: scanPeriodInMillis || 1000, scanPeriodInMillis,
enableSubscriptions: enableSubscriptions || true, enableSubscriptions,
subCheckPeriodInMillis: subCheckPeriodInMillis || 10, subCheckPeriodInMillis,
showMap: showMap || false, showMap,
security: security || SecurityPolicy.BASIC128, security,
}; identity,
this.serverConfigFormGroup.reset(serverConfigState, {emitEvent: false}); }, { emitEvent: false });
} }
} }

77
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 { import {
AddConnectorConfigData, AddConnectorConfigData,
ConnectorBaseConfig, ConnectorBaseConfig,
ConnectorBaseInfo,
ConnectorConfigurationModes, ConnectorConfigurationModes,
ConnectorType, ConnectorType,
GatewayConnector, GatewayConnector,
@ -60,7 +61,7 @@ import {
} from './gateway-widget.models'; } from './gateway-widget.models';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component'; 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 { ErrorStateMatcher } from '@angular/material/core';
import { PageData } from '@shared/models/page/page-data'; import { PageData } from '@shared/models/page/page-data';
@ -133,6 +134,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
private basicConfigSub: Subscription; private basicConfigSub: Subscription;
private jsonConfigSub: Subscription;
private subscriptionOptions: WidgetSubscriptionOptions = { private subscriptionOptions: WidgetSubscriptionOptions = {
callbacks: { callbacks: {
onDataUpdated: () => this.ctx.ngZone.run(() => { 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.sort = this.sort;
this.dataSource.sortingDataAccessor = (data: AttributeData, sortHeaderId: string) => { this.dataSource.sortingDataAccessor = (data: AttributeData, sortHeaderId: string) => {
switch (sortHeaderId) { switch (sortHeaderId) {
@ -275,7 +267,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
} }
saveConnector(): void { 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'; value.configuration = camelCase(value.name) + '.json';
delete value.basicConfig; delete value.basicConfig;
if (value.type !== ConnectorType.GRPC) { 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 { private updateData(reload: boolean = false): void {
this.pageLink.sortOrder.property = this.sort.active; this.pageLink.sortOrder.property = this.sort.active;
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()]; 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 sharedIndex = this.sharedAttributeData.findIndex(data => {
const sharedData = data.value; 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; 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 { private combineData(): void {
this.dataSource.data = [...this.activeData, ...this.inactiveData, ...this.sharedAttributeData].filter((item, index, self) => this.dataSource.data = [...this.activeData, ...this.inactiveData, ...this.sharedAttributeData].filter((item, index, self) =>
index === self.findIndex((t) => t.key === item.key) index === self.findIndex((t) => t.key === item.key)
@ -684,6 +679,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.basicConfigSub.unsubscribe(); this.basicConfigSub.unsubscribe();
} }
this.basicConfigSub = this.connectorForm.get('basicConfig').valueChanges.pipe( this.basicConfigSub = this.connectorForm.get('basicConfig').valueChanges.pipe(
filter(() => !!this.initialConnector),
takeUntil(this.destroy$) takeUntil(this.destroy$)
).subscribe((config) => { ).subscribe((config) => {
const configJson = this.connectorForm.get('configurationJson'); 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<boolean> { private confirmConnectorChange(): Observable<boolean> {
if (this.initialConnector && this.connectorForm.dirty) { if (this.initialConnector && this.connectorForm.dirty) {
return this.dialogService.confirm( return this.dialogService.confirm(
@ -734,19 +746,18 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
case ConnectorType.MQTT: case ConnectorType.MQTT:
case ConnectorType.OPCUA: case ConnectorType.OPCUA:
case ConnectorType.MODBUS: case ConnectorType.MODBUS:
this.connectorForm.get('type').patchValue(connector.type, {emitValue: false, onlySelf: true}); this.connectorForm.get('mode').setValue(connector.mode || ConnectorConfigurationModes.BASIC, {emitEvent: false});
this.connectorForm.get('basicConfig').setValue({}, {emitEvent: false});
setTimeout(() => { setTimeout(() => {
this.connectorForm.patchValue({...connector, mode: connector.mode || ConnectorConfigurationModes.BASIC}); this.connectorForm.patchValue(connector, {emitEvent: false});
this.createBasicConfigWatcher();
this.connectorForm.markAsPristine(); this.connectorForm.markAsPristine();
this.createBasicConfigWatcher();
}); });
break; break;
default: default:
this.connectorForm.patchValue({...connector, mode: null}); this.connectorForm.patchValue({...connector, mode: null});
this.connectorForm.markAsPristine(); this.connectorForm.markAsPristine();
} }
this.createJsonConfigWatcher();
} }
private setClientData(data: PageData<AttributeData>): void { private setClientData(data: PageData<AttributeData>): void {

40
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.html

@ -50,46 +50,6 @@
placeholder="${params}"/> placeholder="${params}"/>
</mat-form-field> </mat-form-field>
</ng-template> </ng-template>
<ng-template [ngSwitchCase]="ConnectorType.MODBUS">
<mat-form-field>
<mat-label>{{ 'gateway.rpc.tag' | translate }}</mat-label>
<input matInput formControlName="tag" placeholder="setValue"/>
</mat-form-field>
<div fxFlex fxLayout="row" fxLayoutGap="10px">
<mat-form-field fxFlex="50" class="mat-block">
<mat-label>{{ 'gateway.rpc.type' | translate }}</mat-label>
<mat-select formControlName="type">
<mat-option *ngFor="let type of modbusCommandTypes" [value]="type">
{{ type }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field fxFlex="50" class="mat-block">
<mat-label>{{ 'gateway.rpc.functionCode' | translate }}</mat-label>
<mat-select formControlName="functionCode">
<mat-option *ngFor="let code of codesArray" [value]="code">
{{ modbusCodesTranslate.get(code) | translate}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<mat-form-field *ngIf="commandForm.get('functionCode').value>4">
<mat-label>{{ 'gateway.rpc.value' | translate }}</mat-label>
<input matInput formControlName="value" placeholder="value"/>
</mat-form-field>
<div fxFlex fxLayout="row" fxLayoutGap="10px">
<mat-form-field fxFlex="50">
<mat-label>{{ 'gateway.rpc.address' | translate }}</mat-label>
<input matInput formControlName="address" type="number" min="0"
placeholder="1" step="1"/>
</mat-form-field>
<mat-form-field fxFlex="50">
<mat-label>{{ 'gateway.rpc.objectsCount' | translate }}</mat-label>
<input matInput formControlName="objectsCount" type="number" min="0"
placeholder="31" step="1"/>
</mat-form-field>
</div>
</ng-template>
<ng-template [ngSwitchCase]="ConnectorType.BACNET"> <ng-template [ngSwitchCase]="ConnectorType.BACNET">
<mat-form-field> <mat-form-field>
<mat-label>{{ 'gateway.rpc.methodRPC' | translate }}</mat-label> <mat-label>{{ 'gateway.rpc.methodRPC' | translate }}</mat-label>

27
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc-connector.component.ts

@ -35,8 +35,6 @@ import {
ConnectorType, ConnectorType,
GatewayConnectorDefaultTypesTranslatesMap, GatewayConnectorDefaultTypesTranslatesMap,
HTTPMethods, HTTPMethods,
ModbusCodesTranslate,
ModbusCommandTypes,
noLeadTrailSpacesRegex, noLeadTrailSpacesRegex,
RPCCommand, RPCCommand,
RPCTemplateConfig, RPCTemplateConfig,
@ -53,7 +51,7 @@ import {
} from '@shared/components/dialog/json-object-edit-dialog.component'; } from '@shared/components/dialog/json-object-edit-dialog.component';
import { jsonRequired } from '@shared/components/json-object-edit.component'; import { jsonRequired } from '@shared/components/json-object-edit.component';
import { deepClone } from '@core/utils'; import { deepClone } from '@core/utils';
import { filter, takeUntil, tap } from "rxjs/operators"; import { takeUntil, tap } from "rxjs/operators";
import { Subject } from "rxjs"; import { Subject } from "rxjs";
@Component({ @Component({
@ -80,9 +78,7 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
saveTemplate: EventEmitter<RPCTemplateConfig> = new EventEmitter(); saveTemplate: EventEmitter<RPCTemplateConfig> = new EventEmitter();
commandForm: FormGroup; commandForm: FormGroup;
codesArray: Array<number> = [1, 2, 3, 4, 5, 6, 15, 16];
ConnectorType = ConnectorType; ConnectorType = ConnectorType;
modbusCommandTypes = Object.values(ModbusCommandTypes) as ModbusCommandTypes[];
bACnetRequestTypes = Object.values(BACnetRequestTypes) as BACnetRequestTypes[]; bACnetRequestTypes = Object.values(BACnetRequestTypes) as BACnetRequestTypes[];
bACnetObjectTypes = Object.values(BACnetObjectTypes) as BACnetObjectTypes[]; bACnetObjectTypes = Object.values(BACnetObjectTypes) as BACnetObjectTypes[];
bLEMethods = Object.values(BLEMethods) as BLEMethods[]; bLEMethods = Object.values(BLEMethods) as BLEMethods[];
@ -98,7 +94,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
SocketMethodProcessingsTranslates = SocketMethodProcessingsTranslates; SocketMethodProcessingsTranslates = SocketMethodProcessingsTranslates;
SNMPMethodsTranslations = SNMPMethodsTranslations; SNMPMethodsTranslations = SNMPMethodsTranslations;
gatewayConnectorDefaultTypesTranslates = GatewayConnectorDefaultTypesTranslatesMap; gatewayConnectorDefaultTypesTranslates = GatewayConnectorDefaultTypesTranslatesMap;
modbusCodesTranslate = ModbusCodesTranslate;
urlPattern = /^[-a-zA-Zd_$:{}?~+=\/.0-9-]*$/; urlPattern = /^[-a-zA-Zd_$:{}?~+=\/.0-9-]*$/;
numbersOnlyPattern = /^[0-9]*$/; numbersOnlyPattern = /^[0-9]*$/;
@ -156,26 +151,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
withResponse: [false, []], withResponse: [false, []],
}); });
break; 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: case ConnectorType.BACNET:
formGroup = this.fb.group({ formGroup = this.fb.group({
method: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]], method: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],

28
ui-ngx/src/app/modules/home/components/widget/lib/gateway/gateway-service-rpc.component.html

@ -41,8 +41,32 @@
</button> </button>
</ng-container> </ng-container>
<ng-template #connectorForm> <ng-template #connectorForm>
<tb-gateway-service-rpc-connector formControlName="params" [connectorType]="connectorType" <tb-gateway-service-rpc-connector
(sendCommand)="sendCommand()" (saveTemplate)="saveTemplate()"/> *ngIf="connectorType !== ConnectorType.MODBUS else modbusParameters"
formControlName="params"
[connectorType]="connectorType"
(sendCommand)="sendCommand()"
(saveTemplate)="saveTemplate()"
/>
<ng-template #modbusParameters>
<div fxLayout="column" class="rpc-parameters">
<div class="mat-subtitle-1 tb-form-panel-title">{{ 'gateway.rpc.title' | translate: {type: gatewayConnectorDefaultTypesTranslates.get(connectorType)} }}</div>
<tb-modbus-rpc-parameters formControlName="params"/>
<div class="template-actions" fxFlex fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="10px">
<button mat-raised-button
(click)="saveTemplate()"
[disabled]="commandForm.get('params').invalid">
{{ 'gateway.rpc-command-save-template' | translate }}
</button>
<button mat-raised-button
color="primary"
(click)="sendCommand()"
[disabled]="commandForm.get('params').invalid">
{{ 'gateway.rpc-command-send' | translate }}
</button>
</div>
</div>
</ng-template>
</ng-template> </ng-template>
</div> </div>
<section class="result-block" [formGroup]="commandForm"> <section class="result-block" [formGroup]="commandForm">

4
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 { .result-block {
padding: 0 5px; padding: 0 5px;
display: flex; display: flex;

4
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 { jsonRequired } from '@shared/components/json-object-edit.component';
import { import {
ConnectorType, ConnectorType,
GatewayConnectorDefaultTypesTranslatesMap,
RPCCommand, RPCCommand,
RPCTemplate, RPCTemplate,
RPCTemplateConfig, RPCTemplateConfig,
@ -71,6 +72,9 @@ export class GatewayServiceRPCComponent implements OnInit {
public connectorType: ConnectorType; public connectorType: ConnectorType;
public templates: Array<RPCTemplate> = []; public templates: Array<RPCTemplate> = [];
readonly ConnectorType = ConnectorType;
readonly gatewayConnectorDefaultTypesTranslates = GatewayConnectorDefaultTypesTranslatesMap;
private subscription: IWidgetSubscription; private subscription: IWidgetSubscription;
private subscriptionOptions: WidgetSubscriptionOptions = { private subscriptionOptions: WidgetSubscriptionOptions = {
callbacks: { callbacks: {

69
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 { Observable } from 'rxjs';
import { ValueTypeData } from '@shared/models/constants'; import { ValueTypeData } from '@shared/models/constants';
export const noLeadTrailSpacesRegex = /^(?! )[\S\s]*(?<! )$/; export const noLeadTrailSpacesRegex = /^\S+(?: \S+)*$/;
export enum StorageTypes { export enum StorageTypes {
MEMORY = 'memory', MEMORY = 'memory',
@ -170,19 +170,27 @@ export interface ConnectorSecurity {
pathToCACert?: string; pathToCACert?: string;
pathToPrivateKey?: string; pathToPrivateKey?: string;
pathToClientCert?: string; pathToClientCert?: string;
mode?: ModeType;
} }
export type ConnectorMapping = DeviceConnectorMapping | RequestMappingData | ConverterConnectorMapping; export type ConnectorMapping = DeviceConnectorMapping | RequestMappingData | ConverterConnectorMapping;
export type ConnectorMappingFormValue = DeviceConnectorMapping | RequestMappingFormValue | ConverterMappingFormValue; export type ConnectorMappingFormValue = DeviceConnectorMapping | RequestMappingFormValue | ConverterMappingFormValue;
export type ConnectorBaseConfig = MQTTBasicConfig | OPCBasicConfig | ModbusBasicConfig; export type ConnectorBaseConfig = ConnectorBaseInfo | MQTTBasicConfig | OPCBasicConfig | ModbusBasicConfig;
export interface ConnectorBaseInfo {
name: string;
id: string;
enableRemoteLogging: boolean;
logLevel: GatewayLogLevel;
}
export interface MQTTBasicConfig { export interface MQTTBasicConfig {
dataMapping: ConverterConnectorMapping[]; dataMapping: ConverterConnectorMapping[];
requestsMapping: Record<RequestType, RequestMappingData> | RequestMappingData[]; requestsMapping: Record<RequestType, RequestMappingData[]> | RequestMappingData[];
broker: BrokerConfig; broker: BrokerConfig;
workers: WorkersConfig; workers?: WorkersConfig;
} }
export interface OPCBasicConfig { export interface OPCBasicConfig {
@ -311,35 +319,15 @@ export interface RPCCommand {
time: number; time: number;
} }
export const ModbusFunctionCodeTranslationsMap = new Map<number, string>([
export enum ModbusCommandTypes { [1, 'gateway.function-codes.read-coils'],
Bits = 'bits', [2, 'gateway.function-codes.read-discrete-inputs'],
Bit = 'bit', [3, 'gateway.function-codes.read-multiple-holding-registers'],
// eslint-disable-next-line id-blacklist [4, 'gateway.function-codes.read-input-registers'],
String = 'string', [5, 'gateway.function-codes.write-single-coil'],
Bytes = 'bytes', [6, 'gateway.function-codes.write-single-holding-register'],
Int8 = '8int', [15, 'gateway.function-codes.write-multiple-coils'],
Uint8 = '8uint', [16, 'gateway.function-codes.write-multiple-holding-registers']
Int16 = '16int',
Uint16 = '16uint',
Float16 = '16float',
Int32 = '32int',
Uint32 = '32uint',
Float32 = '32float',
Int64 = '64int',
Uint64 = '64uint',
Float64 = '64float'
}
export const ModbusCodesTranslate = new Map<number, string>([
[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 enum BACnetRequestTypes { export enum BACnetRequestTypes {
@ -862,6 +850,8 @@ export enum ModbusDataType {
FLOAT64 = '64float' FLOAT64 = '64float'
} }
export const ModbusEditableDataTypes = [ModbusDataType.BYTES, ModbusDataType.BITS, ModbusDataType.STRING];
export enum ModbusObjectCountByDataType { export enum ModbusObjectCountByDataType {
'8int' = 1, '8int' = 1,
'8uint' = 1, '8uint' = 1,
@ -920,19 +910,6 @@ export const ModbusKeysNoKeysTextTranslationsMap = new Map<ModbusValueKey, strin
] ]
); );
export const ModbusFunctionCodeTranslationsMap = new Map<number, string>(
[
[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 { export interface ModbusMasterConfig {
slaves: SlaveConfig[]; slaves: SlaveConfig[];
} }

4
ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts

@ -141,6 +141,9 @@ import {
import { import {
TypeValuePanelComponent TypeValuePanelComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/type-value-panel/type-value-panel.component'; } 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'; import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/scada-symbol-widget.component';
@NgModule({ @NgModule({
@ -229,6 +232,7 @@ import { ScadaSymbolWidgetComponent } from '@home/components/widget/lib/scada/sc
KeyValueIsNotEmptyPipe, KeyValueIsNotEmptyPipe,
ModbusBasicConfigComponent, ModbusBasicConfigComponent,
EllipsisChipListDirective, EllipsisChipListDirective,
ModbusRpcParametersComponent,
], ],
exports: [ exports: [
EntitiesTableWidgetComponent, EntitiesTableWidgetComponent,

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

2
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 { private showTooltip(): void {
this.tooltip.message = this.text; this.tooltip.message = this.text;
this.renderer.setAttribute(this.elementRef.nativeElement, 'matTooltip', this.text);
this.tooltip.show(); this.tooltip.show();
} }

2
ui-ngx/src/app/shared/models/audit-log.models.ts

@ -48,6 +48,7 @@ export enum ActionType {
ALARM_ACK = 'ALARM_ACK', ALARM_ACK = 'ALARM_ACK',
ALARM_CLEAR = 'ALARM_CLEAR', ALARM_CLEAR = 'ALARM_CLEAR',
ALARM_ASSIGNED = 'ALARM_ASSIGNED', ALARM_ASSIGNED = 'ALARM_ASSIGNED',
ALARM_DELETE = 'ALARM_DELETE',
ALARM_UNASSIGNED = 'ALARM_UNASSIGNED', ALARM_UNASSIGNED = 'ALARM_UNASSIGNED',
ADDED_COMMENT = 'ADDED_COMMENT', ADDED_COMMENT = 'ADDED_COMMENT',
UPDATED_COMMENT = 'UPDATED_COMMENT', UPDATED_COMMENT = 'UPDATED_COMMENT',
@ -91,6 +92,7 @@ export const actionTypeTranslations = new Map<ActionType, string>(
[ActionType.RELATIONS_DELETED, 'audit-log.type-relations-delete'], [ActionType.RELATIONS_DELETED, 'audit-log.type-relations-delete'],
[ActionType.ALARM_ACK, 'audit-log.type-alarm-ack'], [ActionType.ALARM_ACK, 'audit-log.type-alarm-ack'],
[ActionType.ALARM_CLEAR, 'audit-log.type-alarm-clear'], [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_ASSIGNED, 'audit-log.type-alarm-assign'],
[ActionType.ALARM_UNASSIGNED, 'audit-log.type-alarm-unassign'], [ActionType.ALARM_UNASSIGNED, 'audit-log.type-alarm-unassign'],
[ActionType.ADDED_COMMENT, 'audit-log.type-added-comment'], [ActionType.ADDED_COMMENT, 'audit-log.type-added-comment'],

1
ui-ngx/src/app/shared/public-api.ts

@ -19,3 +19,4 @@ export * from './decorators/public-api';
export * from './models/public-api'; export * from './models/public-api';
export * from './pipe/public-api'; export * from './pipe/public-api';
export * from './shared.module'; export * from './shared.module';
export * from './directives/public-api';

26
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -936,6 +936,7 @@
"type-relations-delete": "All relation deleted", "type-relations-delete": "All relation deleted",
"type-alarm-ack": "Acknowledged", "type-alarm-ack": "Acknowledged",
"type-alarm-clear": "Cleared", "type-alarm-clear": "Cleared",
"type-alarm-delete": "Deleted",
"type-alarm-assign": "Assigned", "type-alarm-assign": "Assigned",
"type-alarm-unassign": "Unassigned", "type-alarm-unassign": "Unassigned",
"type-added-comment": "Added comment", "type-added-comment": "Added comment",
@ -2923,6 +2924,16 @@
"from-device-request-settings": "Input request parsing", "from-device-request-settings": "Input request parsing",
"from-device-request-settings-hint": "These fields support JSONPath expressions to extract a name from incoming message.", "from-device-request-settings-hint": "These fields support JSONPath expressions to extract a name from incoming message.",
"function-code": "Function code", "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": "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}", "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", "gateway": "Gateway",
@ -3129,14 +3140,6 @@
"template-name-duplicate": "Template with such name already exists, it will be updated.", "template-name-duplicate": "Template with such name already exists, it will be updated.",
"command": "Command", "command": "Command",
"params": "Params", "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" "json-value-invalid": "JSON value has an invalid format"
}, },
"rpc-methods": "RPC methods", "rpc-methods": "RPC methods",
@ -3326,16 +3329,13 @@
"username": "Username", "username": "Username",
"username-required": "Username is required.", "username-required": "Username is required.",
"unit-id-required": "Unit ID 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-coil": "Write Coil",
"write-coils": "Write Coils", "write-coils": "Write Coils",
"write-register": "Write Register", "write-register": "Write Register",
"write-registers": "Write Registers", "write-registers": "Write Registers",
"hints": { "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-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", "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", "host": "Hostname or IP address of platform server",

Loading…
Cancel
Save