Browse Source

Merge with master

pull/10155/head
ViacheslavKlimov 2 years ago
parent
commit
cccfd90d73
  1. 2
      application/src/main/data/json/system/widget_bundles/buttons.json
  2. 6
      application/src/main/data/json/system/widget_types/action_button.json
  3. 7
      application/src/main/data/json/system/widget_types/command_button.json
  4. 7
      application/src/main/data/json/system/widget_types/single_switch.json
  5. 1
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  6. 4
      application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java
  7. 26
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  8. 50
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  9. 16
      application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
  10. 62
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EdgeCommunicationFailureTriggerProcessor.java
  11. 60
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EdgeConnectionTriggerProcessor.java
  12. 15
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationRuleExportService.java
  13. 15
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationRuleImportService.java
  14. 8
      application/src/main/resources/thingsboard.yml
  15. 2
      application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java
  16. 6
      application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java
  17. 2
      common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java
  18. 5
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java
  19. 66
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EdgeCommunicationFailureNotificationInfo.java
  20. 66
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EdgeConnectionNotificationInfo.java
  21. 63
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeCommunicationFailureTrigger.java
  22. 62
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeConnectionTrigger.java
  23. 39
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeCommunicationFailureNotificationRuleTriggerConfig.java
  24. 44
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeConnectionNotificationRuleTriggerConfig.java
  25. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java
  26. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerType.java
  27. 3
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  28. 3
      common/data/src/main/java/org/thingsboard/server/common/data/util/TemplateUtils.java
  29. 2
      common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java
  30. 164
      common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java
  31. 6
      common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/DeviceSessionContext.java
  32. 62
      common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/ScheduledTask.java
  33. 19
      common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpDeviceSimulatorV2.java
  34. 42
      common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpTestV2.java
  35. 43
      common/transport/snmp/src/test/resources/snmp-device-profile-transport-config.json
  36. 13
      common/transport/snmp/src/test/resources/snmp-device-transport-config-v3.json
  37. 6
      common/transport/snmp/src/test/resources/snmp-device-transport-config.json
  38. 289
      common/transport/snmp/src/test/resources/snmp_device_profile.json
  39. 14
      common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java
  40. 3
      dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
  41. 12
      dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java
  42. 42
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java
  43. 34
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java
  44. 132
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java
  45. 72
      dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java
  46. 24
      dao/src/test/java/org/thingsboard/server/dao/service/EdgeEventServiceTest.java
  47. 38
      dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java
  48. 72
      dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceNoSqlSetNullEnabledTest.java
  49. 74
      dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceNoSqlTest.java
  50. 8
      transport/snmp/src/main/resources/tb-snmp-transport.yml
  51. 18
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html
  52. 4
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts
  53. 8
      ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts
  54. 61
      ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html
  55. 23
      ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts
  56. 12
      ui-ngx/src/app/shared/models/edge.models.ts
  57. 8
      ui-ngx/src/app/shared/models/limited-api.models.ts
  58. 22
      ui-ngx/src/app/shared/models/notification.models.ts
  59. 57
      ui-ngx/src/assets/help/en_US/notification/edge_communication_failure.md
  60. 44
      ui-ngx/src/assets/help/en_US/notification/edge_connection.md
  61. 2
      ui-ngx/src/assets/help/en_US/notification/rate_limits.md
  62. 34
      ui-ngx/src/assets/locale/locale.constant-en_US.json

2
application/src/main/data/json/system/widget_bundles/buttons.json

File diff suppressed because one or more lines are too long

6
application/src/main/data/json/system/widget_types/action_button.json

File diff suppressed because one or more lines are too long

7
application/src/main/data/json/system/widget_types/command_button.json

File diff suppressed because one or more lines are too long

7
application/src/main/data/json/system/widget_types/single_switch.json

File diff suppressed because one or more lines are too long

1
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -126,6 +126,7 @@ public class ThingsboardInstallService {
case "3.6.2": case "3.6.2":
log.info("Upgrading ThingsBoard from version 3.6.2 to 3.6.3 ..."); log.info("Upgrading ThingsBoard from version 3.6.2 to 3.6.3 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.6.2"); databaseEntitiesUpgradeService.upgradeDatabase("3.6.2");
systemDataLoaderService.updateDefaultNotificationConfigs();
case "3.6.3": case "3.6.3":
log.info("Upgrading ThingsBoard from version 3.6.3 to 3.7.0 ..."); log.info("Upgrading ThingsBoard from version 3.6.3 to 3.7.0 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.6.3"); databaseEntitiesUpgradeService.upgradeDatabase("3.6.3");

4
application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java

@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService; import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
@ -149,6 +150,9 @@ public class EdgeContextComponent {
@Autowired @Autowired
private ResourceService resourceService; private ResourceService resourceService;
@Autowired
private NotificationRuleProcessor notificationRuleProcessor;
@Autowired @Autowired
private AlarmEdgeProcessor alarmProcessor; private AlarmEdgeProcessor alarmProcessor;

26
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java

@ -39,6 +39,7 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.notification.rule.trigger.EdgeConnectionTrigger;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -264,7 +265,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
} }
private void onEdgeConnect(EdgeId edgeId, EdgeGrpcSession edgeGrpcSession) { private void onEdgeConnect(EdgeId edgeId, EdgeGrpcSession edgeGrpcSession) {
TenantId tenantId = edgeGrpcSession.getEdge().getTenantId(); Edge edge = edgeGrpcSession.getEdge();
TenantId tenantId = edge.getTenantId();
log.info("[{}][{}] edge [{}] connected successfully.", tenantId, edgeGrpcSession.getSessionId(), edgeId); log.info("[{}][{}] edge [{}] connected successfully.", tenantId, edgeGrpcSession.getSessionId(), edgeId);
sessions.put(edgeId, edgeGrpcSession); sessions.put(edgeId, edgeGrpcSession);
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock()); final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
@ -277,7 +279,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
save(tenantId, edgeId, DefaultDeviceStateService.ACTIVITY_STATE, true); save(tenantId, edgeId, DefaultDeviceStateService.ACTIVITY_STATE, true);
long lastConnectTs = System.currentTimeMillis(); long lastConnectTs = System.currentTimeMillis();
save(tenantId, edgeId, DefaultDeviceStateService.LAST_CONNECT_TIME, lastConnectTs); save(tenantId, edgeId, DefaultDeviceStateService.LAST_CONNECT_TIME, lastConnectTs);
pushRuleEngineMessage(tenantId, edgeId, lastConnectTs, TbMsgType.CONNECT_EVENT); pushRuleEngineMessage(tenantId, edge, lastConnectTs, TbMsgType.CONNECT_EVENT);
cancelScheduleEdgeEventsCheck(edgeId); cancelScheduleEdgeEventsCheck(edgeId);
scheduleEdgeEventsCheck(edgeGrpcSession); scheduleEdgeEventsCheck(edgeGrpcSession);
} }
@ -382,7 +384,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
} }
} }
private void onEdgeDisconnect(EdgeId edgeId, UUID sessionId) { private void onEdgeDisconnect(Edge edge, UUID sessionId) {
EdgeId edgeId = edge.getId();
log.info("[{}][{}] edge disconnected!", edgeId, sessionId); log.info("[{}][{}] edge disconnected!", edgeId, sessionId);
EdgeGrpcSession toRemove = sessions.get(edgeId); EdgeGrpcSession toRemove = sessions.get(edgeId);
if (toRemove.getSessionId().equals(sessionId)) { if (toRemove.getSessionId().equals(sessionId)) {
@ -398,7 +401,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
save(tenantId, edgeId, DefaultDeviceStateService.ACTIVITY_STATE, false); save(tenantId, edgeId, DefaultDeviceStateService.ACTIVITY_STATE, false);
long lastDisconnectTs = System.currentTimeMillis(); long lastDisconnectTs = System.currentTimeMillis();
save(tenantId, edgeId, DefaultDeviceStateService.LAST_DISCONNECT_TIME, lastDisconnectTs); save(tenantId, edgeId, DefaultDeviceStateService.LAST_DISCONNECT_TIME, lastDisconnectTs);
pushRuleEngineMessage(toRemove.getEdge().getTenantId(), edgeId, lastDisconnectTs, TbMsgType.DISCONNECT_EVENT); pushRuleEngineMessage(toRemove.getEdge().getTenantId(), edge, lastDisconnectTs, TbMsgType.DISCONNECT_EVENT);
cancelScheduleEdgeEventsCheck(edgeId); cancelScheduleEdgeEventsCheck(edgeId);
} else { } else {
log.debug("[{}] edge session [{}] is not available anymore, nothing to remove. most probably this session is already outdated!", edgeId, sessionId); log.debug("[{}] edge session [{}] is not available anymore, nothing to remove. most probably this session is already outdated!", edgeId, sessionId);
@ -453,16 +456,24 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
} }
} }
private void pushRuleEngineMessage(TenantId tenantId, EdgeId edgeId, long ts, TbMsgType msgType) { private void pushRuleEngineMessage(TenantId tenantId, Edge edge, long ts, TbMsgType msgType) {
try { try {
EdgeId edgeId = edge.getId();
ObjectNode edgeState = JacksonUtil.newObjectNode(); ObjectNode edgeState = JacksonUtil.newObjectNode();
if (msgType.equals(TbMsgType.CONNECT_EVENT)) { boolean isConnected = TbMsgType.CONNECT_EVENT.equals(msgType);
if (isConnected) {
edgeState.put(DefaultDeviceStateService.ACTIVITY_STATE, true); edgeState.put(DefaultDeviceStateService.ACTIVITY_STATE, true);
edgeState.put(DefaultDeviceStateService.LAST_CONNECT_TIME, ts); edgeState.put(DefaultDeviceStateService.LAST_CONNECT_TIME, ts);
} else { } else {
edgeState.put(DefaultDeviceStateService.ACTIVITY_STATE, false); edgeState.put(DefaultDeviceStateService.ACTIVITY_STATE, false);
edgeState.put(DefaultDeviceStateService.LAST_DISCONNECT_TIME, ts); edgeState.put(DefaultDeviceStateService.LAST_DISCONNECT_TIME, ts);
} }
ctx.getNotificationRuleProcessor().process(EdgeConnectionTrigger.builder()
.tenantId(tenantId)
.customerId(edge.getCustomerId())
.edgeId(edgeId)
.edgeName(edge.getName())
.connected(isConnected).build());
String data = JacksonUtil.toString(edgeState); String data = JacksonUtil.toString(edgeState);
TbMsgMetaData md = new TbMsgMetaData(); TbMsgMetaData md = new TbMsgMetaData();
if (!persistToTelemetry) { if (!persistToTelemetry) {
@ -471,7 +482,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
TbMsg tbMsg = TbMsg.newMsg(msgType, edgeId, md, TbMsgDataType.JSON, data); TbMsg tbMsg = TbMsg.newMsg(msgType, edgeId, md, TbMsgDataType.JSON, data);
clusterService.pushMsgToRuleEngine(tenantId, edgeId, tbMsg, null); clusterService.pushMsgToRuleEngine(tenantId, edgeId, tbMsg, null);
} catch (Exception e) { } catch (Exception e) {
log.warn("[{}][{}] Failed to push {}", tenantId, edgeId, msgType, e); log.warn("[{}][{}] Failed to push {}", tenantId, edge.getId(), msgType, e);
} }
} }
} }

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

@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.notification.rule.trigger.EdgeCommunicationFailureTrigger;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.SortOrder;
@ -112,7 +113,7 @@ public final class EdgeGrpcSession implements Closeable {
private final UUID sessionId; private final UUID sessionId;
private final BiConsumer<EdgeId, EdgeGrpcSession> sessionOpenListener; private final BiConsumer<EdgeId, EdgeGrpcSession> sessionOpenListener;
private final BiConsumer<EdgeId, UUID> sessionCloseListener; private final BiConsumer<Edge, UUID> sessionCloseListener;
private final EdgeSessionState sessionState = new EdgeSessionState(); private final EdgeSessionState sessionState = new EdgeSessionState();
@ -138,7 +139,7 @@ public final class EdgeGrpcSession implements Closeable {
private ScheduledExecutorService sendDownlinkExecutorService; private ScheduledExecutorService sendDownlinkExecutorService;
EdgeGrpcSession(EdgeContextComponent ctx, StreamObserver<ResponseMsg> outputStream, BiConsumer<EdgeId, EdgeGrpcSession> sessionOpenListener, EdgeGrpcSession(EdgeContextComponent ctx, StreamObserver<ResponseMsg> outputStream, BiConsumer<EdgeId, EdgeGrpcSession> sessionOpenListener,
BiConsumer<EdgeId, UUID> sessionCloseListener, ScheduledExecutorService sendDownlinkExecutorService, int maxInboundMessageSize) { BiConsumer<Edge, UUID> sessionCloseListener, ScheduledExecutorService sendDownlinkExecutorService, int maxInboundMessageSize) {
this.sessionId = UUID.randomUUID(); this.sessionId = UUID.randomUUID();
this.ctx = ctx; this.ctx = ctx;
this.outputStream = outputStream; this.outputStream = outputStream;
@ -207,7 +208,7 @@ public final class EdgeGrpcSession implements Closeable {
connected = false; connected = false;
if (edge != null) { if (edge != null) {
try { try {
sessionCloseListener.accept(edge.getId(), sessionId); sessionCloseListener.accept(edge, sessionId);
} catch (Exception ignored) { } catch (Exception ignored) {
} }
} }
@ -315,7 +316,7 @@ public final class EdgeGrpcSession implements Closeable {
} catch (Exception e) { } catch (Exception e) {
log.error("[{}][{}] Failed to send downlink message [{}]", this.tenantId, this.sessionId, downlinkMsg, e); log.error("[{}][{}] Failed to send downlink message [{}]", this.tenantId, this.sessionId, downlinkMsg, e);
connected = false; connected = false;
sessionCloseListener.accept(edge.getId(), sessionId); sessionCloseListener.accept(edge, sessionId);
} finally { } finally {
downlinkMsgLock.unlock(); downlinkMsgLock.unlock();
} }
@ -467,15 +468,26 @@ public final class EdgeGrpcSession implements Closeable {
if (isConnected() && sessionState.getPendingMsgsMap().values().size() > 0) { if (isConnected() && sessionState.getPendingMsgsMap().values().size() > 0) {
List<DownlinkMsg> copy = new ArrayList<>(sessionState.getPendingMsgsMap().values()); List<DownlinkMsg> copy = new ArrayList<>(sessionState.getPendingMsgsMap().values());
if (attempt > 1) { if (attempt > 1) {
log.warn("[{}][{}] Failed to deliver the batch: {}, attempt: {}", this.tenantId, this.sessionId, copy, attempt); String error = "Failed to deliver the batch";
String failureMsg = String.format("{%s}: {%s}", error, copy);
if (attempt == 2) {
// Send a failure notification only on the second attempt.
// This ensures that failure alerts are sent just once to avoid redundant notifications.
ctx.getNotificationRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId)
.edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build());
}
log.warn("[{}][{}] {}, attempt: {}", this.tenantId, this.sessionId, failureMsg, attempt);
} }
log.trace("[{}][{}][{}] downlink msg(s) are going to be send.", this.tenantId, this.sessionId, copy.size()); log.trace("[{}][{}][{}] downlink msg(s) are going to be send.", this.tenantId, this.sessionId, copy.size());
for (DownlinkMsg downlinkMsg : copy) { for (DownlinkMsg downlinkMsg : copy) {
if (this.clientMaxInboundMessageSize != 0 && downlinkMsg.getSerializedSize() > this.clientMaxInboundMessageSize) { if (this.clientMaxInboundMessageSize != 0 && downlinkMsg.getSerializedSize() > this.clientMaxInboundMessageSize) {
log.error("[{}][{}][{}] Downlink msg size [{}] exceeds client max inbound message size [{}]. Skipping this message. " + String error = String.format("Client max inbound message size [{%s}] is exceeded. Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE " +
"Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE env variable on the edge and restart it." + "env variable on the edge and restart it.", this.clientMaxInboundMessageSize);
"Message {}", this.tenantId, edge.getId(), this.sessionId, downlinkMsg.getSerializedSize(), String message = String.format("Downlink msg size [{%s}] exceeds client max inbound message size [{%s}]. " +
this.clientMaxInboundMessageSize, downlinkMsg); "Please increase value of CLOUD_RPC_MAX_INBOUND_MESSAGE_SIZE env variable on the edge and restart it.", downlinkMsg.getSerializedSize(), this.clientMaxInboundMessageSize);
log.error("[{}][{}][{}] {} Message {}", this.tenantId, edge.getId(), this.sessionId, message, downlinkMsg);
ctx.getNotificationRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId)
.edgeId(edge.getId()).customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(message).error(error).build());
sessionState.getPendingMsgsMap().remove(downlinkMsg.getDownlinkMsgId()); sessionState.getPendingMsgsMap().remove(downlinkMsg.getDownlinkMsgId());
} else { } else {
sendDownlinkMsg(ResponseMsg.newBuilder() sendDownlinkMsg(ResponseMsg.newBuilder()
@ -486,8 +498,12 @@ public final class EdgeGrpcSession implements Closeable {
if (attempt < MAX_DOWNLINK_ATTEMPTS) { if (attempt < MAX_DOWNLINK_ATTEMPTS) {
scheduleDownlinkMsgsPackSend(attempt + 1); scheduleDownlinkMsgsPackSend(attempt + 1);
} else { } else {
String failureMsg = String.format("Failed to deliver messages: %s", copy);
log.warn("[{}][{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}", log.warn("[{}][{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}",
this.tenantId, this.sessionId, MAX_DOWNLINK_ATTEMPTS, copy); this.tenantId, this.sessionId, MAX_DOWNLINK_ATTEMPTS, copy);
ctx.getNotificationRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId())
.customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg)
.error("Failed to deliver messages after " + MAX_DOWNLINK_ATTEMPTS + " attempts").build());
stopCurrentSendDownlinkMsgsTask(false); stopCurrentSendDownlinkMsgsTask(false);
} }
} else { } else {
@ -792,7 +808,10 @@ public final class EdgeGrpcSession implements Closeable {
} }
} }
} catch (Exception e) { } catch (Exception e) {
String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg);
log.error("[{}][{}] Can't process uplink msg [{}]", this.tenantId, this.sessionId, uplinkMsg, e); log.error("[{}][{}] Can't process uplink msg [{}]", this.tenantId, this.sessionId, uplinkMsg, e);
ctx.getNotificationRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId())
.customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build());
return Futures.immediateFailedFuture(e); return Futures.immediateFailedFuture(e);
} }
return Futures.allAsList(result); return Futures.allAsList(result);
@ -816,15 +835,22 @@ public final class EdgeGrpcSession implements Closeable {
.setMaxInboundMessageSize(maxInboundMessageSize) .setMaxInboundMessageSize(maxInboundMessageSize)
.build(); .build();
} }
String error = "Failed to validate the edge!";
String failureMsg = String.format("{%s} Provided request secret: %s", error, request.getEdgeSecret());
ctx.getNotificationRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId())
.customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(error).build());
return ConnectResponseMsg.newBuilder() return ConnectResponseMsg.newBuilder()
.setResponseCode(ConnectResponseCode.BAD_CREDENTIALS) .setResponseCode(ConnectResponseCode.BAD_CREDENTIALS)
.setErrorMsg("Failed to validate the edge!") .setErrorMsg(failureMsg)
.setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); .setConfiguration(EdgeConfiguration.getDefaultInstance()).build();
} catch (Exception e) { } catch (Exception e) {
log.error("[{}] Failed to process edge connection!", request.getEdgeRoutingKey(), e); String failureMsg = "Failed to process edge connection!";
ctx.getNotificationRuleProcessor().process(EdgeCommunicationFailureTrigger.builder().tenantId(tenantId).edgeId(edge.getId())
.customerId(edge.getCustomerId()).edgeName(edge.getName()).failureMsg(failureMsg).error(e.getMessage()).build());
log.error(failureMsg, e);
return ConnectResponseMsg.newBuilder() return ConnectResponseMsg.newBuilder()
.setResponseCode(ConnectResponseCode.SERVER_UNAVAILABLE) .setResponseCode(ConnectResponseCode.SERVER_UNAVAILABLE)
.setErrorMsg("Failed to process edge connection!") .setErrorMsg(failureMsg)
.setConfiguration(EdgeConfiguration.getDefaultInstance()).build(); .setConfiguration(EdgeConfiguration.getDefaultInstance()).build();
} }
} }

