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()) {
response.setResult(new ResponseEntity<>(HttpStatus.OK));
} else {
response.setErrorResult(new ThingsboardException("Edge is not connected", ThingsboardErrorCode.GENERAL));
response.setErrorResult(new ThingsboardException(fromEdgeSyncResponse.getError(), ThingsboardErrorCode.GENERAL));
}
}

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

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());
} else {
log.info("[{}][{}] sync process completed", this.tenantId, edge.getId());
DownlinkMsg syncCompleteDownlinkMsg = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.setSyncCompletedMsg(SyncCompletedMsg.newBuilder().build())
@ -325,7 +326,11 @@ public final class EdgeGrpcSession implements Closeable {
}
private void sendDownlinkMsg(ResponseMsg downlinkMsg) {
log.trace("[{}][{}] Sending downlink msg [{}]", this.tenantId, this.sessionId, downlinkMsg);
if (downlinkMsg.getDownlinkMsg().getWidgetTypeUpdateMsgCount() > 0) {
log.trace("[{}][{}] Sending downlink widgetTypeUpdateMsg, downlinkMsgId = {}", this.tenantId, this.sessionId, downlinkMsg.getDownlinkMsg().getDownlinkMsgId());
} else {
log.trace("[{}][{}] Sending downlink msg [{}]", this.tenantId, this.sessionId, downlinkMsg);
}
if (isConnected()) {
downlinkMsgLock.lock();
try {
@ -337,7 +342,7 @@ public final class EdgeGrpcSession implements Closeable {
} finally {
downlinkMsgLock.unlock();
}
log.trace("[{}][{}] Response msg successfully sent [{}]", this.tenantId, this.sessionId, downlinkMsg);
log.trace("[{}][{}] Response msg successfully sent. downlinkMsgId = {}", this.tenantId, this.sessionId, downlinkMsg.getDownlinkMsg().getDownlinkMsgId());
}
}
@ -551,9 +556,13 @@ public final class EdgeGrpcSession implements Closeable {
DownlinkMsg downlinkMsg = null;
try {
switch (edgeEvent.getAction()) {
case UPDATED, ADDED, DELETED, ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE, ALARM_ACK, ALARM_CLEAR, ALARM_DELETE, CREDENTIALS_UPDATED, RELATION_ADD_OR_UPDATE, RELATION_DELETED, CREDENTIALS_REQUEST, RPC_CALL, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, ADDED_COMMENT, UPDATED_COMMENT, DELETED_COMMENT -> {
case UPDATED, ADDED, DELETED, ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE, ALARM_ACK, ALARM_CLEAR, ALARM_DELETE, CREDENTIALS_UPDATED, RELATION_ADD_OR_UPDATE, RELATION_DELETED, RPC_CALL, ASSIGNED_TO_CUSTOMER, UNASSIGNED_FROM_CUSTOMER, ADDED_COMMENT, UPDATED_COMMENT, DELETED_COMMENT -> {
downlinkMsg = convertEntityEventToDownlink(edgeEvent);
log.trace("[{}][{}] entity message processed [{}]", this.tenantId, this.sessionId, downlinkMsg);
if (downlinkMsg != null && downlinkMsg.getWidgetTypeUpdateMsgCount() > 0) {
log.trace("[{}][{}] widgetTypeUpdateMsg message processed, downlinkMsgId = {}", this.tenantId, this.sessionId, downlinkMsg.getDownlinkMsgId());
} else {
log.trace("[{}][{}] entity message processed [{}]", this.tenantId, this.sessionId, downlinkMsg);
}
}
case ATTRIBUTES_UPDATED, POST_ATTRIBUTES, ATTRIBUTES_DELETED, TIMESERIES_UPDATED ->
downlinkMsg = ctx.getTelemetryProcessor().convertTelemetryEventToDownlink(edge, edgeEvent);

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

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

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,
EdgeEventActionType action) {
return switch (action) {
case TIMESERIES_UPDATED, ALARM_ACK, ALARM_CLEAR, ALARM_ASSIGNED, ALARM_UNASSIGNED, CREDENTIALS_REQUEST, ADDED_COMMENT, UPDATED_COMMENT ->
case TIMESERIES_UPDATED, ALARM_ACK, ALARM_CLEAR, ALARM_ASSIGNED, ALARM_UNASSIGNED, ADDED_COMMENT, UPDATED_COMMENT ->
true;
default -> switch (type) {
case ALARM, ALARM_COMMENT, RULE_CHAIN, RULE_CHAIN_METADATA, USER, CUSTOMER, TENANT, TENANT_PROFILE, WIDGETS_BUNDLE, WIDGET_TYPE,

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.FromDeviceRpcResponseActorMsg;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceRpcCallMsg;
import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg;
@ -70,7 +69,7 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements
case ENTITY_CREATED_RPC_MESSAGE:
case ENTITY_UPDATED_RPC_MESSAGE:
saveOrUpdateDevice(tenantId, deviceId, deviceUpdateMsg, edge);
return saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.DEVICE, EdgeEventActionType.CREDENTIALS_REQUEST, deviceId, null);
return Futures.immediateFuture(null);
case ENTITY_DELETED_RPC_MESSAGE:
Device deviceToDelete = deviceService.findDeviceById(tenantId, deviceId);
if (deviceToDelete != null) {
@ -232,6 +231,12 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements
DownlinkMsg.Builder builder = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addDeviceUpdateMsg(deviceUpdateMsg);
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(edgeEvent.getTenantId(), deviceId);
if (deviceCredentials != null) {
DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg = ((DeviceMsgConstructor)
deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructDeviceCredentialsUpdatedMsg(deviceCredentials);
builder.addDeviceCredentialsUpdateMsg(deviceCredentialsUpdateMsg).build();
}
if (UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE.equals(msgType)) {
DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(edgeEvent.getTenantId(), device.getDeviceProfileId());
deviceProfile = checkIfDeviceProfileDefaultFieldsAssignedToEdge(edgeEvent.getTenantId(), edgeId, deviceProfile, edgeVersion);
@ -269,22 +274,7 @@ public abstract class DeviceEdgeProcessor extends BaseDeviceProcessor implements
deviceMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion))
.constructDeviceRpcCallMsg(edgeEvent.getEntityId(), edgeEvent.getBody()))
.build();
case CREDENTIALS_REQUEST:
return convertCredentialsRequestEventToDownlink(edgeEvent);
}
return downlinkMsg;
}
private DownlinkMsg convertCredentialsRequestEventToDownlink(EdgeEvent edgeEvent) {
DeviceId deviceId = new DeviceId(edgeEvent.getEntityId());
DeviceCredentialsRequestMsg deviceCredentialsRequestMsg = DeviceCredentialsRequestMsg.newBuilder()
.setDeviceIdMSB(deviceId.getId().getMostSignificantBits())
.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits())
.build();
DownlinkMsg.Builder builder = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addDeviceCredentialsRequestMsg(deviceCredentialsRequestMsg);
return builder.build();
}
}

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)
ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion))
.constructRuleChainUpdatedMsg(msgType, ruleChain, isRoot);
downlinkMsg = DownlinkMsg.newBuilder()
DownlinkMsg.Builder builder = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addRuleChainUpdateMsg(ruleChainUpdateMsg)
.build();
.addRuleChainUpdateMsg(ruleChainUpdateMsg);
RuleChainMetaData ruleChainMetaData = ruleChainService.loadRuleChainMetaData(edgeEvent.getTenantId(), ruleChainId);
RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = ((RuleChainMsgConstructor)
ruleChainMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion))
.constructRuleChainMetadataUpdatedMsg(edgeEvent.getTenantId(), msgType, ruleChainMetaData, edgeVersion);
if (ruleChainMetadataUpdateMsg != null) {
builder.addRuleChainMetadataUpdateMsg(ruleChainMetadataUpdateMsg);
}
downlinkMsg = builder.build();
}
}
case DELETED, UNASSIGNED_FROM_EDGE -> downlinkMsg = DownlinkMsg.newBuilder()

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);
if (user != null) {
UpdateMsgType msgType = getUpdateMsgType(edgeEvent.getAction());
downlinkMsg = DownlinkMsg.newBuilder()
DownlinkMsg.Builder builder = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addUserUpdateMsg(((UserMsgConstructor) userMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructUserUpdatedMsg(msgType, user))
.build();
.addUserUpdateMsg(((UserMsgConstructor) userMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructUserUpdatedMsg(msgType, user));
UserCredentials userCredentialsByUserId = userService.findUserCredentialsByUserId(edgeEvent.getTenantId(), userId);
if (userCredentialsByUserId != null && userCredentialsByUserId.isEnabled()) {
UserCredentialsUpdateMsg userCredentialsUpdateMsg =
((UserMsgConstructor) userMsgConstructorFactory.getMsgConstructorByEdgeVersion(edgeVersion)).constructUserCredentialsUpdatedMsg(userCredentialsByUserId);
builder.addUserCredentialsUpdateMsg(userCredentialsUpdateMsg);
}
downlinkMsg = builder.build();
}
}
case DELETED -> downlinkMsg = DownlinkMsg.newBuilder()

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

@ -82,8 +82,6 @@ import java.util.UUID;
@Slf4j
public class DefaultEdgeRequestsService implements EdgeRequestsService {
private static final int DEFAULT_PAGE_SIZE = 1000;
@Autowired
private EdgeEventService edgeEventService;
@ -142,8 +140,10 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
private ListenableFuture<Void> processEntityAttributesAndAddToEdgeQueue(TenantId tenantId, EntityId entityId, Edge edge,
EdgeEventType entityType, String scope, List<AttributeKvEntry> ssAttributes,
AttributesRequestMsg attributesRequestMsg) {
Map<String, Object> entityData = null;
ObjectNode attributes = null;
ListenableFuture<Void> future;
try {
ListenableFuture<Void> future;
if (ssAttributes == null || ssAttributes.isEmpty()) {
log.trace("[{}][{}] No attributes found for entity {} [{}]", tenantId,
edge.getName(),
@ -151,8 +151,8 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
entityId.getId());
future = Futures.immediateFuture(null);
} else {
Map<String, Object> entityData = new HashMap<>();
ObjectNode attributes = JacksonUtil.newObjectNode();
entityData = new HashMap<>();
attributes = JacksonUtil.newObjectNode();
for (AttributeKvEntry attr : ssAttributes) {
if (DefaultDeviceStateService.PERSISTENT_ATTRIBUTES.contains(attr.getKey())
&& !DefaultDeviceStateService.INACTIVITY_TIMEOUT.equals(attr.getKey())) {
@ -170,7 +170,7 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
attributes.put(attr.getKey(), attr.getValueAsString());
}
}
if (attributes.size() > 0) {
if (!attributes.isEmpty()) {
entityData.put("kv", attributes);
entityData.put("scope", scope);
JsonNode body = JacksonUtil.valueToTree(entityData);
@ -182,12 +182,13 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
}
return Futures.transformAsync(future, v -> processLatestTimeseriesAndAddToEdgeQueue(tenantId, entityId, edge, entityType), dbCallbackExecutorService);
} catch (Exception e) {
String errMsg = String.format("[%s][%s] Failed to save attribute updates to the edge [%s]", tenantId, edge.getId(), attributesRequestMsg);
String errMsg = String.format("[%s][%s] Failed to save attribute updates to the edge [%s], scope = %s, entityData = %s, attributes = %s",
tenantId, edge.getId(), attributesRequestMsg, scope, entityData, attributes);
log.error(errMsg, e);
return Futures.immediateFailedFuture(new RuntimeException(errMsg, e));
}
}
private ListenableFuture<Void> processLatestTimeseriesAndAddToEdgeQueue(TenantId tenantId, EntityId entityId, Edge edge,
EdgeEventType entityType) {
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.EntityKeyType;
import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.entity.EntityService;
@ -355,6 +356,9 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
private void handleWsCmdRuntimeException(String sessionId, RuntimeException e, EntityDataCmd cmd) {
log.debug("[{}] Failed to process ws cmd: {}", sessionId, cmd, e);
if (e instanceof TbRateLimitsException) {
return;
}
wsService.close(sessionId, CloseStatus.SERVICE_RESTARTED);
}

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.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.DeduplicationUtil;
import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.EntityType;
@ -36,10 +39,12 @@ import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.gen.transport.TransportProtos;
@ -48,6 +53,7 @@ import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.discovery.event.ClusterTopologyChangeEvent;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.ws.WebSocketService;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate;
import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate;
import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate;
@ -88,13 +94,20 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
private final TbClusterService clusterService;
private final SubscriptionManagerService subscriptionManagerService;
private final WebSocketService webSocketService;
private final RateLimitService rateLimitService;
private ExecutorService tsCallBackExecutor;
private ScheduledExecutorService staleSessionCleanupExecutor;
@Value("${server.ws.rate_limits.subscriptions_per_tenant:2000:60}")
private String subscriptionsPerTenantRateLimit;
@Value("${server.ws.rate_limits.subscriptions_per_user:500:60}")
private String subscriptionsPerUserRateLimit;
public DefaultTbLocalSubscriptionService(AttributesService attrService, TimeseriesService tsService, TbServiceInfoProvider serviceInfoProvider,
PartitionService partitionService, TbClusterService clusterService,
@Lazy SubscriptionManagerService subscriptionManagerService, @Lazy WebSocketService webSocketService) {
@Lazy SubscriptionManagerService subscriptionManagerService, @Lazy WebSocketService webSocketService,
RateLimitService rateLimitService) {
this.attrService = attrService;
this.tsService = tsService;
this.serviceInfoProvider = serviceInfoProvider;
@ -102,6 +115,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
this.clusterService = clusterService;
this.subscriptionManagerService = subscriptionManagerService;
this.webSocketService = webSocketService;
this.rateLimitService = rateLimitService;
}
private String serviceId;
@ -185,9 +199,18 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
}
@Override
public void addSubscription(TbSubscription<?> subscription) {
public void addSubscription(TbSubscription<?> subscription, WebSocketSessionRef sessionRef) {
TenantId tenantId = subscription.getTenantId();
EntityId entityId = subscription.getEntityId();
if (!rateLimitService.checkRateLimit(LimitedApi.WS_SUBSCRIPTIONS, (Object) tenantId, subscriptionsPerTenantRateLimit)) {
handleRateLimitError(subscription, sessionRef, "Exceeded rate limit for WS subscriptions per tenant");
return;
}
if (sessionRef.getSecurityCtx() != null && !rateLimitService.checkRateLimit(LimitedApi.WS_SUBSCRIPTIONS, sessionRef.getSecurityCtx().getId(), subscriptionsPerUserRateLimit)) {
handleRateLimitError(subscription, sessionRef, "Exceeded rate limit for WS subscriptions per user");
return;
}
log.debug("[{}][{}] Register subscription: {}", tenantId, entityId, subscription);
SubscriptionModificationResult result;
subsLock.lock();
@ -584,4 +607,13 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
subscriptionsBySessionId.keySet().forEach(webSocketService::cleanupIfStale);
}
private void handleRateLimitError(TbSubscription<?> subscription, WebSocketSessionRef sessionRef, String message) {
String deduplicationKey = sessionRef.getSessionId() + message;
if (!DeduplicationUtil.alreadyProcessed(deduplicationKey, TimeUnit.SECONDS.toMillis(15))) {
log.info("{} {}", sessionRef, message);
webSocketService.sendError(sessionRef, subscription.getSubscriptionId(), SubscriptionErrorCode.BAD_REQUEST, message);
}
throw new TbRateLimitsException(message);
}
}

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();
subToEntityIdMap.put(subIdx, entityData.getEntityId());
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);
for (EntityData entityData : data.getData()) {
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);
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)
.build();
subToDynamicValueKeySet.add(subIdx);
localSubscriptionService.addSubscription(sub);
localSubscriptionService.addSubscription(sub, sessionRef);
}
} catch (InterruptedException | ExecutionException e) {
log.info("[{}][{}][{}] Failed to resolve dynamic values: {}", tenantId, customerId, userId, dynamicValues.keySet());

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))
.ts(startTs)
.build();
localSubscriptionService.addSubscription(subscription);
localSubscriptionService.addSubscription(subscription, sessionRef);
}
@Override
@ -342,7 +342,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx<AlarmDataQuery> {
newSubsList.forEach(entity -> createAlarmSubscriptionForEntity(query.getPageLink(), startTs, entity));
}
subIdsToCancel.forEach(subId -> localSubscriptionService.cancelSubscription(getSessionId(), subId));
subsToAdd.forEach(localSubscriptionService::addSubscription);
subsToAdd.forEach(subscription -> localSubscriptionService.addSubscription(subscription, sessionRef));
}
private void resetInvocationCounter() {
@ -361,4 +361,5 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx<AlarmDataQuery> {
EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder);
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));
subsToAdd.forEach(localSubscriptionService::addSubscription);
subsToAdd.forEach(subscription -> localSubscriptionService.addSubscription(subscription, sessionRef));
sendWsMsg(new EntityDataUpdate(cmdId, data, null, maxEntitiesPerDataSubscription));
}
@ -239,4 +239,5 @@ public class TbEntityDataSubCtx extends TbAbstractDataSubCtx<EntityDataQuery> {
protected EntityDataQuery buildEntityDataQuery() {
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.gen.transport.TransportProtos;
import org.thingsboard.server.queue.discovery.event.ClusterTopologyChangeEvent;
import org.thingsboard.server.service.ws.WebSocketSessionRef;
import org.thingsboard.server.service.ws.notification.sub.NotificationRequestUpdate;
import org.thingsboard.server.service.ws.notification.sub.NotificationsSubscriptionUpdate;
@ -29,7 +30,7 @@ import java.util.List;
public interface TbLocalSubscriptionService {
void addSubscription(TbSubscription<?> subscription);
void addSubscription(TbSubscription<?> subscription, WebSocketSessionRef sessionRef);
void onSubEventCallback(TransportProtos.TbEntitySubEventCallbackProto subEventCallback, TbCallback callback);

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

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

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}"
# Maximum time between WS session opening and sending auth command
auth_timeout_ms: "${TB_SERVER_WS_AUTH_TIMEOUT_MS:10000}"
rate_limits:
# Per-tenant rate limit for WS subscriptions
subscriptions_per_tenant: "${TB_SERVER_WS_SUBSCRIPTIONS_PER_TENANT_RATE_LIMIT:2000:60}"
# Per-user rate limit for WS subscriptions
subscriptions_per_user: "${TB_SERVER_WS_SUBSCRIPTIONS_PER_USER_RATE_LIMIT:500:60}"
rest:
server_side_rpc:
# Minimum value of the server-side RPC timeout. May override value provided in the REST API call.

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.edge.Edge;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.model.JwtSettings;
import org.thingsboard.server.dao.edge.EdgeDao;
import org.thingsboard.server.dao.exception.DataValidationException;
@ -73,11 +77,13 @@ import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.EdgeVersion;
import org.thingsboard.server.gen.edge.v1.OAuth2UpdateMsg;
import org.thingsboard.server.gen.edge.v1.QueueUpdateMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainMetadataUpdateMsg;
import org.thingsboard.server.gen.edge.v1.RuleChainUpdateMsg;
import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg;
import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg;
@ -93,6 +99,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.containsString;
@ -887,23 +894,24 @@ public class EdgeControllerTest extends AbstractControllerTest {
edgeImitator.ignoreType(UserCredentialsUpdateMsg.class);
edgeImitator.ignoreType(OAuth2UpdateMsg.class);
edgeImitator.expectMessageAmount(24);
edgeImitator.expectMessageAmount(27);
edgeImitator.connect();
waitForMessages(edgeImitator);
verifyFetchersMsgs(edgeImitator);
verifyFetchersMsgs(edgeImitator, savedDevice);
// verify queue msgs
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popDeviceMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Device 1"));
Assert.assertTrue(popDeviceCredentialsMsg(edgeImitator.getDownlinkMsgs(), savedDevice.getId()));
Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "test"));
Assert.assertTrue(popAssetMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Asset 1"));
Assert.assertTrue(edgeImitator.getDownlinkMsgs().isEmpty());
edgeImitator.expectMessageAmount(20);
edgeImitator.expectMessageAmount(22);
doPost("/api/edge/sync/" + edge.getId());
waitForMessages(edgeImitator);
verifyFetchersMsgs(edgeImitator);
verifyFetchersMsgs(edgeImitator, savedDevice);
Assert.assertTrue(edgeImitator.getDownlinkMsgs().isEmpty());
edgeImitator.allowIgnoredTypes();
@ -920,6 +928,23 @@ public class EdgeControllerTest extends AbstractControllerTest {
.andExpect(status().isOk());
}
private RuleChainId getEdgeRootRuleChainId(EdgeImitator edgeImitator) {
try {
EdgeId edgeId = new EdgeId(new UUID(edgeImitator.getConfiguration().getEdgeIdMSB(), edgeImitator.getConfiguration().getEdgeIdLSB()));
List<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 {
ObjectNode attributes = JacksonUtil.newObjectNode();
attributes.put("active", true);
@ -949,9 +974,10 @@ public class EdgeControllerTest extends AbstractControllerTest {
}
}
private void verifyFetchersMsgs(EdgeImitator edgeImitator) {
private void verifyFetchersMsgs(EdgeImitator edgeImitator, Device savedDevice) {
Assert.assertTrue(popQueueMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Main"));
Assert.assertTrue(popRuleChainMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Edge Root Rule Chain"));
Assert.assertTrue(popRuleChainMetadataMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, getEdgeRootRuleChainId(edgeImitator)));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "general"));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "mail"));
Assert.assertTrue(popAdminSettingsMsg(edgeImitator.getDownlinkMsgs(), "connectivity"));
@ -965,6 +991,7 @@ public class EdgeControllerTest extends AbstractControllerTest {
Assert.assertTrue(popCustomerMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Public"));
Assert.assertTrue(popDeviceProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "default"));
Assert.assertTrue(popDeviceMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Device 1"));
Assert.assertTrue(popDeviceCredentialsMsg(edgeImitator.getDownlinkMsgs(), savedDevice.getId()));
Assert.assertTrue(popAssetProfileMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "test"));
Assert.assertTrue(popAssetMsg(edgeImitator.getDownlinkMsgs(), UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, "Test Sync Edge Asset 1"));
Assert.assertTrue(popTenantMsg(edgeImitator.getDownlinkMsgs(), tenantId));
@ -1002,6 +1029,21 @@ public class EdgeControllerTest extends AbstractControllerTest {
return false;
}
private boolean popRuleChainMetadataMsg(List<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) {
for (AbstractMessage message : messages) {
if (message instanceof AdminSettingsUpdateMsg adminSettingsUpdateMsg) {
@ -1046,6 +1088,20 @@ public class EdgeControllerTest extends AbstractControllerTest {
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) {
for (AbstractMessage message : messages) {
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.AssetUpdateMsg;
import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.EdgeConfiguration;
@ -96,6 +97,7 @@ import org.thingsboard.server.gen.edge.v1.TenantProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.TenantUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UserUpdateMsg;
import java.util.ArrayList;
@ -143,7 +145,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret());
edgeImitator.ignoreType(OAuth2UpdateMsg.class);
edgeImitator.expectMessageAmount(21);
edgeImitator.expectMessageAmount(24);
edgeImitator.connect();
requestEdgeRuleChainMetadata();
@ -250,7 +252,7 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
UUID ruleChainUUID = validateRuleChains();
// 1 from request message
validateMsgsCnt(RuleChainMetadataUpdateMsg.class, 1);
validateMsgsCnt(RuleChainMetadataUpdateMsg.class, 2);
validateRuleChainMetadataUpdates(ruleChainUUID);
// 4 messages ('general', 'mail', 'connectivity', 'jwt')
@ -275,6 +277,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
validateMsgsCnt(DeviceUpdateMsg.class, 1);
validateDevices();
validateMsgsCnt(DeviceCredentialsUpdateMsg.class, 1);
// 1 from asset fetcher
validateMsgsCnt(AssetUpdateMsg.class, 1);
validateAssets();
@ -287,6 +291,8 @@ abstract public class AbstractEdgeTest extends AbstractControllerTest {
validateMsgsCnt(UserUpdateMsg.class, 1);
validateUsers();
validateMsgsCnt(UserCredentialsUpdateMsg.class, 1);
// 1 from tenant fetcher
validateMsgsCnt(TenantUpdateMsg.class, 1);
validateTenant();

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

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

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.dao.service.DaoSqlTest;
import org.thingsboard.server.gen.edge.v1.AttributeDeleteMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg;
import org.thingsboard.server.gen.edge.v1.EntityDataProto;
import org.thingsboard.server.gen.edge.v1.UplinkMsg;
@ -183,7 +184,7 @@ public class TelemetryEdgeTest extends AbstractEdgeTest {
edgeImitator.setRandomFailuresOnTimeseriesDownlink(true);
// imitator will generate failure in 100% of timeseries cases
edgeImitator.setFailureProbability(100);
edgeImitator.expectMessageAmount(numberOfMsgsToSend);
edgeImitator.expectMessageAmount(numberOfMsgsToSend * 2);
for (int idx = 1; idx <= numberOfMsgsToSend; idx++) {
String timeseriesData = "{\"data\":{\"idx\":" + idx + "},\"ts\":" + System.currentTimeMillis() + "}";
JsonNode timeseriesEntityData = JacksonUtil.toJsonNode(timeseriesData);
@ -204,6 +205,9 @@ public class TelemetryEdgeTest extends AbstractEdgeTest {
List<DeviceUpdateMsg> deviceUpdateMsgs = edgeImitator.findAllMessagesByType(DeviceUpdateMsg.class);
Assert.assertEquals(numberOfMsgsToSend, deviceUpdateMsgs.size());
List<DeviceCredentialsUpdateMsg> deviceCredentialsUpdateMsgs = edgeImitator.findAllMessagesByType(DeviceCredentialsUpdateMsg.class);
Assert.assertEquals(numberOfMsgsToSend, deviceCredentialsUpdateMsgs.size());
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());
// update user
edgeImitator.expectMessageAmount(1);
edgeImitator.expectMessageAmount(2);
savedTenantAdmin.setLastName("Borisov");
savedTenantAdmin = doPost("/api/user", savedTenantAdmin, User.class);
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof UserUpdateMsg);
userUpdateMsg = (UserUpdateMsg) latestMessage;
userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class);
Assert.assertTrue(userUpdateMsgOpt.isPresent());
userUpdateMsg = userUpdateMsgOpt.get();
userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true);
Assert.assertNotNull(userMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType());
@ -92,7 +92,7 @@ public class UserEdgeTest extends AbstractEdgeTest {
changePasswordRequest.setNewPassword("newTenant");
doPost("/api/auth/changePassword", changePasswordRequest);
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg);
UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage;
UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true);
@ -155,13 +155,13 @@ public class UserEdgeTest extends AbstractEdgeTest {
Assert.assertEquals(savedCustomerUser.getLastName(), userMsg.getLastName());
// update user
edgeImitator.expectMessageAmount(1);
edgeImitator.expectMessageAmount(2);
savedCustomerUser.setLastName("Addams");
savedCustomerUser = doPost("/api/user", savedCustomerUser, User.class);
Assert.assertTrue(edgeImitator.waitForMessages());
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof UserUpdateMsg);
userUpdateMsg = (UserUpdateMsg) latestMessage;
userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class);
Assert.assertTrue(userUpdateMsgOpt.isPresent());
userUpdateMsg = userUpdateMsgOpt.get();
userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true);
Assert.assertNotNull(userMsg);
Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType());
@ -176,7 +176,7 @@ public class UserEdgeTest extends AbstractEdgeTest {
changePasswordRequest.setNewPassword("newCustomer");
doPost("/api/auth/changePassword", changePasswordRequest);
Assert.assertTrue(edgeImitator.waitForMessages());
latestMessage = edgeImitator.getLatestMessage();
AbstractMessage latestMessage = edgeImitator.getLatestMessage();
Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg);
UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage;
UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true);

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

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 org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.springframework.http.MediaType;
import org.springframework.mock.http.client.MockClientHttpRequest;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.util.ClassUtils;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.StandardCharsets;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
@Slf4j
public class RestTemplateConvertersTest {
@Test
public void testJacksonXmlConverter() {
public void testMappingJackson2HttpMessageConverterIsUsedInsteadOfMappingJackson2XmlHttpMessageConverter() {
ClassLoader classLoader = RestTemplate.class.getClassLoader();
boolean jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
Assertions.assertFalse(jackson2XmlPresent, "XmlMapper must not be present in classpath, please, exclude \"jackson-dataformat-xml\" dependency!");
//If this xml mapper will be present in classpath then we will get "Unsupported Media Type" in RestTemplate
assertThat(jackson2XmlPresent).isTrue();
RestTemplate restTemplate = new RestTemplate();
MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate);
mockServer.expect(requestTo("/test"))
.andExpect(request -> {
MockClientHttpRequest mockRequest = (MockClientHttpRequest) request;
byte[] body = mockRequest.getBodyAsBytes();
String requestBody = new String(body, StandardCharsets.UTF_8);
assertThat(requestBody).contains("{\"name\":\"test\",\"value\":1}");
})
.andRespond(withSuccess("{\"name\":\"test\",\"value\":1}", MediaType.APPLICATION_JSON));
TestObject requestObject = new TestObject("test", 1);
TestObject actualObject = restTemplate.postForObject("/test", requestObject, TestObject.class);
assertThat(actualObject).isEqualTo(requestObject);
mockServer.verify();
}
record TestObject(String name, int value) {}
}

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

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_GATEWAY("transport messages per gateway", false),
TRANSPORT_MESSAGES_PER_GATEWAY_DEVICE("transport messages per gateway device", false),
EMAILS("emails sending", true);
EMAILS("emails sending", true),
WS_SUBSCRIPTIONS("WS subscriptions", false);
private Function<DefaultTenantProfileConfiguration, String> configExtractor;
@Getter

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

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

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!");
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())
.setEdgeIdLSB(response.getEdgeId().getId().getLeastSignificantBits())
.setSuccess(response.isSuccess())
.setError(response.getError())
.build();
}
@ -170,7 +171,8 @@ public class ProtoUtils {
new UUID(proto.getResponseIdMSB(), proto.getResponseIdLSB()),
TenantId.fromUUID(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB())),
new EdgeId(new UUID(proto.getEdgeIdMSB(), proto.getEdgeIdLSB())),
proto.getSuccess()
proto.getSuccess(),
proto.getError()
);
}

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

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

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