16
application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java

@ -692,7 +692,23 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
} }
@Override @Override
@SneakyThrows
public void updateDefaultNotificationConfigs() { public void updateDefaultNotificationConfigs() {
PageDataIterable<TenantId> tenants = new PageDataIterable<>(tenantService::findTenantsIds, 500);
ExecutorService executor = Executors.newFixedThreadPool(Math.max(Runtime.getRuntime().availableProcessors(), 4));
log.info("Updating default edge failure notification configs for all tenants");
AtomicInteger count = new AtomicInteger();
for (TenantId tenantId : tenants) {
executor.submit(() -> {
notificationSettingsService.updateDefaultNotificationConfigs(tenantId);
int n = count.incrementAndGet();
if (n % 500 == 0) {
log.info("{} tenants processed", n);
}
});
}
executor.shutdown();
executor.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
notificationSettingsService.updateDefaultNotificationConfigs(TenantId.SYS_TENANT_ID); notificationSettingsService.updateDefaultNotificationConfigs(TenantId.SYS_TENANT_ID);
} }

62
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EdgeCommunicationFailureTriggerProcessor.java

@ -0,0 +1,62 @@
/**
* 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.service.notification.rule.trigger;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.notification.info.EdgeCommunicationFailureNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.EdgeCommunicationFailureTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeCommunicationFailureNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
@Service
@RequiredArgsConstructor
public class EdgeCommunicationFailureTriggerProcessor implements NotificationRuleTriggerProcessor<EdgeCommunicationFailureTrigger, EdgeCommunicationFailureNotificationRuleTriggerConfig> {
@Override
public boolean matchesFilter(EdgeCommunicationFailureTrigger trigger, EdgeCommunicationFailureNotificationRuleTriggerConfig triggerConfig) {
if (CollectionUtils.isNotEmpty(triggerConfig.getEdges())) {
return !triggerConfig.getEdges().contains(trigger.getEdgeId().getId());
}
return true;
}
@Override
public RuleOriginatedNotificationInfo constructNotificationInfo(EdgeCommunicationFailureTrigger trigger) {
return EdgeCommunicationFailureNotificationInfo.builder()
.tenantId(trigger.getTenantId())
.edgeId(trigger.getEdgeId())
.customerId(trigger.getCustomerId())
.edgeName(trigger.getEdgeName())
.failureMsg(truncateFailureMsg(trigger.getFailureMsg()))
.build();
}
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.EDGE_COMMUNICATION_FAILURE;
}
private String truncateFailureMsg(String input) {
int maxLength = 500;
if (input != null && input.length() > maxLength) {
return input.substring(0, maxLength);
}
return input;
}
}

60
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EdgeConnectionTriggerProcessor.java

@ -0,0 +1,60 @@
/**
* 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.service.notification.rule.trigger;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.notification.info.EdgeConnectionNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.EdgeConnectionTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeConnectionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeConnectionNotificationRuleTriggerConfig.EdgeConnectivityEvent;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
@Service
@RequiredArgsConstructor
public class EdgeConnectionTriggerProcessor implements NotificationRuleTriggerProcessor<EdgeConnectionTrigger, EdgeConnectionNotificationRuleTriggerConfig> {
@Override
public boolean matchesFilter(EdgeConnectionTrigger trigger, EdgeConnectionNotificationRuleTriggerConfig triggerConfig) {
EdgeConnectivityEvent event = trigger.isConnected() ? EdgeConnectivityEvent.CONNECTED : EdgeConnectivityEvent.DISCONNECTED;
if (CollectionUtils.isEmpty(triggerConfig.getNotifyOn()) || !triggerConfig.getNotifyOn().contains(event)) {
return false;
}
if (CollectionUtils.isNotEmpty(triggerConfig.getEdges())) {
return triggerConfig.getEdges().contains(trigger.getEdgeId().getId());
}
return true;
}
@Override
public RuleOriginatedNotificationInfo constructNotificationInfo(EdgeConnectionTrigger trigger) {
return EdgeConnectionNotificationInfo.builder()
.eventType(trigger.isConnected() ? "connected" : "disconnected")
.tenantId(trigger.getTenantId())
.customerId(trigger.getCustomerId())
.edgeId(trigger.getEdgeId())
.edgeName(trigger.getEdgeName())
.build();
}
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.EDGE_CONNECTION;
}
}

15
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/NotificationRuleExportService.java

@ -29,6 +29,8 @@ import org.thingsboard.server.common.data.notification.rule.EscalatedNotificatio
import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleRecipientsConfig; import org.thingsboard.server.common.data.notification.rule.NotificationRuleRecipientsConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeCommunicationFailureNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeConnectionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportData;
@ -65,13 +67,24 @@ public class NotificationRuleExportService<I extends EntityId, E extends Exporta
} }
break; break;
} }
case RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT: case RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT: {
RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig triggerConfig = (RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig) ruleTriggerConfig; RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig triggerConfig = (RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig) ruleTriggerConfig;
Set<UUID> ruleChains = triggerConfig.getRuleChains(); Set<UUID> ruleChains = triggerConfig.getRuleChains();
if (ruleChains != null) { if (ruleChains != null) {
triggerConfig.setRuleChains(toExternalIds(ruleChains, RuleChainId::new, ctx).collect(Collectors.toSet())); triggerConfig.setRuleChains(toExternalIds(ruleChains, RuleChainId::new, ctx).collect(Collectors.toSet()));
} }
break; break;
}
case EDGE_CONNECTION: {
EdgeConnectionNotificationRuleTriggerConfig triggerConfig = (EdgeConnectionNotificationRuleTriggerConfig) ruleTriggerConfig;
triggerConfig.setEdges(null);
break;
}
case EDGE_COMMUNICATION_FAILURE: {
EdgeCommunicationFailureNotificationRuleTriggerConfig triggerConfig = (EdgeCommunicationFailureNotificationRuleTriggerConfig) ruleTriggerConfig;
triggerConfig.setEdges(null);
break;
}
} }
NotificationRuleRecipientsConfig ruleRecipientsConfig = notificationRule.getRecipientsConfig(); NotificationRuleRecipientsConfig ruleRecipientsConfig = notificationRule.getRecipientsConfig();

15
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/NotificationRuleImportService.java

@ -32,6 +32,8 @@ import org.thingsboard.server.common.data.notification.rule.EscalatedNotificatio
import org.thingsboard.server.common.data.notification.rule.NotificationRule; import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleRecipientsConfig; import org.thingsboard.server.common.data.notification.rule.NotificationRuleRecipientsConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeConnectionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeCommunicationFailureNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.rule.trigger.config.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig;
@ -84,7 +86,7 @@ public class NotificationRuleImportService extends BaseEntityImportService<Notif
} }
break; break;
} }
case RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT: case RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT: {
RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig triggerConfig = (RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig) ruleTriggerConfig; RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig triggerConfig = (RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig) ruleTriggerConfig;
Set<UUID> ruleChains = triggerConfig.getRuleChains(); Set<UUID> ruleChains = triggerConfig.getRuleChains();
if (ruleChains != null) { if (ruleChains != null) {
@ -93,6 +95,17 @@ public class NotificationRuleImportService extends BaseEntityImportService<Notif
.collect(Collectors.toSet())); .collect(Collectors.toSet()));
} }
break; break;
}
case EDGE_CONNECTION: {
EdgeConnectionNotificationRuleTriggerConfig triggerConfig = (EdgeConnectionNotificationRuleTriggerConfig) ruleTriggerConfig;
triggerConfig.setEdges(null);
break;
}
case EDGE_COMMUNICATION_FAILURE: {
EdgeCommunicationFailureNotificationRuleTriggerConfig triggerConfig = (EdgeCommunicationFailureNotificationRuleTriggerConfig) ruleTriggerConfig;
triggerConfig.setEdges(null);
break;
}
} }
if (!triggerType.isTenantLevel()) { if (!triggerType.isTenantLevel()) {
throw new IllegalArgumentException("Trigger type " + triggerType + " is not available for tenants"); throw new IllegalArgumentException("Trigger type " + triggerType + " is not available for tenants");

8
application/src/main/resources/thingsboard.yml

@ -1185,14 +1185,18 @@ transport:
bind_port: "${SNMP_BIND_PORT:1620}" bind_port: "${SNMP_BIND_PORT:1620}"
response_processing: response_processing:
# parallelism level for executor (workStealingPool) that is responsible for handling responses from SNMP devices # parallelism level for executor (workStealingPool) that is responsible for handling responses from SNMP devices
parallelism_level: "${SNMP_RESPONSE_PROCESSING_PARALLELISM_LEVEL:20}" parallelism_level: "${SNMP_RESPONSE_PROCESSING_PARALLELISM_LEVEL:4}"
# to configure SNMP to work over UDP or TCP # to configure SNMP to work over UDP or TCP
underlying_protocol: "${SNMP_UNDERLYING_PROTOCOL:udp}" underlying_protocol: "${SNMP_UNDERLYING_PROTOCOL:udp}"
# Batch size to request OID mappings from the device (useful when the device profile has multiple hundreds of OID mappings) # Maximum size of a PDU (amount of OID mappings in a single SNMP request). The request will be split into multiple PDUs if mappings amount exceeds this number
max_request_oids: "${SNMP_MAX_REQUEST_OIDS:100}" max_request_oids: "${SNMP_MAX_REQUEST_OIDS:100}"
# Delay after sending each request chunk (in case the request was split into multiple PDUs due to max_request_oids)
request_chunk_delay_ms: "${SNMP_REQUEST_CHUNK_DELAY_MS:100}"
response: response:
# To ignore SNMP response values that do not match the data type of the configured OID mapping (by default false - will throw an error if any value of the response not match configured data types) # To ignore SNMP response values that do not match the data type of the configured OID mapping (by default false - will throw an error if any value of the response not match configured data types)
ignore_type_cast_errors: "${SNMP_RESPONSE_IGNORE_TYPE_CAST_ERRORS:false}" ignore_type_cast_errors: "${SNMP_RESPONSE_IGNORE_TYPE_CAST_ERRORS:false}"
# Thread pool size for scheduler that executes device querying tasks
scheduler_thread_pool_size: "${SNMP_SCHEDULER_THREAD_POOL_SIZE:4}"
stats: stats:
# Enable/Disable the collection of transport statistics # Enable/Disable the collection of transport statistics
enabled: "${TB_TRANSPORT_STATS_ENABLED:true}" enabled: "${TB_TRANSPORT_STATS_ENABLED:true}"

2
application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java

@ -606,7 +606,7 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest {
private String entityClassToString(HasName entity) { private String entityClassToString(HasName entity) {
String className = entity.getClass().toString() String className = entity.getClass().toString()
.substring(entity.getClass().toString().lastIndexOf(".") + 1); .substring(entity.getClass().toString().lastIndexOf(".") + 1);
List str = className.chars() List<String> str = className.chars()
.mapToObj(x -> (Character.isUpperCase(x)) ? "_" + Character.toString(x) : Character.toString(x)) .mapToObj(x -> (Character.isUpperCase(x)) ? "_" + Character.toString(x) : Character.toString(x))
.collect(Collectors.toList()); .collect(Collectors.toList());
return String.join("", str).toUpperCase(Locale.ENGLISH).substring(1); return String.join("", str).toUpperCase(Locale.ENGLISH).substring(1);

6
application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java

@ -69,6 +69,8 @@ public class RateLimitServiceTest {
profileConfiguration.setCustomerServerRestLimitsConfiguration(rateLimit); profileConfiguration.setCustomerServerRestLimitsConfiguration(rateLimit);
profileConfiguration.setWsUpdatesPerSessionRateLimit(rateLimit); profileConfiguration.setWsUpdatesPerSessionRateLimit(rateLimit);
profileConfiguration.setCassandraQueryTenantRateLimitsConfiguration(rateLimit); profileConfiguration.setCassandraQueryTenantRateLimitsConfiguration(rateLimit);
profileConfiguration.setEdgeEventRateLimits(rateLimit);
profileConfiguration.setEdgeEventRateLimitsPerEdge(rateLimit);
updateTenantProfileConfiguration(profileConfiguration); updateTenantProfileConfiguration(profileConfiguration);
for (LimitedApi limitedApi : List.of( for (LimitedApi limitedApi : List.of(
@ -76,7 +78,9 @@ public class RateLimitServiceTest {
LimitedApi.ENTITY_IMPORT, LimitedApi.ENTITY_IMPORT,
LimitedApi.NOTIFICATION_REQUESTS, LimitedApi.NOTIFICATION_REQUESTS,
LimitedApi.REST_REQUESTS_PER_CUSTOMER, LimitedApi.REST_REQUESTS_PER_CUSTOMER,
LimitedApi.CASSANDRA_QUERIES LimitedApi.CASSANDRA_QUERIES,
LimitedApi.EDGE_EVENTS,
LimitedApi.EDGE_EVENTS_PER_EDGE
)) { )) {
testRateLimits(limitedApi, max, tenantId); testRateLimits(limitedApi, max, tenantId);
} }

2
common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java

@ -31,6 +31,8 @@ public enum LimitedApi {
REST_REQUESTS_PER_CUSTOMER(DefaultTenantProfileConfiguration::getCustomerServerRestLimitsConfiguration, "REST API requests per customer", false), REST_REQUESTS_PER_CUSTOMER(DefaultTenantProfileConfiguration::getCustomerServerRestLimitsConfiguration, "REST API requests per customer", false),
WS_UPDATES_PER_SESSION(DefaultTenantProfileConfiguration::getWsUpdatesPerSessionRateLimit, "WS updates per session", true), WS_UPDATES_PER_SESSION(DefaultTenantProfileConfiguration::getWsUpdatesPerSessionRateLimit, "WS updates per session", true),
CASSANDRA_QUERIES(DefaultTenantProfileConfiguration::getCassandraQueryTenantRateLimitsConfiguration, "Cassandra queries", true), CASSANDRA_QUERIES(DefaultTenantProfileConfiguration::getCassandraQueryTenantRateLimitsConfiguration, "Cassandra queries", true),
EDGE_EVENTS(DefaultTenantProfileConfiguration::getEdgeEventRateLimits, "Edge events", true),
EDGE_EVENTS_PER_EDGE(DefaultTenantProfileConfiguration::getEdgeEventRateLimitsPerEdge, "Edge events per edge", false),
PASSWORD_RESET(false, true), PASSWORD_RESET(false, true),
TWO_FA_VERIFICATION_CODE_SEND(false, true), TWO_FA_VERIFICATION_CODE_SEND(false, true),
TWO_FA_VERIFICATION_CODE_CHECK(false, true), TWO_FA_VERIFICATION_CODE_CHECK(false, true),

5
common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java

@ -28,6 +28,7 @@ public enum NotificationType {
ENTITIES_LIMIT, ENTITIES_LIMIT,
API_USAGE_LIMIT, API_USAGE_LIMIT,
RULE_NODE, RULE_NODE,
RATE_LIMITS RATE_LIMITS,
EDGE_CONNECTION,
EDGE_COMMUNICATION_FAILURE
} }

66
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EdgeCommunicationFailureNotificationInfo.java

@ -0,0 +1,66 @@
/**
* 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.common.data.notification.info;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.Map;
import static org.thingsboard.server.common.data.util.CollectionsUtil.mapOf;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class EdgeCommunicationFailureNotificationInfo implements RuleOriginatedNotificationInfo {
private TenantId tenantId;
private CustomerId customerId;
private EdgeId edgeId;
private String edgeName;
private String failureMsg;
@Override
public Map<String, String> getTemplateData() {
return mapOf(
"edgeId", edgeId.toString(),
"edgeName", edgeName,
"failureMsg", failureMsg
);
}
@Override
public TenantId getAffectedTenantId() {
return tenantId;
}
@Override
public CustomerId getAffectedCustomerId() {
return customerId;
}
@Override
public EntityId getStateEntityId() {
return edgeId;
}
}

66
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EdgeConnectionNotificationInfo.java

@ -0,0 +1,66 @@
/**
* 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.common.data.notification.info;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import java.util.Map;
import static org.thingsboard.server.common.data.util.CollectionsUtil.mapOf;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class EdgeConnectionNotificationInfo implements RuleOriginatedNotificationInfo {
private String eventType;
private TenantId tenantId;
private CustomerId customerId;
private EdgeId edgeId;
private String edgeName;
@Override
public Map<String, String> getTemplateData() {
return mapOf(
"eventType", eventType,
"edgeId", edgeId.toString(),
"edgeName", edgeName
);
}
@Override
public TenantId getAffectedTenantId() {
return tenantId;
}
@Override
public CustomerId getAffectedCustomerId() {
return customerId;
}
@Override
public EntityId getStateEntityId() {
return edgeId;
}
}

63
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeCommunicationFailureTrigger.java

@ -0,0 +1,63 @@
/**
* 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.common.data.notification.rule.trigger;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.util.concurrent.TimeUnit;
@Data
@Builder
public class EdgeCommunicationFailureTrigger implements NotificationRuleTrigger {
private final TenantId tenantId;
private final CustomerId customerId;
private final EdgeId edgeId;
private final String edgeName;
private final String failureMsg;
private final String error;
@Override
public boolean deduplicate() {
return true;
}
@Override
public String getDeduplicationKey() {
return String.join(":", NotificationRuleTrigger.super.getDeduplicationKey(), error);
}
@Override
public long getDefaultDeduplicationDuration() {
return TimeUnit.MINUTES.toMillis(30);
}
@Override
public NotificationRuleTriggerType getType() {
return NotificationRuleTriggerType.EDGE_COMMUNICATION_FAILURE;
}
@Override
public EntityId getOriginatorEntityId() {
return edgeId;
}
}

62
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeConnectionTrigger.java

@ -0,0 +1,62 @@
/**
* 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.common.data.notification.rule.trigger;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.util.concurrent.TimeUnit;
@Data
@Builder
public class EdgeConnectionTrigger implements NotificationRuleTrigger {
private final TenantId tenantId;
private final CustomerId customerId;
private final EdgeId edgeId;
private final boolean connected;
private final String edgeName;
@Override
public boolean deduplicate() {
return true;
}
@Override
public String getDeduplicationKey() {
return String.join(":", NotificationRuleTrigger.super.getDeduplicationKey(), String.valueOf(connected));
}
@Override
public long getDefaultDeduplicationDuration() {
return TimeUnit.MINUTES.toMillis(1);
}
@Override
public NotificationRuleTriggerType getType() {
return NotificationRuleTriggerType.EDGE_CONNECTION;
}
@Override
public EntityId getOriginatorEntityId() {
return edgeId;
}
}

39
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeCommunicationFailureNotificationRuleTriggerConfig.java

@ -0,0 +1,39 @@
/**
* 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.common.data.notification.rule.trigger.config;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Set;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class EdgeCommunicationFailureNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
private Set<UUID> edges; // if empty - all edges
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.EDGE_COMMUNICATION_FAILURE;
}
}

44
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/EdgeConnectionNotificationRuleTriggerConfig.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.server.common.data.notification.rule.trigger.config;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Set;
import java.util.UUID;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class EdgeConnectionNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
private Set<UUID> edges; // if empty - all edges
private Set<EdgeConnectivityEvent> notifyOn;
@Override
public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.EDGE_CONNECTION;
}
public enum EdgeConnectivityEvent {
CONNECTED, DISCONNECTED
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerConfig.java

@ -36,6 +36,8 @@ import java.io.Serializable;
@Type(value = EntitiesLimitNotificationRuleTriggerConfig.class, name = "ENTITIES_LIMIT"), @Type(value = EntitiesLimitNotificationRuleTriggerConfig.class, name = "ENTITIES_LIMIT"),
@Type(value = ApiUsageLimitNotificationRuleTriggerConfig.class, name = "API_USAGE_LIMIT"), @Type(value = ApiUsageLimitNotificationRuleTriggerConfig.class, name = "API_USAGE_LIMIT"),
@Type(value = RateLimitsNotificationRuleTriggerConfig.class, name = "RATE_LIMITS"), @Type(value = RateLimitsNotificationRuleTriggerConfig.class, name = "RATE_LIMITS"),
@Type(value = EdgeConnectionNotificationRuleTriggerConfig.class, name = "EDGE_CONNECTION"),
@Type(value = EdgeCommunicationFailureNotificationRuleTriggerConfig.class, name = "EDGE_COMMUNICATION_FAILURE"),
}) })
public interface NotificationRuleTriggerConfig extends Serializable { public interface NotificationRuleTriggerConfig extends Serializable {

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/config/NotificationRuleTriggerType.java

@ -26,6 +26,8 @@ public enum NotificationRuleTriggerType {
ALARM_ASSIGNMENT, ALARM_ASSIGNMENT,
DEVICE_ACTIVITY, DEVICE_ACTIVITY,
RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT, RULE_ENGINE_COMPONENT_LIFECYCLE_EVENT,
EDGE_CONNECTION,
EDGE_COMMUNICATION_FAILURE,
NEW_PLATFORM_VERSION(false), NEW_PLATFORM_VERSION(false),
ENTITIES_LIMIT(false), ENTITIES_LIMIT(false),
API_USAGE_LIMIT(false), API_USAGE_LIMIT(false),

3
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java

@ -81,6 +81,9 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private String cassandraQueryTenantRateLimitsConfiguration; private String cassandraQueryTenantRateLimitsConfiguration;
private String edgeEventRateLimits;
private String edgeEventRateLimitsPerEdge;
private int defaultStorageTtlDays; private int defaultStorageTtlDays;
private int alarmsTtlDays; private int alarmsTtlDays;
private int rpcTtlDays; private int rpcTtlDays;

3
common/data/src/main/java/org/thingsboard/server/common/data/util/TemplateUtils.java

@ -19,6 +19,7 @@ import org.apache.commons.lang3.StringUtils;
import java.util.Map; import java.util.Map;
import java.util.function.UnaryOperator; import java.util.function.UnaryOperator;
import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import static com.google.common.base.Strings.nullToEmpty; import static com.google.common.base.Strings.nullToEmpty;
@ -49,7 +50,7 @@ public class TemplateUtils {
value = FUNCTIONS.get(function).apply(value); value = FUNCTIONS.get(function).apply(value);
} }
} }
return value; return Matcher.quoteReplacement(value);
}); });
} }

2
common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java

@ -152,12 +152,14 @@ public class SnmpTransportContext extends TransportContext {
try { try {
if (!newProfileTransportConfiguration.equals(sessionContext.getProfileTransportConfiguration())) { if (!newProfileTransportConfiguration.equals(sessionContext.getProfileTransportConfiguration())) {
sessionContext.setProfileTransportConfiguration(newProfileTransportConfiguration); sessionContext.setProfileTransportConfiguration(newProfileTransportConfiguration);
sessionContext.setDevice(device);
sessionContext.initializeTarget(newProfileTransportConfiguration, newDeviceTransportConfiguration); sessionContext.initializeTarget(newProfileTransportConfiguration, newDeviceTransportConfiguration);
snmpTransportService.cancelQueryingTasks(sessionContext); snmpTransportService.cancelQueryingTasks(sessionContext);
snmpTransportService.createQueryingTasks(sessionContext); snmpTransportService.createQueryingTasks(sessionContext);
transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.UPDATED, true, null); transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.UPDATED, true, null);
} else if (!newDeviceTransportConfiguration.equals(sessionContext.getDeviceTransportConfiguration())) { } else if (!newDeviceTransportConfiguration.equals(sessionContext.getDeviceTransportConfiguration())) {
sessionContext.setDeviceTransportConfiguration(newDeviceTransportConfiguration); sessionContext.setDeviceTransportConfiguration(newDeviceTransportConfiguration);
sessionContext.setDevice(device);
sessionContext.initializeTarget(newProfileTransportConfiguration, newDeviceTransportConfiguration); sessionContext.initializeTarget(newProfileTransportConfiguration, newDeviceTransportConfiguration);
transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.UPDATED, true, null); transportService.lifecycleEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), ComponentLifecycleEvent.UPDATED, true, null);
} else { } else {

164
common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/service/SnmpTransportService.java

@ -15,6 +15,11 @@
*/ */
package org.thingsboard.server.transport.snmp.service; package org.thingsboard.server.transport.snmp.service;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListenableScheduledFuture;
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import lombok.Builder; import lombok.Builder;
@ -32,7 +37,7 @@ import org.snmp4j.mp.MPv3;
import org.snmp4j.security.SecurityModels; import org.snmp4j.security.SecurityModels;
import org.snmp4j.security.SecurityProtocols; import org.snmp4j.security.SecurityProtocols;
import org.snmp4j.security.USM; import org.snmp4j.security.USM;
import org.snmp4j.smi.Address; import org.snmp4j.smi.IpAddress;
import org.snmp4j.smi.OctetString; import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.TcpAddress; import org.snmp4j.smi.TcpAddress;
import org.snmp4j.smi.UdpAddress; import org.snmp4j.smi.UdpAddress;
@ -44,6 +49,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.adaptor.JsonConverter;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.TbTransportService; import org.thingsboard.server.common.data.TbTransportService;
import org.thingsboard.server.common.data.kv.DataType; import org.thingsboard.server.common.data.kv.DataType;
@ -53,11 +59,11 @@ import org.thingsboard.server.common.data.transport.snmp.SnmpMethod;
import org.thingsboard.server.common.data.transport.snmp.config.RepeatingQueryingSnmpCommunicationConfig; import org.thingsboard.server.common.data.transport.snmp.config.RepeatingQueryingSnmpCommunicationConfig;
import org.thingsboard.server.common.data.transport.snmp.config.SnmpCommunicationConfig; import org.thingsboard.server.common.data.transport.snmp.config.SnmpCommunicationConfig;
import org.thingsboard.server.common.transport.TransportService; import org.thingsboard.server.common.transport.TransportService;
import org.thingsboard.server.common.adaptor.JsonConverter;
import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.util.TbSnmpTransportComponent; import org.thingsboard.server.queue.util.TbSnmpTransportComponent;
import org.thingsboard.server.transport.snmp.SnmpTransportContext; import org.thingsboard.server.transport.snmp.SnmpTransportContext;
import org.thingsboard.server.transport.snmp.session.DeviceSessionContext; import org.thingsboard.server.transport.snmp.session.DeviceSessionContext;
import org.thingsboard.server.transport.snmp.session.ScheduledTask;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
@ -71,8 +77,6 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -80,6 +84,7 @@ import java.util.stream.Collectors;
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
@SuppressWarnings("UnstableApiUsage")
public class SnmpTransportService implements TbTransportService, CommandResponder { public class SnmpTransportService implements TbTransportService, CommandResponder {
private final TransportService transportService; private final TransportService transportService;
private final PduService pduService; private final PduService pduService;
@ -88,23 +93,27 @@ public class SnmpTransportService implements TbTransportService, CommandResponde
@Getter @Getter
private Snmp snmp; private Snmp snmp;
private ScheduledExecutorService queryingExecutor; private ListeningScheduledExecutorService scheduler;
private ExecutorService responseProcessingExecutor; private ExecutorService executor;
private final Map<SnmpCommunicationSpec, ResponseDataMapper> responseDataMappers = new EnumMap<>(SnmpCommunicationSpec.class); private final Map<SnmpCommunicationSpec, ResponseDataMapper> responseDataMappers = new EnumMap<>(SnmpCommunicationSpec.class);
private final Map<SnmpCommunicationSpec, ResponseProcessor> responseProcessors = new EnumMap<>(SnmpCommunicationSpec.class); private final Map<SnmpCommunicationSpec, ResponseProcessor> responseProcessors = new EnumMap<>(SnmpCommunicationSpec.class);
@Value("${transport.snmp.bind_port:1620}") @Value("${transport.snmp.bind_port:1620}")
private Integer snmpBindPort; private Integer snmpBindPort;
@Value("${transport.snmp.response_processing.parallelism_level}") @Value("${transport.snmp.response_processing.parallelism_level:4}")
private Integer responseProcessingParallelismLevel; private int responseProcessingThreadPoolSize;
@Value("${transport.snmp.scheduler_thread_pool_size:4}")
private int schedulerThreadPoolSize;
@Value("${transport.snmp.underlying_protocol}") @Value("${transport.snmp.underlying_protocol}")
private String snmpUnderlyingProtocol; private String snmpUnderlyingProtocol;
@Value("${transport.snmp.request_chunk_delay_ms:100}")
private int requestChunkDelayMs;
@PostConstruct @PostConstruct
private void init() throws IOException { private void init() throws IOException {
queryingExecutor = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), ThingsBoardThreadFactory.forName("snmp-querying")); scheduler = MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(schedulerThreadPoolSize, ThingsBoardThreadFactory.forName("snmp-querying")));
responseProcessingExecutor = ThingsBoardExecutors.newWorkStealingPool(responseProcessingParallelismLevel, "snmp-response-processing"); executor = ThingsBoardExecutors.newWorkStealingPool(responseProcessingThreadPoolSize, "snmp-response-processing");
initializeSnmp(); initializeSnmp();
configureResponseDataMappers(); configureResponseDataMappers();
@ -115,11 +124,11 @@ public class SnmpTransportService implements TbTransportService, CommandResponde
@PreDestroy @PreDestroy
public void stop() { public void stop() {
if (queryingExecutor != null) { if (scheduler != null) {
queryingExecutor.shutdownNow(); scheduler.shutdownNow();
} }
if (responseProcessingExecutor != null) { if (executor != null) {
responseProcessingExecutor.shutdownNow(); executor.shutdownNow();
} }
} }
@ -144,38 +153,39 @@ public class SnmpTransportService implements TbTransportService, CommandResponde
} }
public void createQueryingTasks(DeviceSessionContext sessionContext) { public void createQueryingTasks(DeviceSessionContext sessionContext) {
List<ScheduledFuture<?>> queryingTasks = sessionContext.getProfileTransportConfiguration().getCommunicationConfigs().stream() sessionContext.getProfileTransportConfiguration().getCommunicationConfigs().stream()
.filter(communicationConfig -> communicationConfig instanceof RepeatingQueryingSnmpCommunicationConfig) .filter(communicationConfig -> communicationConfig instanceof RepeatingQueryingSnmpCommunicationConfig)
.map(config -> { .forEach(config -> {
RepeatingQueryingSnmpCommunicationConfig repeatingCommunicationConfig = (RepeatingQueryingSnmpCommunicationConfig) config; RepeatingQueryingSnmpCommunicationConfig repeatingCommunicationConfig = (RepeatingQueryingSnmpCommunicationConfig) config;
Long queryingFrequency = repeatingCommunicationConfig.getQueryingFrequencyMs(); Long queryingFrequency = repeatingCommunicationConfig.getQueryingFrequencyMs();
return queryingExecutor.scheduleWithFixedDelay(() -> { ScheduledTask scheduledTask = new ScheduledTask();
scheduledTask.init(() -> {
try { try {
if (sessionContext.isActive()) { if (sessionContext.isActive()) {
sendRequest(sessionContext, repeatingCommunicationConfig); return sendRequest(sessionContext, repeatingCommunicationConfig);
} }
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to send SNMP request for device {}: {}", sessionContext.getDeviceId(), e.toString()); log.error("Failed to send SNMP request for device {}: {}", sessionContext.getDeviceId(), e.toString());
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), config.getSpec().getLabel(), e); transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), config.getSpec().getLabel(), e);
} }
}, queryingFrequency, queryingFrequency, TimeUnit.MILLISECONDS); return Futures.immediateVoidFuture();
}) }, queryingFrequency, scheduler);
.collect(Collectors.toList()); sessionContext.getQueryingTasks().add(scheduledTask);
sessionContext.getQueryingTasks().addAll(queryingTasks); });
} }
public void cancelQueryingTasks(DeviceSessionContext sessionContext) { public void cancelQueryingTasks(DeviceSessionContext sessionContext) {
sessionContext.getQueryingTasks().forEach(task -> task.cancel(true)); sessionContext.getQueryingTasks().forEach(ScheduledTask::cancel);
sessionContext.getQueryingTasks().clear(); sessionContext.getQueryingTasks().clear();
} }
private void sendRequest(DeviceSessionContext sessionContext, SnmpCommunicationConfig communicationConfig) { private ListenableFuture<Void> sendRequest(DeviceSessionContext sessionContext, SnmpCommunicationConfig communicationConfig) {
sendRequest(sessionContext, communicationConfig, Collections.emptyMap()); return sendRequest(sessionContext, communicationConfig, Collections.emptyMap());
} }
private void sendRequest(DeviceSessionContext sessionContext, SnmpCommunicationConfig communicationConfig, Map<String, String> values) { private ListenableFuture<Void> sendRequest(DeviceSessionContext sessionContext, SnmpCommunicationConfig communicationConfig, Map<String, String> values) {
List<PDU> request = pduService.createPdus(sessionContext, communicationConfig, values); List<PDU> request = pduService.createPdus(sessionContext, communicationConfig, values);
RequestContext requestContext = RequestContext.builder() RequestContext requestContext = RequestContext.builder()
.communicationSpec(communicationConfig.getSpec()) .communicationSpec(communicationConfig.getSpec())
@ -183,19 +193,40 @@ public class SnmpTransportService implements TbTransportService, CommandResponde
.responseMappings(communicationConfig.getAllMappings()) .responseMappings(communicationConfig.getAllMappings())
.requestSize(request.size()) .requestSize(request.size())
.build(); .build();
sendRequest(sessionContext, request, requestContext); return sendRequest(sessionContext, request, requestContext);
} }
private void sendRequest(DeviceSessionContext sessionContext, List<PDU> request, RequestContext requestContext) { private ListenableFuture<Void> sendRequest(DeviceSessionContext sessionContext, List<PDU> request, RequestContext requestContext) {
for (PDU pdu : request) { if (request.size() <= 1 || requestChunkDelayMs == 0) {
log.debug("Executing SNMP request for device {} with {} variable bindings", sessionContext.getDeviceId(), pdu.size()); for (PDU pdu : request) {
try { sendPdu(pdu, requestContext, sessionContext);
snmp.send(pdu, sessionContext.getTarget(), requestContext, sessionContext); }
} catch (IOException e) { return Futures.immediateVoidFuture();
log.error("Failed to send SNMP request to device {}: {}", sessionContext.getDeviceId(), e.toString()); }
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), e);
List<ListenableFuture<?>> futures = new ArrayList<>();
for (int i = 0, delay = 0; i < request.size(); i++, delay += requestChunkDelayMs) {
PDU pdu = request.get(i);
if (delay == 0) {
sendPdu(pdu, requestContext, sessionContext);
} else {
ListenableScheduledFuture<?> future = scheduler.schedule(() -> {
sendPdu(pdu, requestContext, sessionContext);
}, delay, TimeUnit.MILLISECONDS);
futures.add(future);
} }
} }
return Futures.whenAllComplete(futures).call(() -> null, MoreExecutors.directExecutor());
}
private void sendPdu(PDU pdu, RequestContext requestContext, DeviceSessionContext sessionContext) {
log.debug("[{}] Sending SNMP request with {} variable bindings to {}", sessionContext.getDeviceId(), pdu.size(), sessionContext.getTarget().getAddress());
try {
snmp.send(pdu, sessionContext.getTarget(), requestContext, sessionContext);
} catch (Exception e) {
log.error("[{}] Failed to send SNMP request", sessionContext.getDeviceId(), e);
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), e);
}
} }
public void onAttributeUpdate(DeviceSessionContext sessionContext, TransportProtos.AttributeUpdateNotificationMsg attributeUpdateNotification) { public void onAttributeUpdate(DeviceSessionContext sessionContext, TransportProtos.AttributeUpdateNotificationMsg attributeUpdateNotification) {
@ -251,21 +282,19 @@ public class SnmpTransportService implements TbTransportService, CommandResponde
((Snmp) event.getSource()).cancel(event.getRequest(), sessionContext); ((Snmp) event.getSource()).cancel(event.getRequest(), sessionContext);
RequestContext requestContext = (RequestContext) event.getUserObject(); RequestContext requestContext = (RequestContext) event.getUserObject();
if (event.getError() != null) { if (event.getError() != null) {
log.warn("SNMP response error: {}", event.getError().toString()); log.warn("[{}] SNMP response error: {}", sessionContext.getDeviceId(), event.getError().toString());
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), new RuntimeException(event.getError())); transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), new RuntimeException(event.getError()));
return; return;
} }
PDU responsePdu = event.getResponse(); PDU responsePdu = event.getResponse();
if (log.isTraceEnabled()) { log.trace("[{}] Received PDU: {}", sessionContext.getDeviceId(), responsePdu);
log.trace("Received PDU for device {}: {}", sessionContext.getDeviceId(), responsePdu);
}
List<PDU> response; List<PDU> response;
if (requestContext.getRequestSize() == 1) { if (requestContext.getRequestSize() == 1) {
if (responsePdu == null) { if (responsePdu == null) {
log.debug("No response from SNMP device {}, requestId: {}", sessionContext.getDeviceId(), event.getRequest().getRequestID());
if (requestContext.getMethod() == SnmpMethod.GET) { if (requestContext.getMethod() == SnmpMethod.GET) {
log.debug("[{}][{}] Empty response from device", sessionContext.getDeviceId(), event.getRequest().getRequestID());
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), new RuntimeException("No response from device")); transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), requestContext.getCommunicationSpec().getLabel(), new RuntimeException("No response from device"));
} }
return; return;
@ -281,14 +310,14 @@ public class SnmpTransportService implements TbTransportService, CommandResponde
response.add(responsePart); response.add(responsePart);
} }
} }
log.debug("All response parts are collected for request to device {}", sessionContext.getDeviceId()); log.debug("[{}] All {} response parts are collected for request", sessionContext.getDeviceId(), responseParts.size());
} else { } else {
log.trace("Awaiting other response parts for request to device {}", sessionContext.getDeviceId()); log.trace("[{}] Awaiting other response parts for request", sessionContext.getDeviceId());
return; return;
} }
} }
responseProcessingExecutor.execute(() -> { executor.execute(() -> {
try { try {
processResponse(sessionContext, response, requestContext); processResponse(sessionContext, response, requestContext);
} catch (Exception e) { } catch (Exception e) {
@ -298,24 +327,31 @@ public class SnmpTransportService implements TbTransportService, CommandResponde
} }
/* /*
* SNMP notifications handler * SNMP notifications handler
* *
* TODO: add check for host uniqueness when saving device (for backward compatibility - only for the ones using from-device RPC requests) * TODO: add check for host uniqueness when saving device (for backward compatibility - only for the ones using from-device RPC requests)
* *
* NOTE: SNMP TRAPs support won't work properly when there is more than one SNMP transport, * NOTE: SNMP TRAPs support won't work properly when there is more than one SNMP transport,
* due to load-balancing of requests from devices: session might not be on this instance * due to load-balancing of requests from devices: session might not be on this instance
* */ * */
@Override @Override
public void processPdu(CommandResponderEvent event) { public void processPdu(CommandResponderEvent event) {
Address sourceAddress = event.getPeerAddress(); IpAddress sourceAddress = (IpAddress) event.getPeerAddress();
DeviceSessionContext sessionContext = transportContext.getSessions().stream() List<DeviceSessionContext> sessions = transportContext.getSessions().stream()
.filter(session -> session.getTarget().getAddress().equals(sourceAddress)) .filter(session -> ((IpAddress) session.getTarget().getAddress()).getInetAddress().equals(sourceAddress.getInetAddress()))
.findFirst().orElse(null); .collect(Collectors.toList());
if (sessionContext == null) { if (sessions.isEmpty()) {
log.warn("SNMP TRAP processing failed: couldn't find device session for address {}", sourceAddress); log.warn("Couldn't find device session for SNMP TRAP for address {}", sourceAddress);
return;
} else if (sessions.size() > 1) {
for (DeviceSessionContext sessionContext : sessions) {
transportService.errorEvent(sessionContext.getTenantId(), sessionContext.getDeviceId(), SnmpCommunicationSpec.TO_SERVER_RPC_REQUEST.getLabel(),
new IllegalStateException("Found multiple devices for host " + sourceAddress.getInetAddress().getHostAddress()));
}
return; return;
} }
DeviceSessionContext sessionContext = sessions.get(0);
try { try {
processIncomingTrap(sessionContext, event); processIncomingTrap(sessionContext, event);
} catch (Throwable e) { } catch (Throwable e) {
@ -327,11 +363,11 @@ public class SnmpTransportService implements TbTransportService, CommandResponde
private void processIncomingTrap(DeviceSessionContext sessionContext, CommandResponderEvent event) { private void processIncomingTrap(DeviceSessionContext sessionContext, CommandResponderEvent event) {
PDU pdu = event.getPDU(); PDU pdu = event.getPDU();
if (pdu == null) { if (pdu == null) {
log.warn("Got empty trap from device {}", sessionContext.getDeviceId()); log.warn("[{}] Received empty SNMP trap", sessionContext.getDeviceId());
throw new IllegalArgumentException("Received TRAP with no data"); throw new IllegalArgumentException("Received TRAP with no data");
} }
log.debug("Processing SNMP trap from device {} (PDU: {}}", sessionContext.getDeviceId(), pdu); log.debug("[{}] Processing SNMP trap: {}", sessionContext.getDeviceId(), pdu);
SnmpCommunicationConfig communicationConfig = sessionContext.getProfileTransportConfiguration().getCommunicationConfigs().stream() SnmpCommunicationConfig communicationConfig = sessionContext.getProfileTransportConfiguration().getCommunicationConfigs().stream()
.filter(config -> config.getSpec() == SnmpCommunicationSpec.TO_SERVER_RPC_REQUEST).findFirst() .filter(config -> config.getSpec() == SnmpCommunicationSpec.TO_SERVER_RPC_REQUEST).findFirst()
.orElseThrow(() -> new IllegalArgumentException("No config found for to-server RPC requests")); .orElseThrow(() -> new IllegalArgumentException("No config found for to-server RPC requests"));
@ -341,7 +377,7 @@ public class SnmpTransportService implements TbTransportService, CommandResponde
.method(SnmpMethod.TRAP) .method(SnmpMethod.TRAP)
.build(); .build();
responseProcessingExecutor.execute(() -> { executor.execute(() -> {
processResponse(sessionContext, List.of(pdu), requestContext); processResponse(sessionContext, List.of(pdu), requestContext);
}); });
} }
@ -352,7 +388,7 @@ public class SnmpTransportService implements TbTransportService, CommandResponde
JsonObject responseData = responseDataMappers.get(requestContext.getCommunicationSpec()).map(response, requestContext); JsonObject responseData = responseDataMappers.get(requestContext.getCommunicationSpec()).map(response, requestContext);
if (responseData.size() == 0) { if (responseData.size() == 0) {
log.warn("No values in the SNMP response for device {}", sessionContext.getDeviceId()); log.warn("[{}] No values in the response", sessionContext.getDeviceId());
throw new IllegalArgumentException("No values in the response"); throw new IllegalArgumentException("No values in the response");
} }
@ -428,11 +464,11 @@ public class SnmpTransportService implements TbTransportService, CommandResponde
@PreDestroy @PreDestroy
public void shutdown() { public void shutdown() {
log.info("Stopping SNMP transport!"); log.info("Stopping SNMP transport!");
if (queryingExecutor != null) { if (scheduler != null) {
queryingExecutor.shutdownNow(); scheduler.shutdownNow();
} }
if (responseProcessingExecutor != null) { if (executor != null) {
responseProcessingExecutor.shutdownNow(); executor.shutdownNow();
} }
if (snmp != null) { if (snmp != null) {
try { try {

6
common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/DeviceSessionContext.java

@ -45,7 +45,6 @@ import org.thingsboard.server.transport.snmp.SnmpTransportContext;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@Slf4j @Slf4j
@ -60,7 +59,8 @@ public class DeviceSessionContext extends DeviceAwareSessionContext implements S
@Setter @Setter
private SnmpDeviceTransportConfiguration deviceTransportConfiguration; private SnmpDeviceTransportConfiguration deviceTransportConfiguration;
@Getter @Getter
private final Device device; @Setter
private Device device;
@Getter @Getter
private final TenantId tenantId; private final TenantId tenantId;
@ -73,7 +73,7 @@ public class DeviceSessionContext extends DeviceAwareSessionContext implements S
private Runnable sessionTimeoutHandler; private Runnable sessionTimeoutHandler;
@Getter @Getter
private final List<ScheduledFuture<?>> queryingTasks = new LinkedList<>(); private final List<ScheduledTask> queryingTasks = new LinkedList<>();
@Builder @Builder
public DeviceSessionContext(TenantId tenantId, Device device, DeviceProfile deviceProfile, String token, public DeviceSessionContext(TenantId tenantId, Device device, DeviceProfile deviceProfile, String token,

62
common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/ScheduledTask.java

@ -0,0 +1,62 @@
/**
* 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.snmp.session;
import com.google.common.util.concurrent.AsyncCallable;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Data
@Slf4j
public class ScheduledTask {
private ListenableFuture<?> scheduledFuture;
private boolean stopped = false;
public void init(AsyncCallable<Void> task, long delayMs, ScheduledExecutorService scheduler) {
schedule(task, delayMs, scheduler);
}
private void schedule(AsyncCallable<Void> task, long delayMs, ScheduledExecutorService scheduler) {
scheduledFuture = Futures.scheduleAsync(() -> {
if (stopped) {
return Futures.immediateCancelledFuture();
}
try {
return task.call();
} catch (Throwable t) {
log.error("Unhandled error in scheduled task", t);
return Futures.immediateFailedFuture(t);
}
}, delayMs, TimeUnit.MILLISECONDS, scheduler);
if (!stopped) {
scheduledFuture.addListener(() -> schedule(task, delayMs, scheduler), MoreExecutors.directExecutor());
}
}
public void cancel() {
stopped = true;
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}
}
}

19
common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpDeviceSimulatorV2.java

@ -58,11 +58,10 @@ public class SnmpDeviceSimulatorV2 extends BaseAgent {
private final Target target; private final Target target;
private final Address address; private final Address address;
private final Map<String, String> mappings;
private Snmp snmp; private Snmp snmp;
private final String password; public SnmpDeviceSimulatorV2(int port, String password, Map<String, String> mappings) throws IOException {
public SnmpDeviceSimulatorV2(int port, String password) throws IOException {
super(new File("conf.agent"), new File("bootCounter.agent"), new CommandProcessor(new OctetString("12312"))); super(new File("conf.agent"), new File("bootCounter.agent"), new CommandProcessor(new OctetString("12312")));
CommunityTarget target = new CommunityTarget(); CommunityTarget target = new CommunityTarget();
target.setCommunity(new OctetString(password)); target.setCommunity(new OctetString(password));
@ -72,7 +71,7 @@ public class SnmpDeviceSimulatorV2 extends BaseAgent {
target.setTimeout(1500); target.setTimeout(1500);
target.setVersion(SnmpConstants.version2c); target.setVersion(SnmpConstants.version2c);
this.target = target; this.target = target;
this.password = password; this.mappings = mappings;
} }
public void start() throws IOException { public void start() throws IOException {
@ -85,13 +84,6 @@ public class SnmpDeviceSimulatorV2 extends BaseAgent {
snmp = new Snmp(transportMappings[0]); snmp = new Snmp(transportMappings[0]);
} }
public void setUpMappings(Map<String, String> oidToResponseMappings) {
unregisterManagedObject(getSnmpv2MIB());
oidToResponseMappings.forEach((oid, response) -> {
registerManagedObject(new MOScalar<>(new OID(oid), MOAccessImpl.ACCESS_READ_WRITE, new OctetString(response)));
});
}
public void sendTrap(String host, int port, Map<String, String> values) throws IOException { public void sendTrap(String host, int port, Map<String, String> values) throws IOException {
PDU pdu = new PDU(); PDU pdu = new PDU();
pdu.addAll(values.entrySet().stream() pdu.addAll(values.entrySet().stream()
@ -107,6 +99,10 @@ public class SnmpDeviceSimulatorV2 extends BaseAgent {
@Override @Override
protected void registerManagedObjects() { protected void registerManagedObjects() {
unregisterManagedObject(getSnmpv2MIB());
mappings.forEach((oid, response) -> {
registerManagedObject(new MOScalar<>(new OID(oid), MOAccessImpl.ACCESS_READ_WRITE, new OctetString(response)));
});
} }
protected void registerManagedObject(ManagedObject mo) { protected void registerManagedObject(ManagedObject mo) {
@ -152,6 +148,7 @@ public class SnmpDeviceSimulatorV2 extends BaseAgent {
} }
protected void unregisterManagedObjects() { protected void unregisterManagedObjects() {
unregisterManagedObject(getSnmpv2MIB());
} }
protected void addCommunities(SnmpCommunityMIB communityMIB) { protected void addCommunities(SnmpCommunityMIB communityMIB) {

42
common/transport/snmp/src/test/java/org/thingsboard/server/transport/snmp/SnmpTestV2.java

@ -15,28 +15,34 @@
*/ */
package org.thingsboard.server.transport.snmp; package org.thingsboard.server.transport.snmp;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import org.thingsboard.common.util.JacksonUtil;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.Scanner; import java.util.Scanner;
import java.util.stream.Collectors;
public class SnmpTestV2 { public class SnmpTestV2 {
private static final Scanner scanner = new Scanner(System.in); private static final Scanner scanner = new Scanner(System.in);
public static void main(String[] args) throws IOException { public static void main(String[] args) throws IOException {
SnmpDeviceSimulatorV2 client = new SnmpDeviceSimulatorV2(1610, "public"); Map<String, String> mappings = new LinkedHashMap<>();
for (int i = 1; i <= 50; i++) {
String oid = String.format("1.3.6.1.2.1.%s.1.52", i);
mappings.put(oid, "value_" + i);
}
client.start(); SnmpDeviceSimulatorV2 device = new SnmpDeviceSimulatorV2(1610, "public", mappings);
Map<String, String> mappings = new HashMap<>(); device.start();
// for (int i = 1; i <= 500; i++) {
// String oid = String.format(".1.3.6.1.2.1.%s.1.52", i);
// mappings.put(oid, "value_" + i);
// }
mappings.put("1.3.6.1.2.1.266.1.52", "****");
client.setUpMappings(mappings); System.out.println("Hosting the following values:\n" + mappings.entrySet().stream()
inputTraps(client); .map(entry -> entry.getKey() + " - " + entry.getValue())
.collect(Collectors.joining("\n")));
scanner.nextLine(); scanner.nextLine();
} }
@ -53,4 +59,18 @@ public class SnmpTestV2 {
} }
} }
private static void updateDeviceProfile(String file) throws Exception {
File profileFile = new File(file);
JsonNode deviceProfile = JacksonUtil.OBJECT_MAPPER.readTree(profileFile);
ArrayNode mappingsJson = (ArrayNode) deviceProfile.at("/profileData/transportConfiguration/communicationConfigs/0/mappings");
for (int i = 1; i <= 50; i++) {
String oid = String.format(".1.3.6.1.2.1.%s.1.52", i);
mappingsJson.add(JacksonUtil.newObjectNode()
.put("oid", oid)
.put("key", "key_" + i)
.put("dataType", "STRING"));
}
JacksonUtil.OBJECT_MAPPER.writeValue(profileFile, deviceProfile);
}
} }

43
common/transport/snmp/src/test/resources/snmp-device-profile-transport-config.json

@ -1,43 +0,0 @@
{
"timeoutMs": 500,
"retries": 0,
"communicationConfigs": [
{
"spec": "TELEMETRY_QUERYING",
"queryingFrequencyMs": 3000,
"mappings": [
{
"oid": ".1.3.6.1.2.1.1.1.50",
"key": "temperature",
"dataType": "LONG"
},
{
"oid": ".1.3.6.1.2.1.2.1.52",
"key": "humidity",
"dataType": "DOUBLE"
}
]
},
{
"spec": "CLIENT_ATTRIBUTES_QUERYING",
"queryingFrequencyMs": 5000,
"mappings": [
{
"oid": ".1.3.6.1.2.1.3.1.54",
"key": "isCool",
"dataType": "STRING"
}
]
},
{
"spec": "SHARED_ATTRIBUTES_SETTING",
"mappings": [
{
"oid": ".1.3.6.1.2.1.7.1.58",
"key": "shared",
"dataType": "STRING"
}
]
}
]
}

13
common/transport/snmp/src/test/resources/snmp-device-transport-config-v3.json

@ -1,13 +0,0 @@
{
"address": "192.168.3.23",
"port": 1610,
"protocolVersion": "V3",
"username": "tb-user",
"engineId": "qwertyuioa",
"securityName": "tb-user",
"authenticationProtocol": "SHA_512",
"authenticationPassphrase": "sdfghjkloifgh",
"privacyProtocol": "DES",
"privacyPassphrase": "rtytguijokod"
}

6
common/transport/snmp/src/test/resources/snmp-device-transport-config.json

@ -1,6 +0,0 @@
{
"address": "127.0.0.1",
"port": 1610,
"community": "public",
"protocolVersion": "V2C"
}

289
common/transport/snmp/src/test/resources/snmp_device_profile.json

@ -0,0 +1,289 @@
{
"name": "SNMP Device Profile",
"description": "",
"image": null,
"type": "DEFAULT",
"transportType": "SNMP",
"provisionType": "DISABLED",
"defaultRuleChainId": null,
"defaultDashboardId": null,
"defaultQueueName": null,
"profileData": {
"configuration": {
"type": "DEFAULT"
},
"transportConfiguration": {
"type": "SNMP",
"timeoutMs": 500,
"retries": 0,
"communicationConfigs": [
{
"spec": "TELEMETRY_QUERYING",
"mappings": [
{
"oid": ".1.3.6.1.2.1.1.1.52",
"key": "key_1",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.2.1.52",
"key": "key_2",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.3.1.52",
"key": "key_3",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.4.1.52",
"key": "key_4",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.5.1.52",
"key": "key_5",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.6.1.52",
"key": "key_6",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.7.1.52",
"key": "key_7",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.8.1.52",
"key": "key_8",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.9.1.52",
"key": "key_9",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.10.1.52",
"key": "key_10",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.11.1.52",
"key": "key_11",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.12.1.52",
"key": "key_12",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.13.1.52",
"key": "key_13",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.14.1.52",
"key": "key_14",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.15.1.52",
"key": "key_15",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.16.1.52",
"key": "key_16",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.17.1.52",
"key": "key_17",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.18.1.52",
"key": "key_18",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.19.1.52",
"key": "key_19",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.20.1.52",
"key": "key_20",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.21.1.52",
"key": "key_21",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.22.1.52",
"key": "key_22",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.23.1.52",
"key": "key_23",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.24.1.52",
"key": "key_24",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.25.1.52",
"key": "key_25",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.26.1.52",
"key": "key_26",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.27.1.52",
"key": "key_27",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.28.1.52",
"key": "key_28",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.29.1.52",
"key": "key_29",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.30.1.52",
"key": "key_30",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.31.1.52",
"key": "key_31",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.32.1.52",
"key": "key_32",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.33.1.52",
"key": "key_33",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.34.1.52",
"key": "key_34",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.35.1.52",
"key": "key_35",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.36.1.52",
"key": "key_36",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.37.1.52",
"key": "key_37",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.38.1.52",
"key": "key_38",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.39.1.52",
"key": "key_39",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.40.1.52",
"key": "key_40",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.41.1.52",
"key": "key_41",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.42.1.52",
"key": "key_42",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.43.1.52",
"key": "key_43",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.44.1.52",
"key": "key_44",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.45.1.52",
"key": "key_45",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.46.1.52",
"key": "key_46",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.47.1.52",
"key": "key_47",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.48.1.52",
"key": "key_48",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.49.1.52",
"key": "key_49",
"dataType": "STRING"
},
{
"oid": ".1.3.6.1.2.1.50.1.52",
"key": "key_50",
"dataType": "STRING"
}
],
"queryingFrequencyMs": 5000
}
]
},
"provisionConfiguration": {
"type": "DISABLED",
"provisionDeviceSecret": null
},
"alarms": null
},
"provisionDeviceKey": null,
"firmwareId": null,
"softwareId": null,
"defaultEdgeRuleChainId": null,
"default": false
}

14
common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java

@ -781,7 +781,11 @@ public class DefaultTransportService extends TransportActivityManager implements
.setSuccess(success) .setSuccess(success)
.setError(error != null ? ExceptionUtils.getStackTrace(error) : "")) .setError(error != null ? ExceptionUtils.getStackTrace(error) : ""))
.build(); .build();
sendToCore(tenantId, deviceId, msg, deviceId.getId(), TransportServiceCallback.EMPTY); try {
sendToCore(tenantId, deviceId, msg, deviceId.getId(), TransportServiceCallback.EMPTY);
} catch (Exception e) {
log.error("[{}][{}] Failed to send lifecycle event to core", tenantId, deviceId, e);
}
} }
@Override @Override
@ -794,9 +798,13 @@ public class DefaultTransportService extends TransportActivityManager implements
.setEntityIdLSB(deviceId.getId().getLeastSignificantBits()) .setEntityIdLSB(deviceId.getId().getLeastSignificantBits())
.setServiceId(serviceInfoProvider.getServiceId()) .setServiceId(serviceInfoProvider.getServiceId())
.setMethod(method) .setMethod(method)
.setError(ExceptionUtils.getStackTrace(error))) .setError(ExceptionUtils.getRootCauseMessage(error)))
.build(); .build();
sendToCore(tenantId, deviceId, msg, deviceId.getId(), TransportServiceCallback.EMPTY); try {
sendToCore(tenantId, deviceId, msg, deviceId.getId(), TransportServiceCallback.EMPTY);
} catch (Exception e) {
log.error("[{}][{}] Failed to send error event to core", tenantId, deviceId, e);
}
} }
@Override @Override

3
dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java

@ -162,16 +162,19 @@ public class DeviceServiceImpl extends AbstractCachedEntityService<DeviceCacheKe
() -> deviceDao.findDeviceByTenantIdAndName(tenantId.getId(), name).orElse(null), true); () -> deviceDao.findDeviceByTenantIdAndName(tenantId.getId(), name).orElse(null), true);
} }
@Transactional
@Override @Override
public Device saveDeviceWithAccessToken(Device device, String accessToken) { public Device saveDeviceWithAccessToken(Device device, String accessToken) {
return doSaveDevice(device, accessToken, true); return doSaveDevice(device, accessToken, true);
} }
@Transactional
@Override @Override
public Device saveDevice(Device device, boolean doValidate) { public Device saveDevice(Device device, boolean doValidate) {
return doSaveDevice(device, null, doValidate); return doSaveDevice(device, null, doValidate);
} }
@Transactional
@Override @Override
public Device saveDevice(Device device) { public Device saveDevice(Device device) {
return doSaveDevice(device, null, true); return doSaveDevice(device, null, true);

12
dao/src/main/java/org/thingsboard/server/dao/edge/BaseEdgeEventService.java

@ -26,11 +26,15 @@ import org.jetbrains.annotations.NotNull;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.edge.EdgeEvent; import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent;
import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.DataValidator;
@ -43,7 +47,7 @@ import java.util.concurrent.Executors;
public class BaseEdgeEventService implements EdgeEventService { public class BaseEdgeEventService implements EdgeEventService {
private final EdgeEventDao edgeEventDao; private final EdgeEventDao edgeEventDao;
private final RateLimitService rateLimitService;
private final DataValidator<EdgeEvent> edgeEventValidator; private final DataValidator<EdgeEvent> edgeEventValidator;
private final ApplicationEventPublisher eventPublisher; private final ApplicationEventPublisher eventPublisher;
@ -64,6 +68,12 @@ public class BaseEdgeEventService implements EdgeEventService {
@Override @Override
public ListenableFuture<Void> saveAsync(EdgeEvent edgeEvent) { public ListenableFuture<Void> saveAsync(EdgeEvent edgeEvent) {
if (!rateLimitService.checkRateLimit(LimitedApi.EDGE_EVENTS, edgeEvent.getTenantId())) {
throw new TbRateLimitsException(EntityType.TENANT);
}
if (!rateLimitService.checkRateLimit(LimitedApi.EDGE_EVENTS_PER_EDGE, edgeEvent.getTenantId(), edgeEvent.getEdgeId())) {
throw new TbRateLimitsException(EntityType.EDGE);
}
edgeEventValidator.validate(edgeEvent, EdgeEvent::getTenantId); edgeEventValidator.validate(edgeEvent, EdgeEvent::getTenantId);
ListenableFuture<Void> saveFuture = edgeEventDao.saveAsync(edgeEvent); ListenableFuture<Void> saveFuture = edgeEventDao.saveAsync(edgeEvent);

42
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationSettingsService.java

@ -41,6 +41,7 @@ import org.thingsboard.server.common.data.notification.targets.platform.SystemAd
import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter; import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType; import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.settings.UserSettings; import org.thingsboard.server.common.data.settings.UserSettings;
import org.thingsboard.server.common.data.settings.UserSettingsType; import org.thingsboard.server.common.data.settings.UserSettingsType;
@ -53,6 +54,7 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@ -187,6 +189,8 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS
defaultNotifications.create(tenantId, DefaultNotifications.alarmComment, tenantAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.alarmComment, tenantAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.alarmAssignment, affectedUser.getId()); defaultNotifications.create(tenantId, DefaultNotifications.alarmAssignment, affectedUser.getId());
defaultNotifications.create(tenantId, DefaultNotifications.ruleEngineComponentLifecycleFailure, tenantAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.ruleEngineComponentLifecycleFailure, tenantAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.edgeConnection, tenantAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.edgeCommunicationFailures, tenantAdmins.getId());
} }
@Override @Override
@ -198,17 +202,43 @@ public class DefaultNotificationSettingsService implements NotificationSettingsS
} }
NotificationTarget sysAdmins = notificationTargetService.findNotificationTargetsByTenantIdAndUsersFilterType(tenantId, UsersFilterType.SYSTEM_ADMINISTRATORS).stream() NotificationTarget sysAdmins = notificationTargetService.findNotificationTargetsByTenantIdAndUsersFilterType(tenantId, UsersFilterType.SYSTEM_ADMINISTRATORS).stream()
.findFirst().orElseGet(() -> { .findFirst().orElseGet(() -> createTarget(tenantId, "System administrators", new SystemAdministratorsFilter(), "All system administrators"));
return createTarget(tenantId, "System administrators", new SystemAdministratorsFilter(), "All system administrators");
});
NotificationTarget affectedTenantAdmins = notificationTargetService.findNotificationTargetsByTenantIdAndUsersFilterType(tenantId, UsersFilterType.AFFECTED_TENANT_ADMINISTRATORS).stream() NotificationTarget affectedTenantAdmins = notificationTargetService.findNotificationTargetsByTenantIdAndUsersFilterType(tenantId, UsersFilterType.AFFECTED_TENANT_ADMINISTRATORS).stream()
.findFirst().orElseGet(() -> { .findFirst().orElseGet(() -> createTarget(tenantId, "Affected tenant's administrators", new AffectedTenantAdministratorsFilter(), ""));
return createTarget(tenantId, "Affected tenant's administrators", new AffectedTenantAdministratorsFilter(), "");
});
defaultNotifications.create(tenantId, DefaultNotifications.exceededRateLimits, affectedTenantAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.exceededRateLimits, affectedTenantAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.exceededPerEntityRateLimits, affectedTenantAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.exceededPerEntityRateLimits, affectedTenantAdmins.getId());
defaultNotifications.create(tenantId, DefaultNotifications.exceededRateLimitsForSysadmin, sysAdmins.getId()); defaultNotifications.create(tenantId, DefaultNotifications.exceededRateLimitsForSysadmin, sysAdmins.getId());
} else {
var requiredNotificationTypes = List.of(NotificationType.EDGE_CONNECTION, NotificationType.EDGE_COMMUNICATION_FAILURE);
var existingNotificationTypes = notificationTemplateService.findNotificationTemplatesByTenantIdAndNotificationTypes(
tenantId, requiredNotificationTypes, new PageLink(1))
.getData()
.stream()
.map(NotificationTemplate::getNotificationType)
.collect(Collectors.toSet());
if (existingNotificationTypes.containsAll(requiredNotificationTypes)) {
return;
}
NotificationTarget tenantAdmins = notificationTargetService.findNotificationTargetsByTenantIdAndUsersFilterType(tenantId, UsersFilterType.TENANT_ADMINISTRATORS)
.stream()
.findFirst()
.orElseGet(() -> createTarget(tenantId, "Tenant administrators", new TenantAdministratorsFilter(), "Tenant administrators"));
for (NotificationType type : requiredNotificationTypes) {
if (!existingNotificationTypes.contains(type)) {
switch (type) {
case EDGE_CONNECTION:
defaultNotifications.create(tenantId, DefaultNotifications.edgeConnection, tenantAdmins.getId());
break;
case EDGE_COMMUNICATION_FAILURE:
defaultNotifications.create(tenantId, DefaultNotifications.edgeCommunicationFailures, tenantAdmins.getId());
break;
}
}
}
} }
} }