@ -124,7 +124,7 @@ class ProtoUtilsTest {
@Test
void protoFromEdgeSyncResponseSerialization() {
FromEdgeSyncResponse msg = new FromEdgeSyncResponse(id, tenantId, edgeId, true);
FromEdgeSyncResponse msg = new FromEdgeSyncResponse(id, tenantId, edgeId, true, "Error Msg");
assertThat(ProtoUtils.fromProto(ProtoUtils.toProto(msg))).as("deserialized").isEqualTo(msg);
}

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.coap.ConfigProtos;
import org.thingsboard.server.gen.transport.coap.DeviceInfoProtos;
import org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos;
import org.thingsboard.server.gen.transport.coap.MeasurementsProtos;
import org.thingsboard.server.gen.transport.coap.MeasurementsProtos.ProtoChannel;
import org.thingsboard.server.transport.coap.AbstractCoapTransportResource;
import org.thingsboard.server.transport.coap.CoapTransportContext;
import org.thingsboard.server.transport.coap.callback.CoapDeviceAuthCallback;
@ -49,19 +49,21 @@ import org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils;
import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static com.google.gson.JsonParser.parseString;
import static org.thingsboard.server.transport.coap.CoapTransportService.CONFIGURATION;
import static org.thingsboard.server.transport.coap.CoapTransportService.CURRENT_TIMESTAMP;
import static org.thingsboard.server.transport.coap.CoapTransportService.DEVICE_INFO;
import static org.thingsboard.server.transport.coap.CoapTransportService.MEASUREMENTS;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.isBinarySensor;
import static org.thingsboard.server.transport.coap.efento.utils.CoapEfentoUtils.isSensorError;
@Slf4j
public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
@ -220,127 +222,186 @@ public class CoapEfentoTransportResource extends AbstractCoapTransportResource {
}
}
private List<EfentoTelemetry> getEfentoMeasurements(MeasurementsProtos.ProtoMeasurements protoMeasurements, UUID sessionId) {
List<EfentoTelemetry> getEfentoMeasurements(MeasurementsProtos.ProtoMeasurements protoMeasurements, UUID sessionId) {
String serialNumber = CoapEfentoUtils.convertByteArrayToString(protoMeasurements.getSerialNum().toByteArray());
boolean batteryStatus = protoMeasurements.getBatteryStatus();
int measurementPeriodBase = protoMeasurements.getMeasurementPeriodBase();
int measurementPeriodFactor = protoMeasurements.getMeasurementPeriodFactor();
int signal = protoMeasurements.getSignal();
List<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<>();
if (!CollectionUtils.isEmpty(channelsList)) {
int channel = 0;
JsonObject values;
for (MeasurementsProtos.ProtoChannel protoChannel : channelsList) {
channel++;
boolean isBinarySensor = false;
MeasurementTypeProtos.MeasurementType measurementType = protoChannel.getType();
String measurementTypeName = measurementType.name();
if (measurementType.equals(MeasurementTypeProtos.MeasurementType.OK_ALARM)
|| measurementType.equals(MeasurementTypeProtos.MeasurementType.FLOODING)) {
isBinarySensor = true;
}
if (measurementPeriodFactor == 0 && isBinarySensor) {
measurementPeriodFactor = 14;
} else {
measurementPeriodFactor = 1;
for (int channel = 0; channel < channelsList.size(); channel++) {
ProtoChannel protoChannel = channelsList.get(channel);
List<Integer> sampleOffsetsList = protoChannel.getSampleOffsetsList();
if (CollectionUtils.isEmpty(sampleOffsetsList)) {
log.trace("[{}][{}] sampleOffsetsList list is empty!", sessionId, protoChannel.getType().name());
continue;
}
boolean isBinarySensor = isBinarySensor(protoChannel.getType());
int channelPeriodFactor = (measurementPeriodFactor == 0 ? (isBinarySensor ? 14 : 1) : measurementPeriodFactor);
int measurementPeriod = measurementPeriodBase * channelPeriodFactor;
long measurementPeriodMillis = TimeUnit.SECONDS.toMillis(measurementPeriod);
long startTimestampMillis = TimeUnit.SECONDS.toMillis(protoChannel.getTimestamp());
for (int i = 0; i < sampleOffsetsList.size(); i++) {
int sampleOffset = sampleOffsetsList.get(i);
if (isSensorError(sampleOffset)) {
log.warn("[{}],[{}] Sensor error value! Ignoring.", sessionId, sampleOffset);
continue;
}
int measurementPeriod = measurementPeriodBase * measurementPeriodFactor;
long measurementPeriodMillis = TimeUnit.SECONDS.toMillis(measurementPeriod);
long nextTransmissionAtMillis = TimeUnit.SECONDS.toMillis(protoMeasurements.getNextTransmissionAt());
int startPoint = protoChannel.getStartPoint();
int startTimestamp = protoChannel.getTimestamp();
long startTimestampMillis = TimeUnit.SECONDS.toMillis(startTimestamp);
List<Integer> sampleOffsetsList = protoChannel.getSampleOffsetsList();
if (!CollectionUtils.isEmpty(sampleOffsetsList)) {
int sampleOfssetsListSize = sampleOffsetsList.size();
for (int i = 0; i < sampleOfssetsListSize; i++) {
int sampleOffset = sampleOffsetsList.get(i);
Integer previousSampleOffset = isBinarySensor && i > 0 ? sampleOffsetsList.get(i - 1) : null;
if (sampleOffset == -32768) {
log.warn("[{}],[{}] Sensor error value! Ignoring.", sessionId, sampleOffset);
} else {
switch (measurementType) {
case TEMPERATURE:
values = valuesMap.computeIfAbsent(startTimestampMillis, k ->
CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k));
values.addProperty("temperature_" + channel, ((double) (startPoint + sampleOffset)) / 10f);
startTimestampMillis = startTimestampMillis + measurementPeriodMillis;
break;
case WATER_METER:
values = valuesMap.computeIfAbsent(startTimestampMillis, k ->
CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k));
values.addProperty("pulse_counter_water_" + channel, ((double) (startPoint + sampleOffset)));
startTimestampMillis = startTimestampMillis + measurementPeriodMillis;
break;
case HUMIDITY:
values = valuesMap.computeIfAbsent(startTimestampMillis, k ->
CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k));
values.addProperty("humidity_" + channel, (double) (startPoint + sampleOffset));
startTimestampMillis = startTimestampMillis + measurementPeriodMillis;
break;
case ATMOSPHERIC_PRESSURE:
values = valuesMap.computeIfAbsent(startTimestampMillis, k ->
CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k));
values.addProperty("pressure_" + channel, (double) (startPoint + sampleOffset) / 10f);
startTimestampMillis = startTimestampMillis + measurementPeriodMillis;
break;
case DIFFERENTIAL_PRESSURE:
values = valuesMap.computeIfAbsent(startTimestampMillis, k ->
CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k));
values.addProperty("pressure_diff_" + channel, (double) (startPoint + sampleOffset));
startTimestampMillis = startTimestampMillis + measurementPeriodMillis;
break;
case OK_ALARM:
boolean currentIsOk = sampleOffset < 0;
if (previousSampleOffset != null) {
boolean previousIsOk = previousSampleOffset < 0;
boolean isOk = previousIsOk && currentIsOk;
boolean isAlarm = !previousIsOk && !currentIsOk;
if (isOk || isAlarm) {
break;
}
}
String data = currentIsOk ? "OK" : "ALARM";
long sampleOffsetMillis = TimeUnit.SECONDS.toMillis(sampleOffset);
long measurementTimestamp = startTimestampMillis + Math.abs(sampleOffsetMillis);
values = valuesMap.computeIfAbsent(measurementTimestamp - 1000, k ->
CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k));
values.addProperty("ok_alarm_" + channel, data);
break;
case PULSE_CNT:
values = valuesMap.computeIfAbsent(startTimestampMillis, k ->
CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k));
values.addProperty("pulse_cnt_" + channel, (double) (startPoint + sampleOffset));
startTimestampMillis = startTimestampMillis + measurementPeriodMillis;
break;
case NO_SENSOR:
case UNRECOGNIZED:
log.trace("[{}][{}] Sensor error value! Ignoring.", sessionId, measurementTypeName);
break;
default:
log.trace("[{}],[{}] Unsupported measurementType! Ignoring.", sessionId, measurementTypeName);
break;
}
JsonObject values;
if (isBinarySensor) {
boolean currentIsOk = sampleOffset < 0;
Integer previousSampleOffset = i > 0 ? sampleOffsetsList.get(i - 1) : null;
if (previousSampleOffset != null) { //compare with previous value
boolean previousIsOk = previousSampleOffset < 0;
if (currentIsOk == previousIsOk) {
break;
}
}
long sampleOffsetMillis = TimeUnit.SECONDS.toMillis(sampleOffset);
long measurementTimestamp = startTimestampMillis + Math.abs(sampleOffsetMillis);
values = valuesMap.computeIfAbsent(measurementTimestamp - 1000, k ->
CoapEfentoUtils.setDefaultMeasurements(serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k));
addBinarySample(protoChannel, currentIsOk, values, channel + 1, sessionId);
} else {
log.trace("[{}][{}] sampleOffsetsList list is empty!", sessionId, measurementTypeName);
long timestampMillis = startTimestampMillis + i * measurementPeriodMillis;
values = valuesMap.computeIfAbsent(timestampMillis, k -> CoapEfentoUtils.setDefaultMeasurements(
serialNumber, batteryStatus, measurementPeriod, nextTransmissionAtMillis, signal, k));
addContinuesSample(protoChannel, sampleOffset, values, channel + 1, sessionId);
}
}
} else {
throw new IllegalStateException("[" + sessionId + "]: Failed to get Efento measurements, reason: channels list is empty!");
}
if (!CollectionUtils.isEmpty(valuesMap)) {
List<EfentoTelemetry> efentoMeasurements = new ArrayList<>();
for (Long ts : valuesMap.keySet()) {
EfentoTelemetry measurement = new EfentoTelemetry(ts, valuesMap.get(ts));
efentoMeasurements.add(measurement);
}
return efentoMeasurements;
} else {
if (CollectionUtils.isEmpty(valuesMap)) {
throw new IllegalStateException("[" + sessionId + "]: Failed to collect Efento measurements, reason, values map is empty!");
}
return valuesMap.entrySet().stream()
.map(entry -> new EfentoTelemetry(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
}
private void addContinuesSample(ProtoChannel protoChannel, int sampleOffset, JsonObject values, int channelNumber, UUID sessionId) {
int startPoint = protoChannel.getStartPoint();
switch (protoChannel.getType()) {
case MEASUREMENT_TYPE_TEMPERATURE:
values.addProperty("temperature_" + channelNumber, ((double) (startPoint + sampleOffset)) / 10f);
break;
case MEASUREMENT_TYPE_WATER_METER:
values.addProperty("pulse_counter_water_" + channelNumber, ((double) (startPoint + sampleOffset)));
break;
case MEASUREMENT_TYPE_HUMIDITY:
values.addProperty("humidity_" + channelNumber, (double) (startPoint + sampleOffset));
break;
case MEASUREMENT_TYPE_ATMOSPHERIC_PRESSURE:
values.addProperty("pressure_" + channelNumber, (double) (startPoint + sampleOffset) / 10f);
break;
case MEASUREMENT_TYPE_DIFFERENTIAL_PRESSURE:
values.addProperty("pressure_diff_" + channelNumber, (double) (startPoint + sampleOffset));
break;
case MEASUREMENT_TYPE_PULSE_CNT:
values.addProperty("pulse_cnt_" + channelNumber, (double) (startPoint + sampleOffset));
break;
case MEASUREMENT_TYPE_IAQ:
values.addProperty("iaq_" + channelNumber, (startPoint + sampleOffset));
break;
case MEASUREMENT_TYPE_ELECTRICITY_METER:
values.addProperty("watt_hour_" + channelNumber, (double) (startPoint + sampleOffset));
break;
case MEASUREMENT_TYPE_SOIL_MOISTURE:
values.addProperty("soil_moisture_" + channelNumber, (double) (startPoint + sampleOffset));
break;
case MEASUREMENT_TYPE_AMBIENT_LIGHT:
values.addProperty("ambient_light_" + channelNumber, (double) (startPoint + sampleOffset) / 10f);
break;
case MEASUREMENT_TYPE_HIGH_PRESSURE:
values.addProperty("high_pressure_" + channelNumber, (double) (startPoint + sampleOffset));
break;
case MEASUREMENT_TYPE_DISTANCE_MM:
values.addProperty("distance_mm_" + channelNumber, (double) (startPoint + sampleOffset));
break;
case MEASUREMENT_TYPE_WATER_METER_ACC_MINOR:
values.addProperty("acc_counter_water_minor_" + channelNumber, (double) (startPoint + sampleOffset));
break;
case MEASUREMENT_TYPE_WATER_METER_ACC_MAJOR:
values.addProperty("acc_counter_water_major_" + channelNumber, (double) (startPoint + sampleOffset));
break;
case MEASUREMENT_TYPE_HUMIDITY_ACCURATE:
values.addProperty("humidity_relative_" + channelNumber, (double) (startPoint + sampleOffset) / 10f);
break;
case MEASUREMENT_TYPE_STATIC_IAQ:
values.addProperty("static_iaq_" + channelNumber, (double) (startPoint + sampleOffset));
break;
case MEASUREMENT_TYPE_CO2_EQUIVALENT:
values.addProperty("co2_ppm_" + channelNumber, (double) (startPoint + sampleOffset));
break;
case MEASUREMENT_TYPE_BREATH_VOC:
values.addProperty("breath_voc_ppm_" + channelNumber, (double) (startPoint + sampleOffset));
break;
case MEASUREMENT_TYPE_PERCENTAGE:
values.addProperty("percentage_" + channelNumber, (double) (startPoint + sampleOffset) / 100f);
break;
case MEASUREMENT_TYPE_VOLTAGE:
values.addProperty("voltage_" + channelNumber, (double) (startPoint + sampleOffset) / 10f);
break;
case MEASUREMENT_TYPE_CURRENT:
values.addProperty("current_" + channelNumber, (double) (startPoint + sampleOffset) / 100f);
break;
case MEASUREMENT_TYPE_PULSE_CNT_ACC_MINOR:
values.addProperty("pulse_cnt_minor_" + channelNumber, (double) (startPoint + sampleOffset));
break;
case MEASUREMENT_TYPE_PULSE_CNT_ACC_MAJOR:
values.addProperty("pulse_cnt_major_" + channelNumber, (double) (startPoint + sampleOffset));
break;
case MEASUREMENT_TYPE_ELEC_METER_ACC_MINOR:
values.addProperty("elec_meter_minor_" + channelNumber, (double) (startPoint + sampleOffset));
break;
case MEASUREMENT_TYPE_ELEC_METER_ACC_MAJOR:
values.addProperty("elec_meter_major_" + channelNumber, (double) (startPoint + sampleOffset));
break;
case MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MINOR:
values.addProperty("pulse_cnt_wide_minor_" + channelNumber, (double) (startPoint + sampleOffset));
break;
case MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MAJOR:
values.addProperty("pulse_cnt_wide_major_" + channelNumber, (double) (startPoint + sampleOffset));
break;
case MEASUREMENT_TYPE_CURRENT_PRECISE:
values.addProperty("current_precise_" + channelNumber, (double) (startPoint + sampleOffset)/1000f);
break;
case MEASUREMENT_TYPE_NO_SENSOR:
case UNRECOGNIZED:
log.trace("[{}][{}] Sensor error value! Ignoring.", sessionId, protoChannel.getType().name());
break;
default:
log.trace("[{}],[{}] Unsupported measurementType! Ignoring.", sessionId, protoChannel.getType().name());
break;
}
}
private void addBinarySample(ProtoChannel protoChannel, boolean valueIsOk, JsonObject values, int channel, UUID sessionId) {
switch (protoChannel.getType()) {
case MEASUREMENT_TYPE_OK_ALARM:
values.addProperty("ok_alarm_" + channel, valueIsOk ? "OK" : "ALARM");
break;
case MEASUREMENT_TYPE_FLOODING:
values.addProperty("flooding_" + channel, valueIsOk ? "OK" : "WATER_DETECTED");
break;
case MEASUREMENT_TYPE_OUTPUT_CONTROL:
values.addProperty("output_control_" + channel, valueIsOk ? "OFF" : "ON");
break;
default:
log.trace("[{}],[{}] Unsupported binary measurementType! Ignoring.", sessionId, protoChannel.getType().name());
break;
}
}
private EfentoTelemetry getEfentoDeviceInfo(DeviceInfoProtos.ProtoDeviceInfo protoDeviceInfo) {

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;
import com.google.gson.JsonObject;
import org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.MeasurementType;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
import static org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_FLOODING;
import static org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_OK_ALARM;
import static org.thingsboard.server.gen.transport.coap.MeasurementTypeProtos.MeasurementType.MEASUREMENT_TYPE_OUTPUT_CONTROL;
public class CoapEfentoUtils {
public static String convertByteArrayToString(byte[] a) {
@ -50,4 +55,12 @@ public class CoapEfentoUtils {
return values;
}
public static boolean isBinarySensor(MeasurementType type) {
return type == MEASUREMENT_TYPE_OK_ALARM || type == MEASUREMENT_TYPE_FLOODING || type == MEASUREMENT_TYPE_OUTPUT_CONTROL;
}
public static boolean isSensorError(int sampleOffset) {
return sampleOffset >= 8355840 && sampleOffset <= 8388607;
}
}

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";
/* Message containing optional channels control parameters */
message ProtoChannelControl {
message ProtoOutputControlState {
/* Channel index */
uint32 channel_index = 1;
/* Control parameters. Maximal number equals 4. This field is channel specific: */
/* IO_control channel: */
/* - control_params[0]: */
/* - Byte 0: On state configuration */
/* 0x01 - Low */
/* 0x02 - High */
/* 0x03 - High-Z (disconnected) */
/* - Byte 1: Off state configuration */
/* 0x01 - Low */
/* 0x02 - High */
/* 0x03 - High-Z (disconnected) */
/* - Byte 2: Power on channel state */
/* 0x01 - On */
/* 0x02 - Off */
repeated uint32 control_params = 2;
/* Channel state ON/OFF. Range (1 - OFF; 2 - ON) */
uint32 channel_state = 2;
}
/* Message containing request data for accesing calibration parameters */
@ -58,6 +45,39 @@ message ProtoCalibrationParameters {
repeated int32 parameters = 3;
}
enum BleAdvertisingPeriodMode {
/* Invalid value */
BLE_ADVERTISING_PERIOD_MODE_UNSPECIFIED = 0;
/* Default behavior - faster advertising when measurement period is < 15s. */
BLE_ADVERTISING_PERIOD_MODE_DEFAULT = 1;
/* User-configured normal interval is used. */
BLE_ADVERTISING_PERIOD_MODE_NORMAL = 2;
/* User-configured fast interval is used. */
BLE_ADVERTISING_PERIOD_MODE_FAST = 3;
}
/* Message containing BLE advertising period configuration */
message ProtoBleAdvertisingPeriod {
/* BLE advertising mode: */
/* - 1: Default, BLE advertising interval is set to 1022.5ms or some lower value, based on continuous measurement period. */
/* - 2: Normal, uses user-configured value from 'normal' field. */
/* - 3: Fast, uses user-configured value from 'fast' field (must be lower than or equal to 'normal' field). */
BleAdvertisingPeriodMode mode = 1;
/* BLE advertising interval when in normal mode, configured in 0.625ms steps. */
/* Range: [32:16384] */
uint32 normal = 2;
/* BLE advertising interval when in fast mode, configured in 0.625ms steps. */
/* Range: [32:16384] */
uint32 fast = 3;
}
/* Main message sent in the payload. Each field in this message is independent of the others - only parameters that should be */
/* changed need to be sent in the payload. */
/* If the value of a selected parameter shall not be changed, do not include it in the payload */
@ -115,16 +135,26 @@ message ProtoConfig {
/* 65535 - disable transfer limit function */
uint32 transfer_limit_timer = 10;
/* Data (measurements) server IP address */
/* IP of the data server as string in form x.x.x.x. For example: 18.184.24.239 */
/* For firmware >= 6.07.00: */
/* IP or URL address of the data (measurements) server */
/* The IP or URL of the data server, provided as string with a maximum length of 31 characters */
/* For example, use "18.184.24.239" for an IP address or "efento.test.io" for a URL */
/* For firmware < 6.07.00: */
/* IP address of the data (measurements) server */
/* For example, use "18.184.24.239" */
string data_server_ip = 11;
/* Data (measurements) server port */
/* Range: [1:65535] */
uint32 data_server_port = 12;
/* Update server IP address */
/* IP of data server as string in form x.x.x.x. For example: 18.184.24.239 */
/* For firmware >= 6.07.00: */
/* IP or URL address of the update server */
/* The IP or URL of the update server, provided as string with a maximum length of 31 characters */
/* For example, use "18.184.24.239" for an IP address or "efento.test.io" for a URL */
/* For firmware < 6.07.00: */
/* IP address of the update server */
/* For example, use "18.184.24.239" */
string update_server_ip = 13;
/* Update server port for UDP transmission */
@ -144,7 +174,8 @@ message ProtoConfig {
/* 0xFFFFFFFF or 1000000 - automatic selection */
uint32 plmn_selection = 17;
/* Device will power off its cellular modem for requested number of seconds. Maximum number of seconds 604800 (7 days) */
/* Device will power off its cellular modem for requested number of seconds. */
/* Range: [60:604800] (1 minute : 7 days) */
/* This field is only sent by server */
uint32 disable_modem_request = 18;
@ -263,9 +294,8 @@ message ProtoConfig {
/* Calendar configuration. Up to 6 calendars are supported */
repeated ProtoCalendar calendars = 47;
/* Control parameters for channels. Maximal number of requests equals 6 */
/* This field is only sent by server */
repeated ProtoChannelControl channels_control_request = 48;
/* DEPRECATED - Used for backward compatibility */
reserved 48;
/* Set/get calibration parameters for single channel. */
ProtoCalibrationParameters calibration_parameters_request = 49;
@ -295,7 +325,7 @@ message ProtoConfig {
reserved 52, 53;
/* Encryption key configuration. Sensor sends in this field two last bytes of SHA256 hash calculated from its current */
/* encryption_key configuration. */
/* encryption_key configuration. When encryption key is disabled one byte 0x7F (DEL) is sent. */
/* Max length: 16 bytes. */
/* 0x7F - encryption key disabled. */
bytes encryption_key = 54;
@ -309,4 +339,14 @@ message ProtoConfig {
/* String with special character 0x7F (DEL) only indicates that automatic password is turn on */
/* Password can only be set to custom value if apn_user_name has been configured (is not automatic) */
string apn_password = 56;
/* Reserved by versions above 06.20.00 */
reserved 57;
/* Control output state on channel pin. Maximal number of requests equals 3 */
/* This field is only sent by server */
repeated ProtoOutputControlState output_control_state_request = 58;
/* BLE advertising period configuration. */
ProtoBleAdvertisingPeriod ble_advertising_period = 59;
}

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[33] - Tx_time - [0.1s] - Range: [0:2147483647]. Unknown value: -1 */
repeated sint32 parameters = 2;
/* ICCID of inserted/soldered sim card. String up to 22 characters long. */
/* 0x7F if sim card is not detected, empty (not sent) if device does not have modem. */
/* This field is only sent by device */
string sim_card_identification = 3;
}
message ProtoUpdateInfo
@ -115,16 +120,16 @@ message ProtoUpdateInfo
uint32 timestamp = 1;
/* Update status, possible values: */
/* - 0x1 - No update yet */
/* - 0x2 - No error */
/* - 0x3 - UDP socekt error */
/* - 0x4 - Hash error */
/* - 0x5 - Missing packet error */
/* - 0x6 - Invalid data error */
/* - 0x7 - Sending timeout error */
/* - 0x8 - No SW to update error */
/* - 0x9 - Sending unexpected error */
/* - 0x10 - Unexpected error */
/* - 1 - No update yet */
/* - 2 - No error */
/* - 3 - UDP socekt error */
/* - 4 - Hash error */
/* - 5 - Missing packet error */
/* - 6 - Invalid data error */
/* - 7 - Sending timeout error */
/* - 8 - No SW to update error */
/* - 9 - Sending unexpected error */
/* - 10 - Unexpected error */
uint32 status = 2;
}

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";
enum MeasurementType {
NO_SENSOR = 0;
/* [°C] - Celsius degree. Resolution 0.1°C. Range [-273.2-4000.0]. Type: Continuous */
TEMPERATURE = 1;
/* [% RH] - Relative humidity. Resolution 1%. Range [0-100]. Type: Continuous */
HUMIDITY = 2;
/* [hPa] - Hectopascal (1hPa = 100Pa). Resolution 0.1hPa. Range: [1.0-2000.0]. Type: Continuous */
ATMOSPHERIC_PRESSURE = 3;
/* [Pa] - Pascal. Resolution 1Pa. Range [-10000-10000]Type: Continuous */
DIFFERENTIAL_PRESSURE = 4;
/* Sign indicates state: (+) ALARM, (-) OK. Type: Binary */
OK_ALARM = 5;
/* [IAQ] - Iaq index. Resolution 1IAQ. Range [0-500]. Sensor return also calibration status */
/* as offset to measured value: */
/* - offset 3000: Sensor not stabilized (always returns 25 IAQ value) */
/* - offset 2000: Calibration required (sensor returns not accurate values) */
/* - offset 1000: Calibration on-going (sensor returns not accurate values) */
/* - offset 0: Calibration done (best accuracy of IAQ sensor) */
/* Type: Continuous */
IAQ = 6;
/* Sign indicates water presence: (+) water not detected, (-) water detected. Type: Binary */
FLOODING = 7;
/* [NB] Number of pulses. Resolution 1 pulse. Range [0-16711679]. Type: Continuous */
PULSE_CNT = 8;
/* [Wh] - Watthour; Resolution 1Wh. Range [0-16711679]. Number of Watthours in a single period. Type: Continuous */
ELECTRICITY_METER = 9;
/* [l] - Liter. Resolution 1l. Range [0-16711679]. Number of litres in a single period. Type: Continuous */
WATER_METER = 10;
/* [kPa] - Kilopascal (1kPa = 1000Pa); Resolution 1kPa. Range [-1000-0]. Soil moisture (tension). Type: Continuous */
SOIL_MOISTURE = 11;
/* [ppm] - Parts per million. Resolution 1ppm. Range [0-1000000]. Carbon monoxide concentration. Type: Continuous */
CO_GAS = 12;
/* [ppm] - Parts per million. Resolution 0.01ppm. Range [0-1000000.00]. Nitrogen dioxide concentration. Type: Continuous*/
NO2_GAS = 13;
/* [ppm] - Parts per million. Resolution 1ppm. Range [0-1000000]. Hydrogen sulfide concentration. Type: Continuous */
H2S_GAS = 14;
/* [lx] - Illuminance. Resolution 0.1lx. Range [0-100000.0]. Type: Continuous */
AMBIENT_LIGHT = 15;
/* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0-1000]. */
/* particles with an aerodynamic diameter less than 1 micrometer. Type: Continuous */
PM_1_0 = 16; // µg/m^3
/* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0-1000]. */
/* particles with an aerodynamic diameter less than 2.5 micrometers. Type: Continuous */
PM_2_5 = 17; // µg/m^3
/* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3 Range [0-1000]. */
/* particles with an aerodynamic diameter less than 10 micrometers. Type: Continuous */
PM_10_0 = 18; // µg/m^3
/* [dB] - Decibels. Resolution 0.1 dB. Range: [0-130.0]. Noise level. Type: Continuous */
NOISE_LEVEL = 19; // 0.1 dB
/* [ppm] - Parts per million. Resolution 1ppm. Range [0-1000000]. Ammonia concentration. Type: Continuous */
NH3_GAS = 20;
/* [ppm] - Parts per million. Resolution 1ppm. Range [0-1000000]. Methane concentration. Type: Continuous */
CH4_GAS = 21;
/* [] - No sensor on the channel */
MEASUREMENT_TYPE_NO_SENSOR = 0;
/* [°C] - Celsius degree. Resolution 0.1°C. Range [-273.2:4000.0]. Type: Continuous */
MEASUREMENT_TYPE_TEMPERATURE = 1;
/* [% RH] - Relative humidity. Resolution 1%. Range [0:100]. Type: Continuous */
MEASUREMENT_TYPE_HUMIDITY = 2;
/* [hPa] - Hectopascal (1hPa = 100Pa). Resolution 0.1hPa. Range: [1.0:2000.0]. Atmospheric pressure. Type: Continuous */
MEASUREMENT_TYPE_ATMOSPHERIC_PRESSURE = 3;
/* [Pa] - Pascal. Resolution 1Pa. Range [-10000:10000]. Differential pressure. Type: Continuous */
MEASUREMENT_TYPE_DIFFERENTIAL_PRESSURE = 4;
/* Sign indicates state: (+) ALARM, (-) OK. Type: Binary */
MEASUREMENT_TYPE_OK_ALARM = 5;
/* [IAQ] - IAQ index. Resolution 1IAQ. Range [0:500]. To get IAQ index the value should be divided by 3. */
/* Sensor return also calibration status as metadata (is the remainder when the absolute value is divided by 3): */
/* - 0: Calibration required (sensor returns not accurate values) */
/* - 1: Calibration on-going (sensor returns not accurate values) */
/* - 2: Calibration done (best accuracy of IAQ sensor) */
/* Type: Continuous */
MEASUREMENT_TYPE_IAQ = 6;
/* Sign indicates water presence: (+) water not detected, (-) water detected. Type: Binary */
MEASUREMENT_TYPE_FLOODING = 7;
/* [NB] Number of pulses. Resolution 1 pulse. Range [0:8000000]. Type: Continuous */
MEASUREMENT_TYPE_PULSE_CNT = 8;
/* [Wh] - Watthour; Resolution 1Wh. Range [0:8000000]. Number of Watthours in a single period. Type: Continuous */
MEASUREMENT_TYPE_ELECTRICITY_METER = 9;
/* [l] - Liter. Resolution 1l. Range [0:8000000]. Number of litres in a single period. Type: Continuous */
MEASUREMENT_TYPE_WATER_METER = 10;
/* [kPa] - Kilopascal (1kPa = 1000Pa); Resolution 1kPa. Range [-1000:0]. Soil moisture (tension). Type: Continuous */
MEASUREMENT_TYPE_SOIL_MOISTURE = 11;
/* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon monoxide concentration. Type: Continuous */
MEASUREMENT_TYPE_CO_GAS = 12;
/* [ppm] - Parts per million. Resolution 1.0ppm. Range [0:1000000]. Nitrogen dioxide concentration. Type: Continuous */
MEASUREMENT_TYPE_NO2_GAS = 13;
/* [ppm] - Parts per million. Resolution 0.01ppm. Range [0.00:80000.00]. Hydrogen sulfide concentration. Type: Continuous */
MEASUREMENT_TYPE_H2S_GAS = 14;
/* [lx] - Lux. Resolution 0.1lx. Range [0.0:100000.0]. Illuminance. Type: Continuous */
MEASUREMENT_TYPE_AMBIENT_LIGHT = 15;
/* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3. Range [0:1000]. */
/* Particles with an aerodynamic diameter less than 1 micrometer. Type: Continuous */
MEASUREMENT_TYPE_PM_1_0 = 16;
/* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3. Range [0:1000]. */
/* Particles with an aerodynamic diameter less than 2.5 micrometers. Type: Continuous */
MEASUREMENT_TYPE_PM_2_5 = 17;
/* [µg/m^3] - Micro gram per cubic meter. Resolution 1µg/m^3. Range [0:1000]. */
/* Particles with an aerodynamic diameter less than 10 micrometers. Type: Continuous */
MEASUREMENT_TYPE_PM_10_0 = 18;
/* [dB] - Decibels. Resolution 0.1 dB. Range: [0.0:200.0]. Noise level. Type: Continuous */
MEASUREMENT_TYPE_NOISE_LEVEL = 19;
/* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Ammonia concentration. Type: Continuous */
MEASUREMENT_TYPE_NH3_GAS = 20;
/* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Methane concentration. Type: Continuous */
MEASUREMENT_TYPE_CH4_GAS = 21;
/* [kPa] - Kilopascal (1kPa = 1000Pa, 100kPa = 1bar). Resolution 1kPa. Range [0:200000]. Pressure. Type: Continuous */
MEASUREMENT_TYPE_HIGH_PRESSURE = 22;
/* [mm] - Millimeter. Resolution 1mm. Range [0:100000]. Distance. Type: Continuous */
MEASUREMENT_TYPE_DISTANCE_MM = 23;
/* [l] - Liter. Resolution 1l. Range [0:1000000]. Accumulative water meter (minor). Type: Continuous */
MEASUREMENT_TYPE_WATER_METER_ACC_MINOR = 24;
/* [hl] - Hectoliter. Resolution 1hl. Range [0:1000000]. Accumulative water meter (major). Type: Continuous */
MEASUREMENT_TYPE_WATER_METER_ACC_MAJOR = 25;
/* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. Carbon dioxide concentration. Type: Continuous */
MEASUREMENT_TYPE_CO2_GAS = 26;
/* [% RH] - Relative humidity. Resolution 0.1%. Range [0.0:100.0]. Type: Continuous */
MEASUREMENT_TYPE_HUMIDITY_ACCURATE = 27;
/* [sIAQ] - Static IAQ index. Resolution 1IAQ. Range [0:10000]. To get static IAQ index the value should be divided by 3. */
/* Sensor return also calibration status as metadata (is the remainder when the absolute value is divided by 3): */
/* - 0: Calibration required (sensor returns not accurate values) */
/* - 1: Calibration on-going (sensor returns not accurate values) */
/* - 2: Calibration done (best accuracy of IAQ sensor) */
/* Type: Continuous */
MEASUREMENT_TYPE_STATIC_IAQ = 28;
/* [ppm] - Parts per million. Resolution 1ppm. Range [0:1000000]. CO2 equivalent. */
/* To get CO2 equivalent the value should be divided by 3. */
/* Sensor return also calibration status as metadata (is the remainder when the absolute value is divided by 3): */
/* - 0: Calibration required (sensor returns not accurate values) */
/* - 1: Calibration on-going (sensor returns not accurate values) */
/* - 2: Calibration done (best accuracy of IAQ sensor) */
/* Type: Continuous */
MEASUREMENT_TYPE_CO2_EQUIVALENT = 29;
/* [ppm] - Parts per million. Resolution 1ppm. Range [0:100000]. Breath VOC estimate. */
/* To get breath VOC estimate the value should be divided by 3. */
/* Sensor return also calibration status as metadata (is the remainder when the absolute value is divided by 3): */
/* - 0: Calibration required (sensor returns not accurate values) */
/* - 1: Calibration on-going (sensor returns not accurate values) */
/* - 2: Calibration done (best accuracy of IAQ sensor) */
/* Type: Continuous */
MEASUREMENT_TYPE_BREATH_VOC = 30;
/* Special measurement type reserved for cellular gateway. */
/* Type: Continuous */
MEASUREMENT_TYPE_CELLULAR_GATEWAY = 31;
/* [%] - Percentage. Resolution 0.01%. Range [0.00:100.00]. Type: Continuous */
MEASUREMENT_TYPE_PERCENTAGE = 32;
/* [mV] - Milivolt. Resolution 0.1mV. Range [0.0:100000.0]. Type: Continuous */
MEASUREMENT_TYPE_VOLTAGE = 33;
/* [mA] - Milliampere. Resolution 0.01mA. Range [0.0:10000.00]. Type: Continuous */
MEASUREMENT_TYPE_CURRENT = 34;
/* [NB] Number of pulses. Resolution 1 pulse. Range [0:1000000]. Type: Continuous */
MEASUREMENT_TYPE_PULSE_CNT_ACC_MINOR = 35;
/* [kNB] Number of kilopulses. Resolution 1 kilopulse. Range [0:1000000]. Type: Continuous */
MEASUREMENT_TYPE_PULSE_CNT_ACC_MAJOR = 36;
/* [Wh] - Watt-hour; Resolution 1Wh. Range [0:1000000]. Number of watt-hours in a single period. Type: Continuous */
MEASUREMENT_TYPE_ELEC_METER_ACC_MINOR = 37;
/* [kWh] - Kilowatt-hour; Resolution 1kWh. Range [0:1000000]. Number of kilowatt-hours in a single period. Type: Continuous */
MEASUREMENT_TYPE_ELEC_METER_ACC_MAJOR = 38;
/* [NB] Number of pulses (wide range). Resolution 1 pulse. Range [0:999999]. Type: Continuous */
MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MINOR = 39;
/* [MNB] Number of megapulses (wide range). Resolution 1 megapulse. Range [0:999999]. Type: Continuous */
MEASUREMENT_TYPE_PULSE_CNT_ACC_WIDE_MAJOR = 40;
/* [mA] - Milliampere. Resolution 0.001mA. Range [-4 000.000:4 000.000]. Type: Continuous */
MEASUREMENT_TYPE_CURRENT_PRECISE = 41;
/* Sign indicates state: (+) ON, (-) OFF. Type: Binary */
MEASUREMENT_TYPE_OUTPUT_CONTROL = 42;
}

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

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. */
/* - BREATH_VOC - [ppm] - Parts per million. Resolution 1ppm. Range [0:100000]. Breath VOC estimate. */
/* - PERCENTAGE - [%] - Percentage. Resolution 0.01%. Range [0.00:100.00]. */
/* - VOLTAGE - [mV] - Milivolt. Resolution 0.1mV. Range [0.0:100000.0]. */
/* - CURRENT - [mA] - Miliampere. Resolution 0.01mA. Range [0.00:10000.00]. */
/* - PULSE_CNT_ACC_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [0:1000000]. Accumulative pulse counter (minor). */
/* - PULSE_CNT_ACC_MAJOR - [kNB] - Number of kilopulses. Resolution 1 kilopulse. Range [0:1000000]. */
/* Accumulative pulse counter (major). */
/* - ELEC_METER_ACC_MINOR - [Wh] - Watt-hour. Resolution 1Wh. Range [0:1000000]. Accumulative electricity meter (minor). */
/* - ELEC_METER_ACC_MAJOR - [kWh] - Kilowatt-hour. Resolution 1kWh. Range [0:1000000]. Accumulative electricity meter (major). */
/* - PULSE_CNT_ACC_WIDE_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [0:999999]. */
/* Accumulative pulse counter wide range (minor). */
/* - PULSE_CNT_ACC_WIDE_MAJOR - [MNB] - Number of megapulses. Resolution 1 megapulse. Range [0:999999]. */
/* Accumulative pulse counter wide range (major). */
/* - CURRENT_PRECISE - [mA] - Miliampere. Resolution 0.001mA. Range [-4 000.000:4 000.000]. */
/* - OUTPUT_CONTROL - Not applicable */
/* Encoding R: used to set relative values in the Rules (e.g. differential threshold and hysteresis) */
/* - TEMPERATURE - [°C] - Celsius degree. Resolution 0.1°C. Range [0.1:4273.2]. */
@ -88,11 +96,19 @@ option java_outer_classname = "ProtoRuleProtos";
/* - CO2_EQUIVALENT - [ppm] - Parts per million. Resolution 1ppm. Range [1:1000000]. Carbon dioxide equivalent. */
/* - BREATH_VOC - [ppm] - Parts per million. Resolution 1ppm. Range [1:100000]. Breath VOC estimate. */
/* - PERCENTAGE - [%] - Percentage. Resolution 0.01%. Range [0.01:100.00]. */
/* - VOLTAGE - [mV] - Milivolt. Resolution 0.1mV. Range [0.1:100000.0]. */
/* - CURRENT - [mA] - Miliampere. Resolution 0.01mA. Range [0.01:10000.00]. */
/* - PULSE_CNT_ACC_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [1:1000000]. Accumulative pulse counter (minor). */
/* - PULSE_CNT_ACC_MAJOR - [kNB] - Number of kilopulses. Resolution 1 kilopulse. Range [1:1000000]. */
/* Accumulative pulse counter (major). */
/* - ELEC_METER_ACC_MINOR - [Wh] - Watt-hour. Resolution 1Wh. Range [1:1000000]. Accumulative electricity meter (minor). */
/* - ELEC_METER_ACC_MAJOR - [kWh] - Kilowatt-hour. Resolution 1kWh. Range [1:1000000]. Accumulative electricity meter (major). */
/* - PULSE_CNT_ACC_WIDE_MINOR - [NB] - Number of pulses. Resolution 1 pulse. Range [1:999999]. */
/* Accumulative pulse counter wide range (minor). */
/* - PULSE_CNT_ACC_WIDE_MAJOR - [MNB] - Number of megapulses. Resolution 1 megapulse. Range [1:999999]. */
/* Accumulative pulse counter wide range (major). */
/* - CURRENT_PRECISE - [mA] - Miliampere. Resolution 0.001mA. Range [0.001:8 000.000]. */
/* - OUTPUT_CONTROL - Not applicable */
/* Condition to be checked by the device. If the condition is true, an action is triggered */
enum Condition {
@ -202,6 +218,10 @@ enum Action {
/* To trigger the transmission with ACK */
ACTION_TRIGGER_TRANSMISSION_WITH_ACK = 3;
/* To change BLE advertising period mode to fast (with lower user-configured advertising interval). */
/* Once the rule is deactived avertising period mode returns to previously configured value. */
ACTION_FAST_ADVERTISING_MODE = 4;
}
/* Type of a rule calendars. */

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

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

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

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());
try (WsClient wsClient = wsClientFactory.createClient(accessToken)) {
stopWatch.start();
wsClient.subscribeForTelemetry(devices, TransportHealthChecker.TEST_TELEMETRY_KEY).waitForReply();
reporter.reportLatency(Latencies.WS_SUBSCRIBE, stopWatch.getTime());
for (BaseHealthChecker<C, T> healthChecker : healthCheckers) {
check(healthChecker, wsClient);
}

4
pom.xml

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

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

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

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

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

@ -20,12 +20,19 @@ import {
EntityTableConfig
} from '@home/models/entity/entities-table-config.models';
import {
ActionStatus,
actionStatusTranslations,
ActionType,
actionTypeTranslations,
AuditLog,
AuditLogMode
} from '@shared/models/audit-log.models';
import { EntityTypeResource, entityTypeTranslations } from '@shared/models/entity-type.models';
import {
AliasEntityType,
EntityType,
EntityTypeResource,
entityTypeTranslations
} from '@shared/models/entity-type.models';
import { AuditLogService } from '@core/http/audit-log.service';
import { TranslateService } from '@ngx-translate/core';
import { DatePipe } from '@angular/common';
@ -82,7 +89,7 @@ export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLin
if (this.auditLogMode !== AuditLogMode.ENTITY) {
this.columns.push(
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%'),
);
}
@ -95,9 +102,9 @@ export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLin
this.columns.push(
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%',
(entity) => translate.instant(actionStatusTranslations.get(entity.actionStatus)))
(entity) => this.getActionStatusTranslation(entity.actionStatus))
);
this.cellActionDescriptors.push(
@ -105,11 +112,32 @@ export class AuditLogTableConfig extends EntityTableConfig<AuditLog, TimePageLin
name: this.translate.instant('audit-log.details'),
icon: 'more_horiz',
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>> {
switch (this.auditLogMode) {
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 {
const brokerConfigState = {
...brokerConfig,
version: brokerConfig.version || 5,
clientId: brokerConfig.clientId || 'tb_gw_' + generateSecret(5),
};
this.brokerConfigFormGroup.reset(brokerConfigState, {emitEvent: false});
const {
version = 5,
clientId = `tb_gw_${generateSecret(5)}`,
security = {},
} = brokerConfig;
this.brokerConfigFormGroup.reset({ ...brokerConfig, version, clientId, security }, { emitEvent: false });
this.cdr.markForCheck();
}

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 fxFlex fxLayout="column" class="tb-mapping-table-content">
<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">
<span class="tb-mapping-table-title">{{mappingTypeTranslationsMap.get(mappingType) | translate}}</span>
</div>
@ -75,15 +75,22 @@
</mat-header-cell>
<mat-cell *matCellDef="let mapping; let i = index"
[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
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
(click)="manageMapping($event, i)">
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button
matTooltip="{{ 'action.delete' | translate }}"
matTooltipPosition="above"
(click)="deleteMapping($event, i)">
<tb-icon>delete</tb-icon>
</button>
</ng-template>
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<ng-container [ngTemplateOutlet]="rowActions"></ng-container>
</div>
<div fxHide fxShow.lt-lg fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-icon-button
@ -92,14 +99,7 @@
<mat-icon class="material-icons">more_vert</mat-icon>
</button>
<mat-menu #cellActionsMenu="matMenu" xPosition="before">
<button mat-icon-button
(click)="manageMapping($event, i)">
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button
(click)="deleteMapping($event, i)">
<tb-icon>delete</tb-icon>
</button>
<ng-container [ngTemplateOutlet]="rowActions"></ng-container>
</mat-menu>
</div>
</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>
</mat-tab>
<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>
</mat-tab>
</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 {
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 {
ControlValueAccessor,
FormBuilder,
FormControl,
FormGroup,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormControl,
ValidationErrors,
Validator,
} from '@angular/forms';
import { ConnectorType, ModbusBasicConfig } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { ModbusBasicConfig } from '@home/components/widget/lib/gateway/gateway-widget.models';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { takeUntil } from 'rxjs/operators';
@ -33,6 +35,7 @@ import { Subject } from 'rxjs';
import { EllipsisChipListDirective } from '@shared/directives/ellipsis-chip-list.directive';
import { ModbusSlaveConfigComponent } from '../modbus-slave-config/modbus-slave-config.component';
import { ModbusMasterTableComponent } from '../modbus-master-table/modbus-master-table.component';
import { isEqual } from '@core/utils';
@Component({
selector: 'tb-modbus-basic-config',
@ -66,6 +69,7 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat
@Input() generalTabContent: TemplateRef<any>;
basicFormGroup: FormGroup;
enableSlaveControl: FormControl<boolean>;
onChange: (value: ModbusBasicConfig) => void;
onTouched: () => void;
@ -77,13 +81,21 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat
master: [],
slave: [],
});
this.enableSlaveControl = new FormControl(false);
this.basicFormGroup.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => {
this.onChange(value);
.subscribe(({ master, slave }) => {
this.onChange({ master, slave: slave ?? {} });
this.onTouched();
});
this.enableSlaveControl.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(enable => {
this.updateSlaveEnabling(enable);
this.basicFormGroup.get('slave').updateValueAndValidity({emitEvent: !!this.onChange});
});
}
ngOnDestroy(): void {
@ -106,11 +118,25 @@ export class ModbusBasicConfigComponent implements ControlValueAccessor, Validat
};
this.basicFormGroup.setValue(editedBase, {emitEvent: false});
this.enableSlaveControl.setValue(!!basicConfig.slave && !isEqual(basicConfig.slave, {}));
}
validate(): ValidationErrors | null {
return this.basicFormGroup.valid ? null : {
basicFormGroup: {valid: false}
};
validate(basicFormControl: UntypedFormControl): ValidationErrors | null {
const { master, slave } = basicFormControl.value;
const isEmpty = !master?.slaves?.length && (isEqual(slave, {}) || !slave);
if (!this.basicFormGroup.valid || isEmpty) {
return {
basicFormGroup: {valid: false}
};
}
return null;
}
private updateSlaveEnabling(isEnabled: boolean): void {
if (isEnabled) {
this.basicFormGroup.get('slave').enable({emitEvent: false});
} else {
this.basicFormGroup.get('slave').disable({emitEvent: false});
}
}
}

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-header fxLayout="row wrap">
<mat-panel-title>
<div class="title-container">
<span *ngIf="isMaster else tagName">
{{ keyControl.get('tag').value }}{{ '-' }}{{ keyControl.get('value').value }}
</span>
<ng-template #tagName>{{ keyControl.get('tag').value }}</ng-template>
<div *ngIf="isMaster else tagName" class="title-container" tbTruncateWithTooltip>
{{ keyControl.get('tag').value }}{{ '-' }}{{ keyControl.get('value').value }}
</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-expansion-panel-header>
<ng-template matExpansionPanelContent>
@ -98,7 +102,7 @@
name="value"
formControlName="objectsCount"
placeholder="{{ 'gateway.set' | translate }}"
[readonly]="!editableDataTypes.includes(keyControl.get('type').value)"
[readonly]="!ModbusEditableDataTypes.includes(keyControl.get('type').value)"
/>
</mat-form-field>
</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;
.title-container {
max-width: 11vw;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap
width: 180px;
}
.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 {
ModbusDataType,
ModbusEditableDataTypes,
ModbusFunctionCodeTranslationsMap,
ModbusObjectCountByDataType,
ModbusValue,
@ -40,6 +41,7 @@ import { generateSecret } from '@core/utils';
import { coerceBoolean } from '@shared/decorators/coercion';
import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { TruncateWithTooltipDirective } from '@shared/directives/truncate-with-tooltip.directive';
@Component({
selector: 'tb-modbus-data-keys-panel',
@ -50,6 +52,7 @@ import { Subject } from 'rxjs';
CommonModule,
SharedModule,
GatewayHelpLinkPipe,
TruncateWithTooltipDirective,
]
})
export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
@ -72,14 +75,15 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
functionCodesMap = new Map();
defaultFunctionCodes = [];
readonly editableDataTypes = [ModbusDataType.BYTES, ModbusDataType.BITS, ModbusDataType.STRING];
readonly ModbusEditableDataTypes = ModbusEditableDataTypes;
readonly ModbusFunctionCodeTranslationsMap = ModbusFunctionCodeTranslationsMap;
private destroy$ = new Subject<void>();
private readonly defaultReadFunctionCodes = [3, 4];
private readonly defaultWriteFunctionCodes = [5, 6, 15, 16];
private readonly stringAttrUpdatesWriteFunctionCodes = [6, 16];
private readonly bitsReadFunctionCodes = [1, 2];
private readonly defaultWriteFunctionCodes = [6, 16];
private readonly bitsWriteFunctionCodes = [5, 15];
constructor(private fb: UntypedFormBuilder) {}
@ -161,7 +165,7 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
private observeKeyDataType(keyFormGroup: FormGroup): void {
keyFormGroup.get('type').valueChanges.pipe(takeUntil(this.destroy$)).subscribe(dataType => {
if (!this.editableDataTypes.includes(dataType)) {
if (!this.ModbusEditableDataTypes.includes(dataType)) {
keyFormGroup.get('objectsCount').patchValue(ModbusObjectCountByDataType[dataType], {emitEvent: false});
}
this.updateFunctionCodes(keyFormGroup, dataType);
@ -177,23 +181,23 @@ export class ModbusDataKeysPanelComponent implements OnInit, OnDestroy {
}
private getFunctionCodes(dataType: ModbusDataType): number[] {
const writeFunctionCodes = [
...(dataType === ModbusDataType.BITS ? this.bitsWriteFunctionCodes : []), ...this.defaultWriteFunctionCodes
];
if (this.keysType === ModbusValueKey.ATTRIBUTES_UPDATES) {
return dataType === ModbusDataType.STRING
? this.stringAttrUpdatesWriteFunctionCodes
: this.defaultWriteFunctionCodes;
return writeFunctionCodes.sort((a, b) => a - b);
}
const functionCodes = [...this.defaultReadFunctionCodes];
if (dataType === ModbusDataType.BITS) {
const bitsFunctionCodes = [1, 2];
functionCodes.push(...bitsFunctionCodes);
functionCodes.sort();
functionCodes.push(...this.bitsReadFunctionCodes);
}
if (this.keysType === ModbusValueKey.RPC_REQUESTS) {
functionCodes.push(...this.defaultWriteFunctionCodes);
functionCodes.push(...writeFunctionCodes);
}
return functionCodes;
return functionCodes.sort((a, b) => a - b);
}
private getDefaultFunctionCodes(): number[] {

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-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">
<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">
<span class="tb-master-table-title">{{ 'gateway.servers-slaves' | translate}}</span>
</div>
@ -63,33 +66,40 @@
<mat-header-cell *matHeaderCellDef class="table-value-column">
{{ 'gateway.name' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let mapping" class="table-value-column">
{{ mapping['name'] }}
<mat-cell *matCellDef="let slave" class="table-value-column">
{{ slave['name'] }}
</mat-cell>
</ng-container>
<ng-container [matColumnDef]="'type'">
<mat-header-cell *matHeaderCellDef class="table-value-column">
{{ 'gateway.client-communication-type' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let mapping" class="table-value-column">
{{ ModbusProtocolLabelsMap.get(mapping['type']) }}
<mat-cell *matCellDef="let slave" class="table-value-column">
{{ ModbusProtocolLabelsMap.get(slave['type']) }}
</mat-cell>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<mat-header-cell *matHeaderCellDef
[ngStyle.gt-md]="{ minWidth: '96px', maxWidth: '96px', width: '96px', textAlign: 'center'}">
</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'}">
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<ng-template #rowActions>
<button mat-icon-button
matTooltip="{{ 'action.edit' | translate }}"
matTooltipPosition="above"
(click)="manageSlave($event, i)">
<tb-icon>edit</tb-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>
</button>
</ng-template>
<div fxHide fxShow.gt-md fxFlex fxLayout="row" fxLayoutAlign="end">
<ng-container [ngTemplateOutlet]="rowActions"></ng-container>
</div>
<div fxHide fxShow.lt-lg fxFlex fxLayout="row" fxLayoutAlign="end">
<button mat-icon-button
@ -98,20 +108,13 @@
<mat-icon class="material-icons">more_vert</mat-icon>
</button>
<mat-menu #cellActionsMenu="matMenu" xPosition="before">
<button mat-icon-button
(click)="manageSlave($event, i)">
<tb-icon>edit</tb-icon>
</button>
<button mat-icon-button
(click)="deleteMapping($event, i)">
<tb-icon>delete</tb-icon>
</button>
<ng-container [ngTemplateOutlet]="rowActions"></ng-container>
</mat-menu>
</div>
</mat-cell>
</ng-container>
<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>
<section [fxShow]="!textSearchMode && (dataSource.isEmpty() | async)" fxLayoutAlign="center center"
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,
FormArray,
FormBuilder,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
UntypedFormGroup,
ValidationErrors,
Validator,
} from '@angular/forms';
import {
ModbusMasterConfig,
ModbusProtocolLabelsMap,
ModbusSlaveInfo,
ModbusValues,
SlaveConfig
} from '@home/components/widget/lib/gateway/gateway-widget.models';
import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils';
import { isDefinedAndNotNull } from '@core/utils';
import { SharedModule } from '@shared/shared.module';
import { CommonModule } from '@angular/common';
import { ModbusSlaveDialogComponent } from '../modbus-slave-dialog/modbus-slave-dialog.component';
@ -62,16 +61,11 @@ import { TbTableDatasource } from '@shared/components/table/table-datasource.abs
useExisting: forwardRef(() => ModbusMasterTableComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => ModbusMasterTableComponent),
multi: true
}
],
standalone: true,
imports: [CommonModule, SharedModule]
})
export class ModbusMasterTableComponent implements ControlValueAccessor, Validator, AfterViewInit, OnInit, OnDestroy {
export class ModbusMasterTableComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy {
@ViewChild('searchInput') searchInputField: ElementRef;
@ -138,12 +132,6 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat
this.pushDataAsFormArrays(master.slaves);
}
validate(): ValidationErrors | null {
return this.slaves.controls.length ? null : {
slavesFormGroup: {valid: false}
};
}
enterFilterMode(): void {
this.textSearchMode = true;
this.cdr.detectChanges();
@ -164,12 +152,12 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat
}
const withIndex = isDefinedAndNotNull(index);
const value = withIndex ? this.slaves.at(index).value : {};
this.dialog.open<ModbusSlaveDialogComponent, any, any>(ModbusSlaveDialogComponent, {
this.dialog.open<ModbusSlaveDialogComponent, ModbusSlaveInfo, ModbusValues>(ModbusSlaveDialogComponent, {
disableClose: true,
panelClass: ['tb-dialog', 'tb-fullscreen-dialog'],
data: {
value,
buttonTitle: withIndex ? 'action.add' : 'action.apply'
buttonTitle: withIndex ? 'action.apply' : 'action.add'
}
}).afterClosed()
.pipe(take(1), takeUntil(this.destroy$))
@ -185,7 +173,7 @@ export class ModbusMasterTableComponent implements ControlValueAccessor, Validat
});
}
deleteMapping($event: Event, index: number): void {
deleteSlave($event: Event, index: number): void {
if ($event) {
$event.stopPropagation();
}

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

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>
<div mat-dialog-content [formGroup]="slaveConfigFormGroup" class="tb-form-panel">
<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">
<mat-form-field class="tb-flex no-gap" appearance="outline" subscriptSizing="dynamic">
<input matInput name="value" formControlName="name" placeholder="{{ 'gateway.set' | translate }}"/>
@ -306,7 +306,7 @@
</mat-slide-toggle>
</div>
<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">
<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 }}"/>
@ -314,7 +314,7 @@
</div>
</div>
<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">
<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 }}"/>
@ -322,7 +322,7 @@
</div>
</div>
<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">
<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 }}"/>
@ -330,7 +330,7 @@
</div>
</div>
<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">
<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 }}"/>

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;
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]],
parity: [ModbusParity.None],
strict: [true],
unitId: [0, [Validators.required]],
unitId: [null, [Validators.required]],
deviceName: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
deviceType: ['', [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
sendDataOnlyOnChange: [false],
@ -173,10 +173,10 @@ export class ModbusSlaveDialogComponent extends DialogComponent<ModbusSlaveDialo
retries: [true],
retryOnEmpty: [true],
retryOnInvalid: [true],
pollPeriod: [5000],
connectAttemptTimeMs: [5000],
connectAttemptCount: [5],
waitAfterFailedAttemptsMs: [300000],
pollPeriod: [5000, [Validators.required]],
connectAttemptTimeMs: [5000, [Validators.required]],
connectAttemptCount: [5, [Validators.required]],
waitAfterFailedAttemptsMs: [300000, [Validators.required]],
values: [{}],
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
* limitations under the License.
*/
:host {
.mat-mdc-tab-body-wrapper {
min-height: 320px;
}
:host ::ng-deep .mat-mdc-tab-body-wrapper {
min-height: 320px;
}
::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 {
MappingTableComponent
} from '@home/components/widget/lib/gateway/connectors-configuration/mapping-table/mapping-table.component';
import { isDefinedAndNotNull } from '@core/utils';
@Component({
selector: 'tb-mqtt-basic-config',
@ -84,7 +85,7 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator
mappingTypes = MappingType;
basicFormGroup: FormGroup;
private onChange: (value: string) => void;
private onChange: (value: MQTTBasicConfig) => void;
private onTouched: () => void;
private destroy$ = new Subject<void>();
@ -100,7 +101,7 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator
this.basicFormGroup.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(value => {
this.onChange(value);
this.onChange(this.getMappedMQTTConfig(value));
this.onTouched();
});
}
@ -110,7 +111,7 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator
this.destroy$.complete();
}
registerOnChange(fn: (value: string) => void): void {
registerOnChange(fn: (value: MQTTBasicConfig) => void): void {
this.onChange = fn;
}
@ -135,13 +136,30 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator
this.basicFormGroup.setValue(editedBase, {emitEvent: false});
}
private getMappedMQTTConfig(basicConfig: MQTTBasicConfig): MQTTBasicConfig {
let { broker, workers, dataMapping, requestsMapping } = basicConfig || {};
if (isDefinedAndNotNull(workers.maxNumberOfWorkers) || isDefinedAndNotNull(workers.maxMessageNumberPerWorker)) {
broker = {
...broker,
...workers,
};
}
if ((requestsMapping as RequestMappingData[])?.length) {
requestsMapping = this.getRequestDataObject(requestsMapping as RequestMappingData[]);
}
return { broker, workers, dataMapping, requestsMapping };
}
validate(): ValidationErrors | null {
return this.basicFormGroup.valid ? null : {
basicFormGroup: {valid: false}
};
}
private getRequestDataArray(value: Record<RequestType, RequestMappingData>): RequestMappingData[] {
private getRequestDataArray(value: Record<RequestType, RequestMappingData[]>): RequestMappingData[] {
const mappingConfigs = [];
if (isObject(value)) {
@ -157,4 +175,17 @@ export class MqttBasicConfigComponent implements ControlValueAccessor, Validator
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)]],
showMap: [false, []],
security: [SecurityPolicy.BASIC128, []],
identity: [{}, [Validators.required]]
identity: []
});
this.serverConfigFormGroup.valueChanges.pipe(
@ -116,16 +116,25 @@ export class OpcServerConfigComponent implements ControlValueAccessor, Validator
}
writeValue(serverConfig: ServerConfig): void {
const { timeoutInMillis, scanPeriodInMillis, enableSubscriptions, subCheckPeriodInMillis, showMap, security } = serverConfig;
const serverConfigState = {
const {
timeoutInMillis = 1000,
scanPeriodInMillis = 1000,
enableSubscriptions = true,
subCheckPeriodInMillis = 10,
showMap = false,
security = SecurityPolicy.BASIC128,
identity = {},
} = serverConfig;
this.serverConfigFormGroup.reset({
...serverConfig,
timeoutInMillis: timeoutInMillis || 1000,
scanPeriodInMillis: scanPeriodInMillis || 1000,
enableSubscriptions: enableSubscriptions || true,
subCheckPeriodInMillis: subCheckPeriodInMillis || 10,
showMap: showMap || false,
security: security || SecurityPolicy.BASIC128,
};
this.serverConfigFormGroup.reset(serverConfigState, {emitEvent: false});
timeoutInMillis,
scanPeriodInMillis,
enableSubscriptions,
subCheckPeriodInMillis,
showMap,
security,
identity,
}, { emitEvent: false });
}
}

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 {
AddConnectorConfigData,
ConnectorBaseConfig,
ConnectorBaseInfo,
ConnectorConfigurationModes,
ConnectorType,
GatewayConnector,
@ -60,7 +61,7 @@ import {
} from './gateway-widget.models';
import { MatDialog } from '@angular/material/dialog';
import { AddConnectorDialogComponent } from '@home/components/widget/lib/gateway/dialog/add-connector-dialog.component';
import { debounceTime, take, takeUntil, tap } from 'rxjs/operators';
import { debounceTime, filter, take, takeUntil, tap } from 'rxjs/operators';
import { ErrorStateMatcher } from '@angular/material/core';
import { PageData } from '@shared/models/page/page-data';
@ -133,6 +134,8 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
private basicConfigSub: Subscription;
private jsonConfigSub: Subscription;
private subscriptionOptions: WidgetSubscriptionOptions = {
callbacks: {
onDataUpdated: () => this.ctx.ngZone.run(() => {
@ -209,17 +212,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
});
this.connectorForm.get('configurationJson').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((config) => {
const basicConfig = this.connectorForm.get('basicConfig');
const type = this.connectorForm.get('type').value;
const mode = this.connectorForm.get('mode').value;
if (!isEqual(config, basicConfig?.value) && this.allowBasicConfig.has(type) && mode === ConnectorConfigurationModes.ADVANCED) {
this.connectorForm.get('basicConfig').patchValue(config, {emitEvent: false});
}
});
this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = (data: AttributeData, sortHeaderId: string) => {
switch (sortHeaderId) {
@ -275,7 +267,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
saveConnector(): void {
const value = this.connectorForm.get('type').value === ConnectorType.MQTT ? this.getMappedMQTTValue() : this.connectorForm.value;
const value = { ...this.connectorForm.value };
value.configuration = camelCase(value.name) + '.json';
delete value.basicConfig;
if (value.type !== ConnectorType.GRPC) {
@ -336,20 +328,6 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
});
}
private getMappedMQTTValue(): GatewayConnector {
const value = this.connectorForm.value;
return {
...value,
configurationJson: {
...value.configurationJson,
broker: {
...value.configurationJson.broker,
...value.configurationJson.workers,
}
}
};
}
private updateData(reload: boolean = false): void {
this.pageLink.sortOrder.property = this.sort.active;
this.pageLink.sortOrder.direction = Direction[this.sort.direction.toUpperCase()];
@ -383,11 +361,28 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
}
const sharedIndex = this.sharedAttributeData.findIndex(data => {
const sharedData = data.value;
return sharedData.name === connectorData.name && sharedData.ts && sharedData.ts <= connectorData.ts;
const hasSameName = sharedData.name === connectorData.name;
const hasEmptyConfig = isEqual(sharedData.configurationJson, {}) && hasSameName;
const hasSameConfig = this.hasSameConfig(sharedData.configurationJson, connectorData.configurationJson);
const isRecentlyCreated = sharedData.ts && sharedData.ts <= connectorData.ts;
return hasSameName && isRecentlyCreated && (hasSameConfig || hasEmptyConfig);
});
return sharedIndex !== -1;
}
private hasSameConfig(sharedDataConfigJson: ConnectorBaseInfo, connectorDataConfigJson: ConnectorBaseInfo): boolean {
const { name, id, enableRemoteLogging, logLevel, ...sharedDataConfig } = sharedDataConfigJson;
const {
name: connectorName,
id: connectorId,
enableRemoteLogging: connectorEnableRemoteLogging,
logLevel: connectorLogLevel,
...connectorConfig
} = connectorDataConfigJson;
return isEqual(sharedDataConfig, connectorConfig);
}
private combineData(): void {
this.dataSource.data = [...this.activeData, ...this.inactiveData, ...this.sharedAttributeData].filter((item, index, self) =>
index === self.findIndex((t) => t.key === item.key)
@ -684,6 +679,7 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
this.basicConfigSub.unsubscribe();
}
this.basicConfigSub = this.connectorForm.get('basicConfig').valueChanges.pipe(
filter(() => !!this.initialConnector),
takeUntil(this.destroy$)
).subscribe((config) => {
const configJson = this.connectorForm.get('configurationJson');
@ -696,6 +692,22 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
});
}
private createJsonConfigWatcher(): void {
if (this.jsonConfigSub) {
this.jsonConfigSub.unsubscribe();
}
this.jsonConfigSub = this.connectorForm.get('configurationJson').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe((config) => {
const basicConfig = this.connectorForm.get('basicConfig');
const type = this.connectorForm.get('type').value;
const mode = this.connectorForm.get('mode').value;
if (!isEqual(config, basicConfig?.value) && this.allowBasicConfig.has(type) && mode === ConnectorConfigurationModes.ADVANCED) {
this.connectorForm.get('basicConfig').patchValue(config, {emitEvent: false});
}
});
}
private confirmConnectorChange(): Observable<boolean> {
if (this.initialConnector && this.connectorForm.dirty) {
return this.dialogService.confirm(
@ -734,19 +746,18 @@ export class GatewayConnectorComponent extends PageComponent implements AfterVie
case ConnectorType.MQTT:
case ConnectorType.OPCUA:
case ConnectorType.MODBUS:
this.connectorForm.get('type').patchValue(connector.type, {emitValue: false, onlySelf: true});
this.connectorForm.get('basicConfig').setValue({}, {emitEvent: false});
this.connectorForm.get('mode').setValue(connector.mode || ConnectorConfigurationModes.BASIC, {emitEvent: false});
setTimeout(() => {
this.connectorForm.patchValue({...connector, mode: connector.mode || ConnectorConfigurationModes.BASIC});
this.createBasicConfigWatcher();
this.connectorForm.patchValue(connector, {emitEvent: false});
this.connectorForm.markAsPristine();
this.createBasicConfigWatcher();
});
break;
default:
this.connectorForm.patchValue({...connector, mode: null});
this.connectorForm.markAsPristine();
}
this.createJsonConfigWatcher();
}
private setClientData(data: PageData<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}"/>
</mat-form-field>
</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">
<mat-form-field>
<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,
GatewayConnectorDefaultTypesTranslatesMap,
HTTPMethods,
ModbusCodesTranslate,
ModbusCommandTypes,
noLeadTrailSpacesRegex,
RPCCommand,
RPCTemplateConfig,
@ -53,7 +51,7 @@ import {
} from '@shared/components/dialog/json-object-edit-dialog.component';
import { jsonRequired } from '@shared/components/json-object-edit.component';
import { deepClone } from '@core/utils';
import { filter, takeUntil, tap } from "rxjs/operators";
import { takeUntil, tap } from "rxjs/operators";
import { Subject } from "rxjs";
@Component({
@ -80,9 +78,7 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
saveTemplate: EventEmitter<RPCTemplateConfig> = new EventEmitter();
commandForm: FormGroup;
codesArray: Array<number> = [1, 2, 3, 4, 5, 6, 15, 16];
ConnectorType = ConnectorType;
modbusCommandTypes = Object.values(ModbusCommandTypes) as ModbusCommandTypes[];
bACnetRequestTypes = Object.values(BACnetRequestTypes) as BACnetRequestTypes[];
bACnetObjectTypes = Object.values(BACnetObjectTypes) as BACnetObjectTypes[];
bLEMethods = Object.values(BLEMethods) as BLEMethods[];
@ -98,7 +94,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
SocketMethodProcessingsTranslates = SocketMethodProcessingsTranslates;
SNMPMethodsTranslations = SNMPMethodsTranslations;
gatewayConnectorDefaultTypesTranslates = GatewayConnectorDefaultTypesTranslatesMap;
modbusCodesTranslate = ModbusCodesTranslate;
urlPattern = /^[-a-zA-Zd_$:{}?~+=\/.0-9-]*$/;
numbersOnlyPattern = /^[0-9]*$/;
@ -156,26 +151,6 @@ export class GatewayServiceRPCConnectorComponent implements OnInit, OnDestroy, C
withResponse: [false, []],
});
break;
case ConnectorType.MODBUS:
formGroup = this.fb.group({
tag: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],
type: [null, [Validators.required]],
functionCode: [null, [Validators.required]],
value: [null, []],
address: [null, [Validators.required, Validators.min(0), Validators.pattern(this.numbersOnlyPattern)]],
objectsCount: [null, [Validators.required, Validators.min(0), Validators.pattern(this.numbersOnlyPattern)]]
})
const valueForm = formGroup.get('value');
formGroup.get('functionCode').valueChanges.subscribe(value => {
if (value > 4) {
valueForm.addValidators([Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]);
} else {
valueForm.clearValidators();
valueForm.setValue(null);
}
valueForm.updateValueAndValidity();
})
break;
case ConnectorType.BACNET:
formGroup = this.fb.group({
method: [null, [Validators.required, Validators.pattern(noLeadTrailSpacesRegex)]],

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

@ -41,8 +41,32 @@
</button>
</ng-container>
<ng-template #connectorForm>
<tb-gateway-service-rpc-connector formControlName="params" [connectorType]="connectorType"
(sendCommand)="sendCommand()" (saveTemplate)="saveTemplate()"/>
<tb-gateway-service-rpc-connector
*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>
</div>
<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 {
padding: 0 5px;
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 {
ConnectorType,
GatewayConnectorDefaultTypesTranslatesMap,
RPCCommand,
RPCTemplate,
RPCTemplateConfig,
@ -71,6 +72,9 @@ export class GatewayServiceRPCComponent implements OnInit {
public connectorType: ConnectorType;
public templates: Array<RPCTemplate> = [];
readonly ConnectorType = ConnectorType;
readonly gatewayConnectorDefaultTypesTranslates = GatewayConnectorDefaultTypesTranslatesMap;
private subscription: IWidgetSubscription;
private subscriptionOptions: WidgetSubscriptionOptions = {
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 { ValueTypeData } from '@shared/models/constants';
export const noLeadTrailSpacesRegex = /^(?! )[\S\s]*(?<! )$/;
export const noLeadTrailSpacesRegex = /^\S+(?: \S+)*$/;
export enum StorageTypes {
MEMORY = 'memory',
@ -170,19 +170,27 @@ export interface ConnectorSecurity {
pathToCACert?: string;
pathToPrivateKey?: string;
pathToClientCert?: string;
mode?: ModeType;
}
export type ConnectorMapping = DeviceConnectorMapping | RequestMappingData | ConverterConnectorMapping;
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 {
dataMapping: ConverterConnectorMapping[];
requestsMapping: Record<RequestType, RequestMappingData> | RequestMappingData[];
requestsMapping: Record<RequestType, RequestMappingData[]> | RequestMappingData[];
broker: BrokerConfig;
workers: WorkersConfig;
workers?: WorkersConfig;
}
export interface OPCBasicConfig {
@ -311,35 +319,15 @@ export interface RPCCommand {
time: number;
}
export enum ModbusCommandTypes {
Bits = 'bits',
Bit = 'bit',
// eslint-disable-next-line id-blacklist
String = 'string',
Bytes = 'bytes',
Int8 = '8int',
Uint8 = '8uint',
Int16 = '16int',
Uint16 = '16uint',
Float16 = '16float',
Int32 = '32int',
Uint32 = '32uint',
Float32 = '32float',
Int64 = '64int',
Uint64 = '64uint',
Float64 = '64float'
}
export const ModbusCodesTranslate = new Map<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 const ModbusFunctionCodeTranslationsMap = new Map<number, string>([
[1, 'gateway.function-codes.read-coils'],
[2, 'gateway.function-codes.read-discrete-inputs'],
[3, 'gateway.function-codes.read-multiple-holding-registers'],
[4, 'gateway.function-codes.read-input-registers'],
[5, 'gateway.function-codes.write-single-coil'],
[6, 'gateway.function-codes.write-single-holding-register'],
[15, 'gateway.function-codes.write-multiple-coils'],
[16, 'gateway.function-codes.write-multiple-holding-registers']
]);
export enum BACnetRequestTypes {
@ -862,6 +850,8 @@ export enum ModbusDataType {
FLOAT64 = '64float'
}
export const ModbusEditableDataTypes = [ModbusDataType.BYTES, ModbusDataType.BITS, ModbusDataType.STRING];
export enum ModbusObjectCountByDataType {
'8int' = 1,
'8uint' = 1,
@ -920,19 +910,6 @@ export const ModbusKeysNoKeysTextTranslationsMap = new Map<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 {
slaves: SlaveConfig[];
}

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

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

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 {
this.tooltip.message = this.text;
this.renderer.setAttribute(this.elementRef.nativeElement, 'matTooltip', this.text);
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_CLEAR = 'ALARM_CLEAR',
ALARM_ASSIGNED = 'ALARM_ASSIGNED',
ALARM_DELETE = 'ALARM_DELETE',
ALARM_UNASSIGNED = 'ALARM_UNASSIGNED',
ADDED_COMMENT = 'ADDED_COMMENT',
UPDATED_COMMENT = 'UPDATED_COMMENT',
@ -91,6 +92,7 @@ export const actionTypeTranslations = new Map<ActionType, string>(
[ActionType.RELATIONS_DELETED, 'audit-log.type-relations-delete'],
[ActionType.ALARM_ACK, 'audit-log.type-alarm-ack'],
[ActionType.ALARM_CLEAR, 'audit-log.type-alarm-clear'],
[ActionType.ALARM_DELETE, 'audit-log.type-alarm-delete'],
[ActionType.ALARM_ASSIGNED, 'audit-log.type-alarm-assign'],
[ActionType.ALARM_UNASSIGNED, 'audit-log.type-alarm-unassign'],
[ActionType.ADDED_COMMENT, 'audit-log.type-added-comment'],

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 './pipe/public-api';
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-alarm-ack": "Acknowledged",
"type-alarm-clear": "Cleared",
"type-alarm-delete": "Deleted",
"type-alarm-assign": "Assigned",
"type-alarm-unassign": "Unassigned",
"type-added-comment": "Added comment",
@ -2923,6 +2924,16 @@
"from-device-request-settings": "Input request parsing",
"from-device-request-settings-hint": "These fields support JSONPath expressions to extract a name from incoming message.",
"function-code": "Function code",
"function-codes": {
"read-coils": "01 - Read Coils",
"read-discrete-inputs": "02 - Read Discrete Inputs",
"read-multiple-holding-registers": "03 - Read Multiple Holding Registers",
"read-input-registers": "04 - Read Input Registers",
"write-single-coil": "05 - Write Single Coil",
"write-single-holding-register": "06 - Write Single Holding Register",
"write-multiple-coils": "15 - Write Multiple Coils",
"write-multiple-holding-registers": "16 - Write Multiple Holding Registers"
},
"to-device-response-settings": "Output request processing",
"to-device-response-settings-hint": "For these fields you can use the following variables and they will be replaced with actual values: ${deviceName}, ${attributeKey}, ${attributeValue}",
"gateway": "Gateway",
@ -3129,14 +3140,6 @@
"template-name-duplicate": "Template with such name already exists, it will be updated.",
"command": "Command",
"params": "Params",
"read-coils": "01: Read Coils",
"read-discrete-inputs": "02: Read Discrete Inputs",
"read-multiple-holding-registers": "03: Read Multiple Holding Registers",
"read-input-registers": "04: Read Input Registers",
"write-single-coil": "05: Write Single Coil",
"write-single-holding-register": "06: Write Single Holding Register",
"write-multiple-coils": "15: Write Multiple Coils",
"write-multiple-holding-registers": "16: Write Multiple Holding Registers",
"json-value-invalid": "JSON value has an invalid format"
},
"rpc-methods": "RPC methods",
@ -3326,16 +3329,13 @@
"username": "Username",
"username-required": "Username is required.",
"unit-id-required": "Unit ID is required.",
"read-coils": "Read Coils",
"read-discrete-inputs": "Read Discrete Inputs",
"read-multiple-holding-registers": "Read Multiple Holding Register",
"read-input-registers": "Read Input Registers",
"write-coil": "Write Coil",
"write-coils": "Write Coils",
"write-register": "Write Register",
"write-registers": "Write Registers",
"hints": {
"modbus-server": "Starting with version 3.0, Gateway can run as a Modbus slave.",
"modbus-master": "Configuration sections for connecting to Modbus servers and reading data from them.",
"modbus-server": "Configuration section for the Modbus server, storing data and sending updates to the platform when changes occur or at fixed intervals.",
"remote-configuration": "Enables remote configuration and management of the gateway",
"remote-shell": "Enables remote control of the operating system with the gateway from the Remote Shell widget",
"host": "Hostname or IP address of platform server",

Loading…
Cancel
Save