34
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java

@ -40,6 +40,9 @@ import org.thingsboard.server.common.data.notification.rule.trigger.config.Alarm
import org.thingsboard.server.common.data.notification.rule.trigger.config.ApiUsageLimitNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.ApiUsageLimitNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig.DeviceEvent; import org.thingsboard.server.common.data.notification.rule.trigger.config.DeviceActivityNotificationRuleTriggerConfig.DeviceEvent;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeConnectionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeConnectionNotificationRuleTriggerConfig.EdgeConnectivityEvent;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EdgeCommunicationFailureNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EntitiesLimitNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.EntitiesLimitNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.EntityActionNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.EntityActionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NewPlatformVersionNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.config.NewPlatformVersionNotificationRuleTriggerConfig;
@ -325,6 +328,35 @@ public class DefaultNotifications {
.description("Send notification to tenant admins when any Rule chain or Rule node failed to start, update or stop") .description("Send notification to tenant admins when any Rule chain or Rule node failed to start, update or stop")
.build()) .build())
.build(); .build();
public static final DefaultNotification edgeConnection = DefaultNotification.builder()
.name("Edge connection notification")
.type(NotificationType.EDGE_CONNECTION)
.subject("Edge connection status change")
.text("Edge '${edgeName}' is now ${eventType}")
.icon("info").color(null)
.button("Go to Edge").link("/edgeManagement/instances/${edgeId}")
.rule(DefaultRule.builder()
.name("Edge connection status change")
.triggerConfig(EdgeConnectionNotificationRuleTriggerConfig.builder()
.edges(null)
.notifyOn(Set.of(EdgeConnectivityEvent.CONNECTED, EdgeConnectivityEvent.DISCONNECTED))
.build())
.description("Send notification to tenant admins when the connection status between TB and Edge changes")
.build())
.build();
public static final DefaultNotification edgeCommunicationFailures = DefaultNotification.builder()
.name("Edge communication failure notification")
.type(NotificationType.EDGE_COMMUNICATION_FAILURE)
.subject("Edge '${edgeName}' communication failure occured")
.text("Failure message: '${failureMsg}'")
.icon("error").color(RED_COLOR)
.button("Go to Edge").link("/edgeManagement/instances/${edgeId}")
.rule(DefaultRule.builder()
.name("Edge communication failure")
.triggerConfig(EdgeCommunicationFailureNotificationRuleTriggerConfig.builder().edges(null).build())
.description("Send notification to tenant admins when communication failures occur")
.build())
.build();
public static final DefaultNotification jwtSigningKeyIssue = DefaultNotification.builder() public static final DefaultNotification jwtSigningKeyIssue = DefaultNotification.builder()
.name("JWT Signing Key issue notification") .name("JWT Signing Key issue notification")
@ -346,7 +378,7 @@ public class DefaultNotifications {
if (defaultNotification.getRule() != null && targets.length > 0) { if (defaultNotification.getRule() != null && targets.length > 0) {
NotificationRule rule = defaultNotification.toRule(template.getId(), targets); NotificationRule rule = defaultNotification.toRule(template.getId(), targets);
rule.setTenantId(tenantId); rule.setTenantId(tenantId);
rule = ruleService.saveNotificationRule(tenantId, rule); ruleService.saveNotificationRule(tenantId, rule);
} }
} }

132
dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java

@ -65,6 +65,7 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
@ -85,6 +86,18 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
public static final String ASC_ORDER = "ASC"; public static final String ASC_ORDER = "ASC";
public static final long SECONDS_IN_DAY = TimeUnit.DAYS.toSeconds(1); public static final long SECONDS_IN_DAY = TimeUnit.DAYS.toSeconds(1);
protected static final List<Long> FIXED_PARTITION = List.of(0L); protected static final List<Long> FIXED_PARTITION = List.of(0L);
protected static final String INSERT_WITH_NULL = INSERT_INTO + ModelConstants.TS_KV_CF +
"(" + ModelConstants.ENTITY_TYPE_COLUMN +
"," + ModelConstants.ENTITY_ID_COLUMN +
"," + ModelConstants.KEY_COLUMN +
"," + ModelConstants.PARTITION_COLUMN +
"," + ModelConstants.TS_COLUMN +
"," + ModelConstants.BOOLEAN_VALUE_COLUMN +
"," + ModelConstants.STRING_VALUE_COLUMN +
"," + ModelConstants.LONG_VALUE_COLUMN +
"," + ModelConstants.DOUBLE_VALUE_COLUMN +
"," + ModelConstants.JSON_VALUE_COLUMN + ")" +
" VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
private CassandraTsPartitionsCache cassandraTsPartitionsCache; private CassandraTsPartitionsCache cassandraTsPartitionsCache;
@ -117,6 +130,8 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
private PreparedStatement[] fetchStmtsAsc; private PreparedStatement[] fetchStmtsAsc;
private PreparedStatement[] fetchStmtsDesc; private PreparedStatement[] fetchStmtsDesc;
private PreparedStatement deleteStmt; private PreparedStatement deleteStmt;
private PreparedStatement saveWithNullStmt;
private PreparedStatement saveWithNullWithTtlStmt;
private final Lock stmtCreationLock = new ReentrantLock(); private final Lock stmtCreationLock = new ReentrantLock();
private boolean isInstall() { private boolean isInstall() {
@ -159,19 +174,36 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
ttl = computeTtl(ttl); ttl = computeTtl(ttl);
int dataPointDays = tsKvEntry.getDataPoints() * Math.max(1, (int) (ttl / SECONDS_IN_DAY)); int dataPointDays = tsKvEntry.getDataPoints() * Math.max(1, (int) (ttl / SECONDS_IN_DAY));
long partition = toPartitionTs(tsKvEntry.getTs()); long partition = toPartitionTs(tsKvEntry.getTs());
String entityType = entityId.getEntityType().name();
UUID entityIdId = entityId.getId();
String entryKey = tsKvEntry.getKey();
long ts = tsKvEntry.getTs();
DataType type = tsKvEntry.getDataType(); DataType type = tsKvEntry.getDataType();
BoundStatementBuilder stmtBuilder;
if (setNullValuesEnabled) { if (setNullValuesEnabled) {
processSetNullValues(tenantId, entityId, tsKvEntry, ttl, futures, partition, type); Boolean booleanValue = tsKvEntry.getBooleanValue().orElse(null);
} String strValue = tsKvEntry.getStrValue().orElse(null);
BoundStatementBuilder stmtBuilder = new BoundStatementBuilder((ttl == 0 ? getSaveStmt(type) : getSaveTtlStmt(type)).bind()); Long longValue = tsKvEntry.getLongValue().orElse(null);
stmtBuilder.setString(0, entityId.getEntityType().name()) Double doubleValue = tsKvEntry.getDoubleValue().orElse(null);
.setUuid(1, entityId.getId()) String jsonValue = tsKvEntry.getJsonValue().orElse(null);
.setString(2, tsKvEntry.getKey()) if (ttl == 0) {
.setLong(3, partition) stmtBuilder = new BoundStatementBuilder(getSaveWithNullStmt()
.setLong(4, tsKvEntry.getTs()); .bind(entityType, entityIdId, entryKey, partition, ts, booleanValue, strValue, longValue, doubleValue, jsonValue));
addValue(tsKvEntry, stmtBuilder, 5); } else {
if (ttl > 0) { stmtBuilder = new BoundStatementBuilder(getSaveWithNullWithTtlStmt()
stmtBuilder.setInt(6, (int) ttl); .bind(entityType, entityIdId, entryKey, partition, ts, booleanValue, strValue, longValue, doubleValue, jsonValue, (int) ttl));
}
} else {
stmtBuilder = new BoundStatementBuilder((ttl == 0 ? getSaveStmt(type) : getSaveTtlStmt(type)).bind());
stmtBuilder.setString(0, entityType)
.setUuid(1, entityIdId)
.setString(2, entryKey)
.setLong(3, partition)
.setLong(4, ts);
addValue(tsKvEntry, stmtBuilder, 5);
if (ttl > 0) {
stmtBuilder.setInt(6, (int) ttl);
}
} }
BoundStatement stmt = stmtBuilder.build(); BoundStatement stmt = stmtBuilder.build();
futures.add(getFuture(executeAsyncWrite(tenantId, stmt), rs -> null)); futures.add(getFuture(executeAsyncWrite(tenantId, stmt), rs -> null));
@ -449,56 +481,6 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
return tsFormat.getTruncateUnit().equals(ChronoUnit.FOREVER); return tsFormat.getTruncateUnit().equals(ChronoUnit.FOREVER);
} }
private void processSetNullValues(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl, List<ListenableFuture<Void>> futures, long partition, DataType type) {
switch (type) {
case LONG:
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.BOOLEAN));
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.DOUBLE));
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.STRING));
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.JSON));
break;
case BOOLEAN:
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.DOUBLE));
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.LONG));
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.STRING));
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.JSON));
break;
case DOUBLE:
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.BOOLEAN));
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.LONG));
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.STRING));
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.JSON));
break;
case STRING:
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.BOOLEAN));
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.DOUBLE));
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.LONG));
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.JSON));
break;
case JSON:
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.BOOLEAN));
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.DOUBLE));
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.LONG));
futures.add(saveNull(tenantId, entityId, tsKvEntry, ttl, partition, DataType.STRING));
break;
}
}
private ListenableFuture<Void> saveNull(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry, long ttl, long partition, DataType type) {
BoundStatementBuilder stmtBuilder = new BoundStatementBuilder((ttl == 0 ? getSaveStmt(type) : getSaveTtlStmt(type)).bind());
stmtBuilder.setString(0, entityId.getEntityType().name())
.setUuid(1, entityId.getId())
.setString(2, tsKvEntry.getKey())
.setLong(3, partition)
.setLong(4, tsKvEntry.getTs());
stmtBuilder.setToNull(getColumnName(type));
if (ttl > 0) {
stmtBuilder.setInt(6, (int) ttl);
}
BoundStatement stmt = stmtBuilder.build();
return getFuture(executeAsyncWrite(tenantId, stmt), rs -> null);
}
private ListenableFuture<Integer> doSavePartition(TenantId tenantId, EntityId entityId, String key, long ttl, long partition) { private ListenableFuture<Integer> doSavePartition(TenantId tenantId, EntityId entityId, String key, long ttl, long partition) {
log.debug("Saving partition {} for the entity [{}-{}] and key {}", partition, entityId.getEntityType(), entityId.getId(), key); log.debug("Saving partition {} for the entity [{}-{}] and key {}", partition, entityId.getEntityType(), entityId.getId(), key);
PreparedStatement preparedStatement = ttl == 0 ? getPartitionInsertStmt() : getPartitionInsertTtlStmt(); PreparedStatement preparedStatement = ttl == 0 ? getPartitionInsertStmt() : getPartitionInsertTtlStmt();
@ -591,6 +573,34 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD
return deleteStmt; return deleteStmt;
} }
private PreparedStatement getSaveWithNullStmt() {
if (saveWithNullStmt == null) {
stmtCreationLock.lock();
try {
if (saveWithNullStmt == null) {
saveWithNullStmt = prepare(INSERT_WITH_NULL);
}
} finally {
stmtCreationLock.unlock();
}
}
return saveWithNullStmt;
}
private PreparedStatement getSaveWithNullWithTtlStmt() {
if (saveWithNullWithTtlStmt == null) {
stmtCreationLock.lock();
try {
if (saveWithNullWithTtlStmt == null) {
saveWithNullWithTtlStmt = prepare(INSERT_WITH_NULL + " USING TTL ?");
}
} finally {
stmtCreationLock.unlock();
}
}
return saveWithNullWithTtlStmt;
}
private PreparedStatement getSaveStmt(DataType dataType) { private PreparedStatement getSaveStmt(DataType dataType) {
if (saveStmts == null) { if (saveStmts == null) {
stmtCreationLock.lock(); stmtCreationLock.lock();

72
dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java

@ -16,12 +16,15 @@
package org.thingsboard.server.dao.service; package org.thingsboard.server.dao.service;
import com.datastax.oss.driver.api.core.uuid.Uuids; import com.datastax.oss.driver.api.core.uuid.Uuids;
import org.hibernate.exception.ConstraintViolationException;
import org.junit.After; import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition; import org.springframework.transaction.support.DefaultTransactionDefinition;
@ -35,8 +38,6 @@ import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; import org.thingsboard.server.common.data.ota.ChecksumAlgorithm;
@ -50,15 +51,19 @@ import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService; import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException;
import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.ota.OtaPackageService;
import org.thingsboard.server.dao.service.validator.DeviceCredentialsDataValidator;
import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantProfileService;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE; import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
@ -79,6 +84,8 @@ public class DeviceServiceTest extends AbstractServiceTest {
TenantProfileService tenantProfileService; TenantProfileService tenantProfileService;
@Autowired @Autowired
private PlatformTransactionManager platformTransactionManager; private PlatformTransactionManager platformTransactionManager;
@SpyBean
private DeviceCredentialsDataValidator validator;
private IdComparator<Device> idComparator = new IdComparator<>(); private IdComparator<Device> idComparator = new IdComparator<>();
private TenantId anotherTenantId; private TenantId anotherTenantId;
@ -129,6 +136,67 @@ public class DeviceServiceTest extends AbstractServiceTest {
}); });
} }
@Test
public void testSaveDevicesWithTheSameAccessToken() {
Device device = new Device();
device.setTenantId(tenantId);
device.setName(StringUtils.randomAlphabetic(10));
device.setType("default");
String accessToken = StringUtils.generateSafeToken(10);
Device savedDevice = deviceService.saveDeviceWithAccessToken(device, accessToken);
DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, savedDevice.getId());
Assert.assertEquals(accessToken, deviceCredentials.getCredentialsId());
Device duplicatedDevice = new Device();
duplicatedDevice.setTenantId(tenantId);
duplicatedDevice.setName(StringUtils.randomAlphabetic(10));
duplicatedDevice.setType("default");
assertThatThrownBy(() -> deviceService.saveDeviceWithAccessToken(duplicatedDevice, accessToken))
.isInstanceOf(DeviceCredentialsValidationException.class)
.hasMessageContaining("Device credentials are already assigned to another device!");
Device deviceByName = deviceService.findDeviceByTenantIdAndName(tenantId, duplicatedDevice.getName());
Assertions.assertNull(deviceByName);
}
@Test
public void testShouldRollbackNotValidatedDeviceIfDeviceCredentialsValidationFailed() {
Mockito.reset(validator);
Mockito.doThrow(new DataValidationException("mock message"))
.when(validator).validate(any(), any());
Device device = new Device();
device.setTenantId(tenantId);
device.setName(StringUtils.randomAlphabetic(10));
device.setType("default");
assertThatThrownBy(() -> deviceService.saveDevice(device, false))
.isInstanceOf(DataValidationException.class)
.hasMessageContaining("mock message");
Device deviceByName = deviceService.findDeviceByTenantIdAndName(tenantId, device.getName());
Assertions.assertNull(deviceByName);
}
@Test
public void testShouldRollbackValidatedDeviceIfDeviceCredentialsValidationFailed() {
Mockito.reset(validator);
Mockito.doThrow(new DataValidationException("mock message"))
.when(validator).validate(any(), any());
Device device = new Device();
device.setTenantId(tenantId);
device.setName(StringUtils.randomAlphabetic(10));
device.setType("default");
assertThatThrownBy(() -> deviceService.saveDevice(device))
.isInstanceOf(DataValidationException.class)
.hasMessageContaining("mock message");
Device deviceByName = deviceService.findDeviceByTenantIdAndName(tenantId, device.getName());
Assertions.assertNull(deviceByName);
}
@Test @Test
public void testCountByTenantId() { public void testCountByTenantId() {
Assert.assertEquals(0, deviceService.countByTenantId(tenantId)); Assert.assertEquals(0, deviceService.countByTenantId(tenantId));

24
dao/src/test/java/org/thingsboard/server/dao/service/EdgeEventServiceTest.java

@ -40,7 +40,7 @@ import java.text.ParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import static org.apache.commons.lang3.time.DateFormatUtils.ISO_DATETIME_TIME_ZONE_FORMAT; import static org.apache.commons.lang3.time.DateFormatUtils.ISO_8601_EXTENDED_DATETIME_FORMAT;
@DaoSqlTest @DaoSqlTest
public class EdgeEventServiceTest extends AbstractServiceTest { public class EdgeEventServiceTest extends AbstractServiceTest {
@ -56,19 +56,18 @@ public class EdgeEventServiceTest extends AbstractServiceTest {
@Before @Before
public void before() throws ParseException { public void before() throws ParseException {
timeBeforeStartTime = ISO_DATETIME_TIME_ZONE_FORMAT.parse("2016-11-01T11:30:00Z").getTime(); timeBeforeStartTime = ISO_8601_EXTENDED_DATETIME_FORMAT.parse("2016-11-01T11:30:00").getTime();
startTime = ISO_DATETIME_TIME_ZONE_FORMAT.parse("2016-11-01T12:00:00Z").getTime(); startTime = ISO_8601_EXTENDED_DATETIME_FORMAT.parse("2016-11-01T12:00:00").getTime();
eventTime = ISO_DATETIME_TIME_ZONE_FORMAT.parse("2016-11-01T12:30:00Z").getTime(); eventTime = ISO_8601_EXTENDED_DATETIME_FORMAT.parse("2016-11-01T12:30:00").getTime();
endTime = ISO_DATETIME_TIME_ZONE_FORMAT.parse("2016-11-01T13:00:00Z").getTime(); endTime = ISO_8601_EXTENDED_DATETIME_FORMAT.parse("2016-11-01T13:00:00").getTime();
timeAfterEndTime = ISO_DATETIME_TIME_ZONE_FORMAT.parse("2016-11-01T13:30:30Z").getTime(); timeAfterEndTime = ISO_8601_EXTENDED_DATETIME_FORMAT.parse("2016-11-01T13:30:30").getTime();
} }
@Test @Test
public void saveEdgeEvent() throws Exception { public void saveEdgeEvent() throws Exception {
EdgeId edgeId = new EdgeId(Uuids.timeBased()); EdgeId edgeId = new EdgeId(Uuids.timeBased());
DeviceId deviceId = new DeviceId(Uuids.timeBased()); DeviceId deviceId = new DeviceId(Uuids.timeBased());
TenantId tenantId = new TenantId(Uuids.timeBased()); EdgeEvent edgeEvent = generateEdgeEvent(tenantId, edgeId, deviceId);
EdgeEvent edgeEvent = generateEdgeEvent(tenantId, edgeId, deviceId, EdgeEventActionType.ADDED);
edgeEventService.saveAsync(edgeEvent).get(); edgeEventService.saveAsync(edgeEvent).get();
PageData<EdgeEvent> edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, 0L, null, new TimePageLink(1)); PageData<EdgeEvent> edgeEvents = edgeEventService.findEdgeEvents(tenantId, edgeId, 0L, null, new TimePageLink(1));
@ -81,9 +80,11 @@ public class EdgeEventServiceTest extends AbstractServiceTest {
Assert.assertEquals(saved.getType(), edgeEvent.getType()); Assert.assertEquals(saved.getType(), edgeEvent.getType());
Assert.assertEquals(saved.getAction(), edgeEvent.getAction()); Assert.assertEquals(saved.getAction(), edgeEvent.getAction());
Assert.assertEquals(saved.getBody(), edgeEvent.getBody()); Assert.assertEquals(saved.getBody(), edgeEvent.getBody());
edgeEventService.cleanupEvents(1);
} }
protected EdgeEvent generateEdgeEvent(TenantId tenantId, EdgeId edgeId, EntityId entityId, EdgeEventActionType edgeEventAction) throws IOException { protected EdgeEvent generateEdgeEvent(TenantId tenantId, EdgeId edgeId, EntityId entityId) throws IOException {
if (tenantId == null) { if (tenantId == null) {
tenantId = TenantId.fromUUID(Uuids.timeBased()); tenantId = TenantId.fromUUID(Uuids.timeBased());
} }
@ -92,7 +93,7 @@ public class EdgeEventServiceTest extends AbstractServiceTest {
edgeEvent.setEdgeId(edgeId); edgeEvent.setEdgeId(edgeId);
edgeEvent.setEntityId(entityId.getId()); edgeEvent.setEntityId(entityId.getId());
edgeEvent.setType(EdgeEventType.DEVICE); edgeEvent.setType(EdgeEventType.DEVICE);
edgeEvent.setAction(edgeEventAction); edgeEvent.setAction(EdgeEventActionType.ADDED);
edgeEvent.setBody(readFromResource("TestJsonData.json")); edgeEvent.setBody(readFromResource("TestJsonData.json"));
return edgeEvent; return edgeEvent;
} }
@ -101,7 +102,6 @@ public class EdgeEventServiceTest extends AbstractServiceTest {
public void findEdgeEventsByTimeDescOrder() throws Exception { public void findEdgeEventsByTimeDescOrder() throws Exception {
EdgeId edgeId = new EdgeId(Uuids.timeBased()); EdgeId edgeId = new EdgeId(Uuids.timeBased());
DeviceId deviceId = new DeviceId(Uuids.timeBased()); DeviceId deviceId = new DeviceId(Uuids.timeBased());
TenantId tenantId = TenantId.fromUUID(Uuids.timeBased());
List<ListenableFuture<Void>> futures = new ArrayList<>(); List<ListenableFuture<Void>> futures = new ArrayList<>();
futures.add(saveEdgeEventWithProvidedTime(timeBeforeStartTime, edgeId, deviceId, tenantId)); futures.add(saveEdgeEventWithProvidedTime(timeBeforeStartTime, edgeId, deviceId, tenantId));
@ -133,7 +133,7 @@ public class EdgeEventServiceTest extends AbstractServiceTest {
} }
private ListenableFuture<Void> saveEdgeEventWithProvidedTime(long time, EdgeId edgeId, EntityId entityId, TenantId tenantId) throws Exception { private ListenableFuture<Void> saveEdgeEventWithProvidedTime(long time, EdgeId edgeId, EntityId entityId, TenantId tenantId) throws Exception {
EdgeEvent edgeEvent = generateEdgeEvent(tenantId, edgeId, entityId, EdgeEventActionType.ADDED); EdgeEvent edgeEvent = generateEdgeEvent(tenantId, edgeId, entityId);
edgeEvent.setId(new EdgeEventId(Uuids.startOf(time))); edgeEvent.setId(new EdgeEventId(Uuids.startOf(time)));
return edgeEventService.saveAsync(edgeEvent); return edgeEventService.saveAsync(edgeEvent);
} }

38
dao/src/test/java/org/thingsboard/server/dao/service/timeseries/BaseTimeseriesServiceTest.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
@ -55,7 +56,9 @@ import java.util.concurrent.TimeoutException;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
@ -65,12 +68,12 @@ import static org.junit.Assert.assertNotNull;
public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest { public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
@Autowired @Autowired
TimeseriesService tsService; protected TimeseriesService tsService;
@Autowired @Autowired
EntityViewService entityViewService; EntityViewService entityViewService;
static final int MAX_TIMEOUT = 30; protected static final int MAX_TIMEOUT = 30;
private static final String STRING_KEY = "stringKey"; private static final String STRING_KEY = "stringKey";
private static final String LONG_KEY = "longKey"; private static final String LONG_KEY = "longKey";
@ -85,7 +88,7 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
KvEntry doubleKvEntry = new DoubleDataEntry(DOUBLE_KEY, Double.MAX_VALUE); KvEntry doubleKvEntry = new DoubleDataEntry(DOUBLE_KEY, Double.MAX_VALUE);
KvEntry booleanKvEntry = new BooleanDataEntry(BOOLEAN_KEY, Boolean.TRUE); KvEntry booleanKvEntry = new BooleanDataEntry(BOOLEAN_KEY, Boolean.TRUE);
private TenantId tenantId; protected TenantId tenantId;
@Before @Before
public void before() { public void before() {
@ -674,6 +677,32 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
assertEquals(3, list.size()); assertEquals(3, list.size());
} }
@Test
public void shouldSaveEntryOfEachType() throws Exception {
BasicTsKvEntry booleanEntry = new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(1), new BooleanDataEntry("test", true));
BasicTsKvEntry stringEntry = new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(2), new StringDataEntry("test", "text"));
BasicTsKvEntry longEntry = new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(3), new LongDataEntry("test", 15L));
BasicTsKvEntry doubleEntry = new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(4), new DoubleDataEntry("test", 10.5));
BasicTsKvEntry jsonEntry = new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(5), new JsonDataEntry("test", "{\"test\":\"testValue\"}"));
List<TsKvEntry> timeseries = List.of(booleanEntry, stringEntry, longEntry, doubleEntry, jsonEntry);
DeviceId deviceId = new DeviceId(Uuids.timeBased());
for (TsKvEntry tsKvEntry : timeseries) {
save(tenantId, deviceId, tsKvEntry);
}
List<TsKvEntry> listUntil3Minutes = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery("test", 0L,
TimeUnit.MINUTES.toMillis(3), 1000, 10, Aggregation.NONE))).get(MAX_TIMEOUT, TimeUnit.SECONDS);
assertEquals(2, listUntil3Minutes.size());
assertThat(listUntil3Minutes).containsOnlyOnceElementsOf(List.of(
booleanEntry, stringEntry));
List<TsKvEntry> fullList = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery("test", 0L,
TimeUnit.MINUTES.toMillis(6), 1000, 10, Aggregation.NONE))).get(MAX_TIMEOUT, TimeUnit.SECONDS);
assertEquals(5, fullList.size());
assertThat(fullList).containsOnlyOnceElementsOf(timeseries);
}
private TsKvEntry save(DeviceId deviceId, long ts, long value) throws Exception { private TsKvEntry save(DeviceId deviceId, long ts, long value) throws Exception {
TsKvEntry entry = new BasicTsKvEntry(ts, new LongDataEntry(LONG_KEY, value)); TsKvEntry entry = new BasicTsKvEntry(ts, new LongDataEntry(LONG_KEY, value));
tsService.save(tenantId, deviceId, entry).get(MAX_TIMEOUT, TimeUnit.SECONDS); tsService.save(tenantId, deviceId, entry).get(MAX_TIMEOUT, TimeUnit.SECONDS);
@ -692,6 +721,9 @@ public abstract class BaseTimeseriesServiceTest extends AbstractServiceTest {
return entry; return entry;
} }
private void save(TenantId tenantId, DeviceId deviceId, TsKvEntry tsKvEntry) throws Exception {
tsService.save(tenantId, deviceId, tsKvEntry).get(MAX_TIMEOUT, TimeUnit.SECONDS);
}
private void saveEntries(DeviceId deviceId, long ts) throws ExecutionException, InterruptedException, TimeoutException { private void saveEntries(DeviceId deviceId, long ts) throws ExecutionException, InterruptedException, TimeoutException {
tsService.save(tenantId, deviceId, toTsEntry(ts, stringKvEntry)).get(MAX_TIMEOUT, TimeUnit.SECONDS); tsService.save(tenantId, deviceId, toTsEntry(ts, stringKvEntry)).get(MAX_TIMEOUT, TimeUnit.SECONDS);

72
dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceNoSqlSetNullEnabledTest.java

@ -0,0 +1,72 @@
/**
* 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.dao.service.timeseries.nosql;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import org.junit.Test;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.service.DaoNoSqlTest;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@DaoNoSqlTest
@TestPropertySource(properties = {
"cassandra.query.set_null_values_enabled=true",
})
public class TimeseriesServiceNoSqlSetNullEnabledTest extends TimeseriesServiceNoSqlTest {
@Override
@Test
public void testNullValuesOfNoneTargetColumn() throws ExecutionException, InterruptedException, TimeoutException {
long ts = TimeUnit.MINUTES.toMillis(1);
TsKvEntry longEntry = new BasicTsKvEntry(ts, new LongDataEntry("temp", 0L));
double doubleValue = 20.6;
TsKvEntry doubleEntry = new BasicTsKvEntry(ts, new DoubleDataEntry("temp", doubleValue));
DeviceId deviceId = new DeviceId(Uuids.timeBased());
tsService.save(tenantId, deviceId, longEntry).get(MAX_TIMEOUT, TimeUnit.SECONDS);
tsService.save(tenantId, deviceId, doubleEntry).get(MAX_TIMEOUT, TimeUnit.SECONDS);
List<TsKvEntry> listWithoutAgg = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery("temp", 0L,
ts + 1 , 1000, 3, Aggregation.NONE))).get(MAX_TIMEOUT, TimeUnit.SECONDS);
assertEquals(1, listWithoutAgg.size());
assertFalse(listWithoutAgg.get(0).getLongValue().isPresent());
assertTrue(listWithoutAgg.get(0).getDoubleValue().isPresent());
assertThat(listWithoutAgg.get(0).getDoubleValue().get()).isEqualTo(doubleValue);
// long value should be set to null after second insert, so avg = doubleValue
List<TsKvEntry> listWithAgg = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery("temp", 0L,
ts + 1 , 1000, 3, Aggregation.AVG))).get(MAX_TIMEOUT, TimeUnit.SECONDS);
assertEquals(1, listWithAgg.size());
assertTrue(listWithAgg.get(0).getDoubleValue().isPresent());
assertThat(listWithAgg.get(0).getDoubleValue().get()).isEqualTo(doubleValue);
}
}

74
dao/src/test/java/org/thingsboard/server/dao/service/timeseries/nosql/TimeseriesServiceNoSqlTest.java

@ -15,9 +15,83 @@
*/ */
package org.thingsboard.server.dao.service.timeseries.nosql; package org.thingsboard.server.dao.service.timeseries.nosql;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import org.junit.Test;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.kv.Aggregation;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.service.DaoNoSqlTest; import org.thingsboard.server.dao.service.DaoNoSqlTest;
import org.thingsboard.server.dao.service.timeseries.BaseTimeseriesServiceTest; import org.thingsboard.server.dao.service.timeseries.BaseTimeseriesServiceTest;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@DaoNoSqlTest @DaoNoSqlTest
public class TimeseriesServiceNoSqlTest extends BaseTimeseriesServiceTest { public class TimeseriesServiceNoSqlTest extends BaseTimeseriesServiceTest {
@Test
public void shouldSaveEntryOfEachTypeWithTtl() throws ExecutionException, InterruptedException, TimeoutException {
long ttlInSec = TimeUnit.SECONDS.toSeconds(3);
List<TsKvEntry> timeseries = List.of(
new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(1), new BooleanDataEntry("test", true)),
new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(2), new StringDataEntry("test", "text")),
new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(3), new LongDataEntry("test", 15L)),
new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(4), new DoubleDataEntry("test", 10.5)),
new BasicTsKvEntry(TimeUnit.MINUTES.toMillis(5), new JsonDataEntry("test", "{\"test\":\"testValue\"}")));
DeviceId deviceId = new DeviceId(Uuids.timeBased());
tsService.save(tenantId, deviceId, timeseries, ttlInSec);
List<TsKvEntry> fullList = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery("test", 0L,
TimeUnit.MINUTES.toMillis(6), 1000, 10, Aggregation.NONE))).get(MAX_TIMEOUT, TimeUnit.SECONDS);
assertEquals(5, fullList.size());
// check entries after ttl
Thread.sleep(TimeUnit.SECONDS.toMillis(ttlInSec + 1));
List<TsKvEntry> listAfterTtl = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery("test", 0L,
TimeUnit.MINUTES.toMillis(6), 1000, 10, Aggregation.NONE))).get(MAX_TIMEOUT, TimeUnit.SECONDS);
assertEquals(0, listAfterTtl.size());
}
@Test
public void testNullValuesOfNoneTargetColumn() throws ExecutionException, InterruptedException, TimeoutException {
long ts = TimeUnit.MINUTES.toMillis(1);
long longValue = 10L;
TsKvEntry longEntry = new BasicTsKvEntry(ts, new LongDataEntry("temp", longValue));
double doubleValue = 20.6;
TsKvEntry doubleEntry = new BasicTsKvEntry(ts, new DoubleDataEntry("temp", doubleValue));
DeviceId deviceId = new DeviceId(Uuids.timeBased());
tsService.save(tenantId, deviceId, longEntry).get(MAX_TIMEOUT, TimeUnit.SECONDS);
tsService.save(tenantId, deviceId, doubleEntry).get(MAX_TIMEOUT, TimeUnit.SECONDS);
List<TsKvEntry> listWithoutAgg = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery("temp", 0L,
ts + 1 , 1000, 3, Aggregation.NONE))).get(MAX_TIMEOUT, TimeUnit.SECONDS);
assertEquals(1, listWithoutAgg.size());
assertTrue(listWithoutAgg.get(0).getLongValue().isPresent());
assertFalse(listWithoutAgg.get(0).getDoubleValue().isPresent());
assertThat(listWithoutAgg.get(0).getLongValue().get()).isEqualTo(longValue);
// long value should not be reset to null, so avg = (doubleValue + longValue)/ 2
List<TsKvEntry> listWithAgg = tsService.findAll(tenantId, deviceId, Collections.singletonList(new BaseReadTsKvQuery("temp", 0L,
ts + 1, 200000, 3, Aggregation.AVG))).get(MAX_TIMEOUT, TimeUnit.SECONDS);
assertEquals(1, listWithAgg.size());
assertTrue(listWithAgg.get(0).getDoubleValue().isPresent());
double expectedValue = (doubleValue + longValue)/ 2;
assertThat(listWithAgg.get(0).getDoubleValue().get()).isEqualTo(expectedValue);
}
} }

8
transport/snmp/src/main/resources/tb-snmp-transport.yml

@ -139,14 +139,18 @@ transport:
bind_port: "${SNMP_BIND_PORT:1620}" bind_port: "${SNMP_BIND_PORT:1620}"
response_processing: response_processing:
# parallelism level for executor (workStealingPool) that is responsible for handling responses from SNMP devices # parallelism level for executor (workStealingPool) that is responsible for handling responses from SNMP devices
parallelism_level: "${SNMP_RESPONSE_PROCESSING_PARALLELISM_LEVEL:20}" parallelism_level: "${SNMP_RESPONSE_PROCESSING_PARALLELISM_LEVEL:4}"
# to configure SNMP to work over UDP or TCP # to configure SNMP to work over UDP or TCP
underlying_protocol: "${SNMP_UNDERLYING_PROTOCOL:udp}" underlying_protocol: "${SNMP_UNDERLYING_PROTOCOL:udp}"
# Batch size to request OID mappings from the device (useful when the device profile has multiple hundreds of OID mappings) # Maximum size of a PDU (amount of OID mappings in a single SNMP request). The request will be split into multiple PDUs if mappings amount exceeds this number
max_request_oids: "${SNMP_MAX_REQUEST_OIDS:100}" max_request_oids: "${SNMP_MAX_REQUEST_OIDS:100}"
# Delay after sending each request chunk (in case the request was split into multiple PDUs due to max_request_oids)
request_chunk_delay_ms: "${SNMP_REQUEST_CHUNK_DELAY_MS:100}"
response: response:
# To ignore SNMP response values that do not match the data type of the configured OID mapping (by default false - will throw an error if any value of the response not match configured data types) # To ignore SNMP response values that do not match the data type of the configured OID mapping (by default false - will throw an error if any value of the response not match configured data types)
ignore_type_cast_errors: "${SNMP_RESPONSE_IGNORE_TYPE_CAST_ERRORS:false}" ignore_type_cast_errors: "${SNMP_RESPONSE_IGNORE_TYPE_CAST_ERRORS:false}"
# Thread pool size for scheduler that executes device querying tasks
scheduler_thread_pool_size: "${SNMP_SCHEDULER_THREAD_POOL_SIZE:4}"
sessions: sessions:
# Session inactivity timeout is a global configuration parameter that defines how long the device transport session will be opened after the last message arrives from the device. # Session inactivity timeout is a global configuration parameter that defines how long the device transport session will be opened after the last message arrives from the device.
# The parameter value is in milliseconds. # The parameter value is in milliseconds.

18
ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html

@ -523,7 +523,7 @@
</mat-panel-description> </mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<ng-template matExpansionPanelContent> <ng-template matExpansionPanelContent>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px"> <div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<tb-rate-limits fxFlex formControlName="transportTenantTelemetryDataPointsRateLimit" <tb-rate-limits fxFlex formControlName="transportTenantTelemetryDataPointsRateLimit"
[type]="rateLimitsType.TENANT_TELEMETRY_DATA_POINTS"> [type]="rateLimitsType.TENANT_TELEMETRY_DATA_POINTS">
</tb-rate-limits> </tb-rate-limits>
@ -531,7 +531,7 @@
[type]="rateLimitsType.DEVICE_TELEMETRY_DATA_POINTS"> [type]="rateLimitsType.DEVICE_TELEMETRY_DATA_POINTS">
</tb-rate-limits> </tb-rate-limits>
</div> </div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px"> <div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<tb-rate-limits fxFlex formControlName="tenantServerRestLimitsConfiguration" <tb-rate-limits fxFlex formControlName="tenantServerRestLimitsConfiguration"
[type]="rateLimitsType.TENANT_SERVER_REST_LIMITS_CONFIGURATION"> [type]="rateLimitsType.TENANT_SERVER_REST_LIMITS_CONFIGURATION">
</tb-rate-limits> </tb-rate-limits>
@ -539,7 +539,7 @@
[type]="rateLimitsType.CUSTOMER_SERVER_REST_LIMITS_CONFIGURATION"> [type]="rateLimitsType.CUSTOMER_SERVER_REST_LIMITS_CONFIGURATION">
</tb-rate-limits> </tb-rate-limits>
</div> </div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px"> <div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<tb-rate-limits fxFlex formControlName="tenantEntityExportRateLimit" <tb-rate-limits fxFlex formControlName="tenantEntityExportRateLimit"
[type]="rateLimitsType.TENANT_ENTITY_EXPORT_RATE_LIMIT"> [type]="rateLimitsType.TENANT_ENTITY_EXPORT_RATE_LIMIT">
</tb-rate-limits> </tb-rate-limits>
@ -547,7 +547,7 @@
[type]="rateLimitsType.TENANT_ENTITY_IMPORT_RATE_LIMIT"> [type]="rateLimitsType.TENANT_ENTITY_IMPORT_RATE_LIMIT">
</tb-rate-limits> </tb-rate-limits>
</div> </div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px"> <div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<tb-rate-limits fxFlex formControlName="wsUpdatesPerSessionRateLimit" <tb-rate-limits fxFlex formControlName="wsUpdatesPerSessionRateLimit"
[type]="rateLimitsType.WS_UPDATE_PER_SESSION_RATE_LIMIT"> [type]="rateLimitsType.WS_UPDATE_PER_SESSION_RATE_LIMIT">
</tb-rate-limits> </tb-rate-limits>
@ -555,7 +555,7 @@
[type]="rateLimitsType.CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION"> [type]="rateLimitsType.CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION">
</tb-rate-limits> </tb-rate-limits>
</div> </div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px"> <div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<tb-rate-limits fxFlex="50" formControlName="tenantNotificationRequestsRateLimit" <tb-rate-limits fxFlex="50" formControlName="tenantNotificationRequestsRateLimit"
[type]="rateLimitsType.TENANT_NOTIFICATION_REQUEST_RATE_LIMIT"> [type]="rateLimitsType.TENANT_NOTIFICATION_REQUEST_RATE_LIMIT">
</tb-rate-limits> </tb-rate-limits>
@ -563,6 +563,14 @@
[type]="rateLimitsType.TENANT_NOTIFICATION_REQUESTS_PER_RULE_RATE_LIMIT"> [type]="rateLimitsType.TENANT_NOTIFICATION_REQUESTS_PER_RULE_RATE_LIMIT">
</tb-rate-limits> </tb-rate-limits>
</div> </div>
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="16px">
<tb-rate-limits fxFlex="50" formControlName="edgeEventRateLimits"
[type]="rateLimitsType.EDGE_EVENTS_RATE_LIMIT">
</tb-rate-limits>
<tb-rate-limits fxFlex="50" formControlName="edgeEventRateLimitsPerEdge"
[type]="rateLimitsType.EDGE_EVENTS_PER_EDGE_RATE_LIMIT">
</tb-rate-limits>
</div>
</ng-template> </ng-template>
</mat-expansion-panel> </mat-expansion-panel>
</fieldset> </fieldset>

4
ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts

@ -106,7 +106,9 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA
maxWsSubscriptionsPerRegularUser: [null, [Validators.min(0)]], maxWsSubscriptionsPerRegularUser: [null, [Validators.min(0)]],
maxWsSubscriptionsPerPublicUser: [null, [Validators.min(0)]], maxWsSubscriptionsPerPublicUser: [null, [Validators.min(0)]],
wsUpdatesPerSessionRateLimit: [null, []], wsUpdatesPerSessionRateLimit: [null, []],
cassandraQueryTenantRateLimitsConfiguration: [null, []] cassandraQueryTenantRateLimitsConfiguration: [null, []],
edgeEventRateLimits: [null, []],
edgeEventRateLimitsPerEdge: [null, []]
}); });
this.defaultTenantProfileConfigurationFormGroup.get('smsEnabled').valueChanges.pipe( this.defaultTenantProfileConfigurationFormGroup.get('smsEnabled').valueChanges.pipe(

8
ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts

@ -35,7 +35,9 @@ export enum RateLimitsType {
TENANT_ENTITY_EXPORT_RATE_LIMIT = 'TENANT_ENTITY_EXPORT_RATE_LIMIT', TENANT_ENTITY_EXPORT_RATE_LIMIT = 'TENANT_ENTITY_EXPORT_RATE_LIMIT',
TENANT_ENTITY_IMPORT_RATE_LIMIT = 'TENANT_ENTITY_IMPORT_RATE_LIMIT', TENANT_ENTITY_IMPORT_RATE_LIMIT = 'TENANT_ENTITY_IMPORT_RATE_LIMIT',
TENANT_NOTIFICATION_REQUEST_RATE_LIMIT = 'TENANT_NOTIFICATION_REQUEST_RATE_LIMIT', TENANT_NOTIFICATION_REQUEST_RATE_LIMIT = 'TENANT_NOTIFICATION_REQUEST_RATE_LIMIT',
TENANT_NOTIFICATION_REQUESTS_PER_RULE_RATE_LIMIT = 'TENANT_NOTIFICATION_REQUESTS_PER_RULE_RATE_LIMIT' TENANT_NOTIFICATION_REQUESTS_PER_RULE_RATE_LIMIT = 'TENANT_NOTIFICATION_REQUESTS_PER_RULE_RATE_LIMIT',
EDGE_EVENTS_RATE_LIMIT = 'EDGE_EVENTS_RATE_LIMIT',
EDGE_EVENTS_PER_EDGE_RATE_LIMIT = 'EDGE_EVENTS_PER_EDGE_RATE_LIMIT'
} }
export const rateLimitsLabelTranslationMap = new Map<RateLimitsType, string>( export const rateLimitsLabelTranslationMap = new Map<RateLimitsType, string>(
@ -54,6 +56,8 @@ export const rateLimitsLabelTranslationMap = new Map<RateLimitsType, string>(
[RateLimitsType.TENANT_ENTITY_IMPORT_RATE_LIMIT, 'tenant-profile.tenant-entity-import-rate-limit'], [RateLimitsType.TENANT_ENTITY_IMPORT_RATE_LIMIT, 'tenant-profile.tenant-entity-import-rate-limit'],
[RateLimitsType.TENANT_NOTIFICATION_REQUEST_RATE_LIMIT, 'tenant-profile.tenant-notification-request-rate-limit'], [RateLimitsType.TENANT_NOTIFICATION_REQUEST_RATE_LIMIT, 'tenant-profile.tenant-notification-request-rate-limit'],
[RateLimitsType.TENANT_NOTIFICATION_REQUESTS_PER_RULE_RATE_LIMIT, 'tenant-profile.tenant-notification-requests-per-rule-rate-limit'], [RateLimitsType.TENANT_NOTIFICATION_REQUESTS_PER_RULE_RATE_LIMIT, 'tenant-profile.tenant-notification-requests-per-rule-rate-limit'],
[RateLimitsType.EDGE_EVENTS_RATE_LIMIT, 'tenant-profile.rate-limits.edge-events-rate-limit'],
[RateLimitsType.EDGE_EVENTS_PER_EDGE_RATE_LIMIT, 'tenant-profile.rate-limits.edge-events-per-edge-rate-limit'],
] ]
); );
@ -73,6 +77,8 @@ export const rateLimitsDialogTitleTranslationMap = new Map<RateLimitsType, strin
[RateLimitsType.TENANT_ENTITY_IMPORT_RATE_LIMIT, 'tenant-profile.rate-limits.edit-tenant-entity-import-rate-limit-title'], [RateLimitsType.TENANT_ENTITY_IMPORT_RATE_LIMIT, 'tenant-profile.rate-limits.edit-tenant-entity-import-rate-limit-title'],
[RateLimitsType.TENANT_NOTIFICATION_REQUEST_RATE_LIMIT, 'tenant-profile.rate-limits.edit-tenant-notification-request-rate-limit-title'], [RateLimitsType.TENANT_NOTIFICATION_REQUEST_RATE_LIMIT, 'tenant-profile.rate-limits.edit-tenant-notification-request-rate-limit-title'],
[RateLimitsType.TENANT_NOTIFICATION_REQUESTS_PER_RULE_RATE_LIMIT, 'tenant-profile.rate-limits.edit-tenant-notification-requests-per-rule-rate-limit-title'], [RateLimitsType.TENANT_NOTIFICATION_REQUESTS_PER_RULE_RATE_LIMIT, 'tenant-profile.rate-limits.edit-tenant-notification-requests-per-rule-rate-limit-title'],
[RateLimitsType.EDGE_EVENTS_RATE_LIMIT, 'tenant-profile.rate-limits.edit-edge-events-rate-limit'],
[RateLimitsType.EDGE_EVENTS_PER_EDGE_RATE_LIMIT, 'tenant-profile.rate-limits.edit-edge-events-per-edge-rate-limit'],
] ]
); );

61
ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.html

@ -408,6 +408,67 @@
</form> </form>
</mat-step> </mat-step>
<mat-step [stepControl]="edgeConnectionTemplateForm"
*ngIf="ruleNotificationForm.get('triggerType').value === triggerType.EDGE_CONNECTION">
<ng-template matStepLabel>{{ 'notification.edge-trigger-settings' | translate }}</ng-template>
<form [formGroup]="edgeConnectionTemplateForm">
<section formGroupName="triggerConfig">
<fieldset class="fields-group tb-margin-before-field">
<legend translate>notification.filter</legend>
<tb-entity-list
formControlName="edges"
subscriptSizing="dynamic"
labelText="{{'edge.edge-instances' | translate}}"
placeholderText="{{ 'edge.edge-instances' | translate }}"
hint="{{ 'notification.edge-list-rule-hint' | translate }}"
[entityType]="entityType.EDGE">
</tb-entity-list>
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-label translate>notification.notify-on</mat-label>
<mat-select formControlName="notifyOn" multiple
placeholder="{{ !edgeConnectionTemplateForm.get('triggerConfig.notifyOn').value?.length ? ('event.all-events' | translate) : '' }}">
<mat-option *ngFor="let edgeEvent of edgeConnectionEvents" [value]="edgeEvent">
{{ edgeConnectionEventTranslationMap.get(edgeEvent) | translate }}
</mat-option>
</mat-select>
</mat-form-field>
</fieldset>
</section>
</form>
<form [formGroup]="ruleNotificationForm">
<section formGroupName="additionalConfig">
<mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label>
<input matInput formControlName="description">
</mat-form-field>
</section>
</form>
</mat-step>
<mat-step [stepControl]="edgeCommunicationFailureTemplateForm"
*ngIf="ruleNotificationForm.get('triggerType').value === triggerType.EDGE_COMMUNICATION_FAILURE">
<ng-template matStepLabel>{{ 'notification.edge-trigger-settings' | translate }}</ng-template>
<form [formGroup]="edgeCommunicationFailureTemplateForm">
<section formGroupName="triggerConfig">
<fieldset class="fields-group tb-margin-before-field">
<legend translate>notification.filter</legend>
<tb-entity-list labelText="{{'edge.edge-instances' | translate}}"
[entityType]="entityType.EDGE"
formControlName="edges">
</tb-entity-list>
</fieldset>
</section>
</form>
<form [formGroup]="ruleNotificationForm">
<section formGroupName="additionalConfig">
<mat-form-field class="mat-block">
<mat-label translate>notification.description</mat-label>
<input matInput formControlName="description">
</mat-form-field>
</section>
</form>
</mat-step>
<mat-step *ngIf="ruleNotificationForm.get('triggerType').value === triggerType.ENTITIES_LIMIT" <mat-step *ngIf="ruleNotificationForm.get('triggerType').value === triggerType.ENTITIES_LIMIT"
[stepControl]="entitiesLimitTemplateForm"> [stepControl]="entitiesLimitTemplateForm">
<ng-template matStepLabel>{{ 'notification.entities-limit-trigger-settings' | translate }}</ng-template> <ng-template matStepLabel>{{ 'notification.entities-limit-trigger-settings' | translate }}</ng-template>

23
ui-ngx/src/app/modules/home/pages/notification/rule/rule-notification-dialog.component.ts

@ -68,6 +68,7 @@ import {
} from '@shared/models/api-usage.models'; } from '@shared/models/api-usage.models';
import { LimitedApi, LimitedApiTranslationMap } from '@shared/models/limited-api.models'; import { LimitedApi, LimitedApiTranslationMap } from '@shared/models/limited-api.models';
import { StringItemsOption } from '@shared/components/string-items-list.component'; import { StringItemsOption } from '@shared/components/string-items-list.component';
import { EdgeConnectionEvent, EdgeConnectionEventTranslationMap } from '@shared/models/edge.models';
export interface RuleNotificationDialogData { export interface RuleNotificationDialogData {
rule?: NotificationRule; rule?: NotificationRule;
@ -98,6 +99,8 @@ export class RuleNotificationDialogComponent extends
apiUsageLimitTemplateForm: FormGroup; apiUsageLimitTemplateForm: FormGroup;
newPlatformVersionTemplateForm: FormGroup; newPlatformVersionTemplateForm: FormGroup;
rateLimitsTemplateForm: FormGroup; rateLimitsTemplateForm: FormGroup;
edgeCommunicationFailureTemplateForm: FormGroup;
edgeConnectionTemplateForm: FormGroup;
triggerType = TriggerType; triggerType = TriggerType;
triggerTypes: TriggerType[]; triggerTypes: TriggerType[];
@ -132,6 +135,9 @@ export class RuleNotificationDialogComponent extends
apiFeatures: ApiFeature[] = Object.values(ApiFeature); apiFeatures: ApiFeature[] = Object.values(ApiFeature);
apiFeatureTranslationMap = ApiFeatureTranslationMap; apiFeatureTranslationMap = ApiFeatureTranslationMap;
edgeConnectionEvents: EdgeConnectionEvent[] = Object.values(EdgeConnectionEvent);
edgeConnectionEventTranslationMap = EdgeConnectionEventTranslationMap;
limitedApis: StringItemsOption[]; limitedApis: StringItemsOption[];
entityType = EntityType; entityType = EntityType;
@ -221,6 +227,19 @@ export class RuleNotificationDialogComponent extends
} }
}); });
this.edgeConnectionTemplateForm = this.fb.group({
triggerConfig: this.fb.group({
edges: [null],
notifyOn: [null]
})
});
this.edgeCommunicationFailureTemplateForm = this.fb.group({
triggerConfig: this.fb.group({
edges: [null]
})
});
this.alarmTemplateForm = this.fb.group({ this.alarmTemplateForm = this.fb.group({
triggerConfig: this.fb.group({ triggerConfig: this.fb.group({
alarmTypes: [null], alarmTypes: [null],
@ -328,7 +347,9 @@ export class RuleNotificationDialogComponent extends
[TriggerType.ENTITIES_LIMIT, this.entitiesLimitTemplateForm], [TriggerType.ENTITIES_LIMIT, this.entitiesLimitTemplateForm],
[TriggerType.API_USAGE_LIMIT, this.apiUsageLimitTemplateForm], [TriggerType.API_USAGE_LIMIT, this.apiUsageLimitTemplateForm],
[TriggerType.NEW_PLATFORM_VERSION, this.newPlatformVersionTemplateForm], [TriggerType.NEW_PLATFORM_VERSION, this.newPlatformVersionTemplateForm],
[TriggerType.RATE_LIMITS, this.rateLimitsTemplateForm] [TriggerType.RATE_LIMITS, this.rateLimitsTemplateForm],
[TriggerType.EDGE_COMMUNICATION_FAILURE, this.edgeCommunicationFailureTemplateForm],
[TriggerType.EDGE_CONNECTION, this.edgeConnectionTemplateForm]
]); ]);
if (data.isAdd || data.isCopy) { if (data.isAdd || data.isCopy) {

12
ui-ngx/src/app/shared/models/edge.models.ts

@ -190,3 +190,15 @@ export enum EdgeInstructionsMethod {
} }
export const edgeVersionAttributeKey = 'edgeVersion'; export const edgeVersionAttributeKey = 'edgeVersion';
export enum EdgeConnectionEvent {
CONNECTED= 'CONNECTED',
DISCONNECTED = 'DISCONNECTED'
}
export const EdgeConnectionEventTranslationMap = new Map<EdgeConnectionEvent, string>(
[
[EdgeConnectionEvent.CONNECTED, 'edge.connected'],
[EdgeConnectionEvent.DISCONNECTED, 'edge.disconnected']
]
);

8
ui-ngx/src/app/shared/models/limited-api.models.ts

@ -24,7 +24,9 @@ export enum LimitedApi {
WS_UPDATES_PER_SESSION = 'WS_UPDATES_PER_SESSION', WS_UPDATES_PER_SESSION = 'WS_UPDATES_PER_SESSION',
CASSANDRA_QUERIES = 'CASSANDRA_QUERIES', CASSANDRA_QUERIES = 'CASSANDRA_QUERIES',
TRANSPORT_MESSAGES_PER_TENANT = 'TRANSPORT_MESSAGES_PER_TENANT', TRANSPORT_MESSAGES_PER_TENANT = 'TRANSPORT_MESSAGES_PER_TENANT',
TRANSPORT_MESSAGES_PER_DEVICE = 'TRANSPORT_MESSAGES_PER_DEVICE' TRANSPORT_MESSAGES_PER_DEVICE = 'TRANSPORT_MESSAGES_PER_DEVICE',
EDGE_EVENTS = 'EDGE_EVENTS',
EDGE_EVENTS_PER_EDGE = 'EDGE_EVENTS_PER_EDGE'
} }
export const LimitedApiTranslationMap = new Map<LimitedApi, string>( export const LimitedApiTranslationMap = new Map<LimitedApi, string>(
@ -38,6 +40,8 @@ export const LimitedApiTranslationMap = new Map<LimitedApi, string>(
[LimitedApi.WS_UPDATES_PER_SESSION, 'api-limit.ws-updates-per-session'], [LimitedApi.WS_UPDATES_PER_SESSION, 'api-limit.ws-updates-per-session'],
[LimitedApi.CASSANDRA_QUERIES, 'api-limit.cassandra-queries'], [LimitedApi.CASSANDRA_QUERIES, 'api-limit.cassandra-queries'],
[LimitedApi.TRANSPORT_MESSAGES_PER_TENANT, 'api-limit.transport-messages'], [LimitedApi.TRANSPORT_MESSAGES_PER_TENANT, 'api-limit.transport-messages'],
[LimitedApi.TRANSPORT_MESSAGES_PER_DEVICE, 'api-limit.transport-messages-per-device'] [LimitedApi.TRANSPORT_MESSAGES_PER_DEVICE, 'api-limit.transport-messages-per-device'],
[LimitedApi.EDGE_EVENTS, 'api-limit.edge-events'],
[LimitedApi.EDGE_EVENTS_PER_EDGE, 'api-limit.edge-events-per-edge'],
] ]
); );

22
ui-ngx/src/app/shared/models/notification.models.ts

@ -474,7 +474,9 @@ export enum NotificationType {
API_USAGE_LIMIT = 'API_USAGE_LIMIT', API_USAGE_LIMIT = 'API_USAGE_LIMIT',
NEW_PLATFORM_VERSION = 'NEW_PLATFORM_VERSION', NEW_PLATFORM_VERSION = 'NEW_PLATFORM_VERSION',
RULE_NODE = 'RULE_NODE', RULE_NODE = 'RULE_NODE',
RATE_LIMITS = 'RATE_LIMITS' RATE_LIMITS = 'RATE_LIMITS',
EDGE_CONNECTION = 'EDGE_CONNECTION',
EDGE_COMMUNICATION_FAILURE = 'EDGE_COMMUNICATION_FAILURE'
} }
export const NotificationTypeIcons = new Map<NotificationType, string | null>([ export const NotificationTypeIcons = new Map<NotificationType, string | null>([
@ -585,6 +587,18 @@ export const NotificationTemplateTypeTranslateMap = new Map<NotificationType, No
name: 'notification.template-type.rate-limits', name: 'notification.template-type.rate-limits',
helpId: 'notification/rate_limits' helpId: 'notification/rate_limits'
} }
],
[NotificationType.EDGE_CONNECTION,
{
name: 'notification.template-type.edge-connection',
helpId: 'notification/edge_connection'
}
],
[NotificationType.EDGE_COMMUNICATION_FAILURE,
{
name: 'notification.template-type.edge-communication-failure',
helpId: 'notification/edge_communication_failure'
}
] ]
]); ]);
@ -598,7 +612,9 @@ export enum TriggerType {
ENTITIES_LIMIT = 'ENTITIES_LIMIT', ENTITIES_LIMIT = 'ENTITIES_LIMIT',
API_USAGE_LIMIT = 'API_USAGE_LIMIT', API_USAGE_LIMIT = 'API_USAGE_LIMIT',
NEW_PLATFORM_VERSION = 'NEW_PLATFORM_VERSION', NEW_PLATFORM_VERSION = 'NEW_PLATFORM_VERSION',
RATE_LIMITS = 'RATE_LIMITS' RATE_LIMITS = 'RATE_LIMITS',
EDGE_CONNECTION = 'EDGE_CONNECTION',
EDGE_COMMUNICATION_FAILURE = 'EDGE_COMMUNICATION_FAILURE'
} }
export const TriggerTypeTranslationMap = new Map<TriggerType, string>([ export const TriggerTypeTranslationMap = new Map<TriggerType, string>([
@ -612,6 +628,8 @@ export const TriggerTypeTranslationMap = new Map<TriggerType, string>([
[TriggerType.API_USAGE_LIMIT, 'notification.trigger.api-usage-limit'], [TriggerType.API_USAGE_LIMIT, 'notification.trigger.api-usage-limit'],
[TriggerType.NEW_PLATFORM_VERSION, 'notification.trigger.new-platform-version'], [TriggerType.NEW_PLATFORM_VERSION, 'notification.trigger.new-platform-version'],
[TriggerType.RATE_LIMITS, 'notification.trigger.rate-limits'], [TriggerType.RATE_LIMITS, 'notification.trigger.rate-limits'],
[TriggerType.EDGE_CONNECTION, 'notification.trigger.edge-connection'],
[TriggerType.EDGE_COMMUNICATION_FAILURE, 'notification.trigger.edge-communication-failure']
]); ]);
export interface NotificationUserSettings { export interface NotificationUserSettings {

57
ui-ngx/src/assets/help/en_US/notification/edge_communication_failure.md

@ -0,0 +1,57 @@
#### Edge communication failure notification templatization
<div class="divider"></div>
<br/>
Notification subject and message fields support templatization.
The list of available templatization parameters depends on the template type.
See the available types and parameters below:
Available template parameters:
* `edgeId` - the edge id as uuid string;
* `edgeName` - the name of the edge;
* `failureMsg` - the string representation of the failure, occurred on the Edge;
Parameter names must be wrapped using `${...}`. For example: `${edgeName}`.
You may also modify the value of the parameter with one of the suffixes:
* `upperCase`, for example - `${edgeName:upperCase}`
* `lowerCase`, for example - `${edgeName:lowerCase}`
* `capitalize`, for example - `${edgeName:capitalize}`
<div class="divider"></div>
##### Examples
Let's assume the notification about the failing of processing connection to Edge.
The following template:
```text
Edge '${edgeName}' communication failure occurred
{:copy-code}
```
will be transformed to:
```text
Edge 'DatacenterEdge' communication failure occurred
```
<br/>
The following template:
```text
Failure message: '${failureMsg}'
{:copy-code}
```
will be transformed to:
```text
Failure message: 'Failed to process edge connection!'
```
<br>
<br>

44
ui-ngx/src/assets/help/en_US/notification/edge_connection.md

@ -0,0 +1,44 @@
#### Edge connection notification templatization
<div class="divider"></div>
<br/>
Notification subject and message fields support templatization.
The list of available templatization parameters depends on the template type.
See the available types and parameters below:
Available template parameters:
* `edgeId` - the edge id as uuid string;
* `edgeName` - the name of the edge;
* `eventType` - the string representation of the connectivity status: connected or disconnected;
Parameter names must be wrapped using `${...}`. For example: `${edgeName}`.
You may also modify the value of the parameter with one of the suffixes:
* `upperCase`, for example - `${edgeName:upperCase}`
* `lowerCase`, for example - `${edgeName:lowerCase}`
* `capitalize`, for example - `${edgeName:capitalize}`
<div class="divider"></div>
##### Examples
Let's assume the notification about the connecting Edge into the ThingsBoard.
The following template:
```text
Edge '${edgeName}' is now ${eventType}
{:copy-code}
```
will be transformed to:
```text
Edge 'DatacenterEdge' is now connected
```
<br/>
<br>
<br>

2
ui-ngx/src/assets/help/en_US/notification/rate_limits.md

@ -11,7 +11,7 @@ Available template parameters:
* `api` - rate-limited API label; one of: 'REST API requests', 'REST API requests per customer', 'transport messages', * `api` - rate-limited API label; one of: 'REST API requests', 'REST API requests per customer', 'transport messages',
'transport messages per device', 'Cassandra queries', 'WS updates per session', 'notification requests', 'notification requests per rule', 'transport messages per device', 'Cassandra queries', 'WS updates per session', 'notification requests', 'notification requests per rule',
'entity version creation', 'entity version load'; 'entity version creation', 'entity version load', 'Edge events', 'Edge events per edge';
* `limitLevelEntityType` - entity type of the limit level entity, e.g. 'Tenant', 'Device', 'Notification rule', 'Customer', etc.; * `limitLevelEntityType` - entity type of the limit level entity, e.g. 'Tenant', 'Device', 'Notification rule', 'Customer', etc.;
* `limitLevelEntityId` - id of the limit level entity; * `limitLevelEntityId` - id of the limit level entity;
* `limitLevelEntityName` - name of the limit level entity; * `limitLevelEntityName` - name of the limit level entity;

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

@ -856,7 +856,9 @@
"rest-api-requests-per-customer": "REST API requests per customer", "rest-api-requests-per-customer": "REST API requests per customer",
"transport-messages": "Transport messages", "transport-messages": "Transport messages",
"transport-messages-per-device": "Transport messages per device", "transport-messages-per-device": "Transport messages per device",
"ws-updates-per-session": "WS updates per session" "ws-updates-per-session": "WS updates per session",
"edge-events": "Edge events",
"edge-events-per-edge": "Edge events per edge"
}, },
"audit-log": { "audit-log": {
"audit": "Audit", "audit": "Audit",
@ -2037,7 +2039,9 @@
"missing-related-rule-chains-title": "Edge has missing related rule chain(s)", "missing-related-rule-chains-title": "Edge has missing related rule chain(s)",
"missing-related-rule-chains-text": "Assigned to edge rule chain(s) use rule nodes that forward message(s) to rule chain(s) that are not assigned to this edge. <br><br> List of missing rule chain(s): <br> {{missingRuleChains}}", "missing-related-rule-chains-text": "Assigned to edge rule chain(s) use rule nodes that forward message(s) to rule chain(s) that are not assigned to this edge. <br><br> List of missing rule chain(s): <br> {{missingRuleChains}}",
"upgrade-instructions": "Upgrade Instructions", "upgrade-instructions": "Upgrade Instructions",
"widget-datasource-error": "This widget supports only EDGE entity datasource" "widget-datasource-error": "This widget supports only EDGE entity datasource",
"connected": "Connected",
"disconnected": "Disconnected"
}, },
"edge-event": { "edge-event": {
"type-dashboard": "Dashboard", "type-dashboard": "Dashboard",
@ -3288,6 +3292,8 @@
"device-list-rule-hint": "If the field is empty, the trigger will be applied to all devices", "device-list-rule-hint": "If the field is empty, the trigger will be applied to all devices",
"device-profiles-list-rule-hint": "If the field is empty, the trigger will be applied to all device profiles", "device-profiles-list-rule-hint": "If the field is empty, the trigger will be applied to all device profiles",
"disabled": "Disabled", "disabled": "Disabled",
"edge-trigger-settings": "Edge trigger settings",
"edge-list-rule-hint": "If the field is empty, the trigger will be applied to all edge instances",
"edit-notification-recipients-group": "Edit notification recipients group", "edit-notification-recipients-group": "Edit notification recipients group",
"edit-notification-template": "Edit notification template", "edit-notification-template": "Edit notification template",
"edit-rule": "Edit rule", "edit-rule": "Edit rule",
@ -3432,7 +3438,9 @@
"rule-engine-lifecycle-event": "Rule engine lifecycle event", "rule-engine-lifecycle-event": "Rule engine lifecycle event",
"rule-node": "Rule node", "rule-node": "Rule node",
"new-platform-version": "New platform version", "new-platform-version": "New platform version",
"rate-limits": "Exceeded rate limits" "rate-limits": "Exceeded rate limits",
"edge-communication-failure": "Edge communication failure",
"edge-connection": "Edge connection"
}, },
"templates": "Templates", "templates": "Templates",
"notification-templates": "Notifications / Templates", "notification-templates": "Notifications / Templates",
@ -3453,6 +3461,8 @@
"rule-engine-lifecycle-event": "Rule engine lifecycle event", "rule-engine-lifecycle-event": "Rule engine lifecycle event",
"new-platform-version": "New platform version", "new-platform-version": "New platform version",
"rate-limits": "Exceeded rate limits", "rate-limits": "Exceeded rate limits",
"edge-connection": "Edge connection",
"edge-communication-failure": "Edge communication failure",
"trigger": "Trigger", "trigger": "Trigger",
"trigger-required": "Trigger is required" "trigger-required": "Trigger is required"
}, },
@ -4185,6 +4195,10 @@
"edit-tenant-entity-import-rate-limit-title": "Edit entity version load rate limits", "edit-tenant-entity-import-rate-limit-title": "Edit entity version load rate limits",
"edit-tenant-notification-request-rate-limit-title": "Edit notification requests rate limits", "edit-tenant-notification-request-rate-limit-title": "Edit notification requests rate limits",
"edit-tenant-notification-requests-per-rule-rate-limit-title": "Edit notification requests per notification rule rate limits", "edit-tenant-notification-requests-per-rule-rate-limit-title": "Edit notification requests per notification rule rate limits",
"edit-edge-events-rate-limit": "Edit edge events rate limits",
"edit-edge-events-per-edge-rate-limit": "Edit edge events per edge rate limits",
"edge-events-rate-limit": "Edge events",
"edge-events-per-edge-rate-limit": "Edge events per edge",
"messages-per": "messages per", "messages-per": "messages per",
"not-set": "Not set", "not-set": "Not set",
"number-of-messages": "Number of messages", "number-of-messages": "Number of messages",
@ -5185,7 +5199,7 @@
"action-button": { "action-button": {
"behavior": "Behavior", "behavior": "Behavior",
"on-click": "On click", "on-click": "On click",
"on-click-hint": "Action performed when the button is clicked." "on-click-hint": "Action triggered when the button is clicked"
}, },
"command-button": { "command-button": {
"behavior": "Behavior", "behavior": "Behavior",
@ -5214,9 +5228,9 @@
}, },
"button-state": { "button-state": {
"activated-state": "Activated state", "activated-state": "Activated state",
"activated-state-hint": "Condition under which the button is active.", "activated-state-hint": "Configure condition under which the button is active.",
"disabled-state": "Disabled state", "disabled-state": "Disabled state",
"disabled-state-hint": "Condition under which the button is disabled.", "disabled-state-hint": "Configure condition under which the button is disabled.",
"enabled": "Enabled", "enabled": "Enabled",
"hovered": "Hovered", "hovered": "Hovered",
"pressed": "Pressed", "pressed": "Pressed",
@ -6526,13 +6540,13 @@
}, },
"rpc-state": { "rpc-state": {
"initial-state": "Initial state", "initial-state": "Initial state",
"initial-state-hint": "Action to get the initial value of the component.", "initial-state-hint": "Action to get the initial state (On/Off) of the component.",
"disabled-state": "Disabled state", "disabled-state": "Disabled state",
"disabled-state-hint": "Condition under which the component is disabled.", "disabled-state-hint": "Configure condition under which the component is disabled.",
"turn-on": "Turn 'On'", "turn-on": "Turn 'On'",
"turn-on-hint": "Action performed to turn ON the component.", "turn-on-hint": "Action triggered when the slider is switched to 'On'",
"turn-off": "Turn 'Off'", "turn-off": "Turn 'Off'",
"turn-off-hint": "Action performed to turn OFF the component.", "turn-off-hint": "Action triggered when the slider is switched to 'Off'",
"on": "On", "on": "On",
"off": "Off", "off": "Off",
"disabled": "Disabled" "disabled": "Disabled"

Loading…
Cancel
Save