Browse Source

Merge branch 'rc'

pull/13596/head
Igor Kulikov 12 months ago
parent
commit
550109080e
  1. 2
      application/src/main/data/json/demo/dashboards/rule_engine_statistics.json
  2. 13
      application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java
  3. 33
      application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java
  4. 7
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ResourcesShortageTriggerProcessor.java
  5. 25
      application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java
  6. 4
      application/src/main/java/org/thingsboard/server/service/ws/DefaultWebSocketService.java
  7. 39
      application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java
  8. 6
      application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java
  9. 40
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  10. 1
      common/data/src/main/java/org/thingsboard/server/common/data/SystemInfoData.java
  11. 7
      common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsState.java
  12. 11
      common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java
  13. 10
      common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java
  14. 1
      common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java
  15. 6
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/ResourcesShortageNotificationInfo.java
  16. 9
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeCommunicationFailureTrigger.java
  17. 9
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EdgeConnectionTrigger.java
  18. 9
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NewPlatformVersionTrigger.java
  19. 11
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTrigger.java
  20. 8
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RateLimitsTrigger.java
  21. 8
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ResourcesShortageTrigger.java
  22. 1
      common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java
  23. 1
      common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java
  24. 51
      common/edqs/src/main/java/org/thingsboard/server/edqs/util/VersionsStore.java
  25. 3
      common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java
  26. 2
      common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java
  27. 10
      dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java
  28. 3
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotifications.java
  29. 8
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
  30. 11
      dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java
  31. 19
      dao/src/main/java/org/thingsboard/server/dao/util/TenantRateLimitException.java
  32. 16
      msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java
  33. 19
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java
  34. 20
      netty-mqtt/src/test/java/org/thingsboard/mqtt/MqttClientTest.java
  35. 11
      ui-ngx/src/app/core/http/ota-package.service.ts
  36. 2
      ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss
  37. 18
      ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.ts
  38. 2
      ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts
  39. 6
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.scss
  40. 2
      ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.html
  41. 1
      ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts
  42. 6
      ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts
  43. 11
      ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts
  44. 7
      ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts
  45. 9
      ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-tooltip.models.ts
  46. 1
      ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts
  47. 9
      ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts
  48. 6
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.html
  49. 2
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.ts
  50. 15
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.html
  51. 6
      ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.ts
  52. 2
      ui-ngx/src/assets/dashboard/api_usage.json
  53. 6
      ui-ngx/src/assets/help/en_US/notification/resources_shortage.md
  54. 2
      ui-ngx/src/assets/locale/locale.constant-en_US.json
  55. 24
      ui-ngx/src/scss/animations.scss

2
application/src/main/data/json/demo/dashboards/rule_engine_statistics.json

@ -564,6 +564,7 @@
},
"tooltipDateColor": "rgba(0, 0, 0, 0.76)",
"tooltipDateInterval": true,
"tooltipHideZeroFalse": true,
"tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)",
"tooltipBackgroundBlur": 4,
"animation": {
@ -977,6 +978,7 @@
},
"tooltipDateColor": "rgba(0, 0, 0, 0.76)",
"tooltipDateInterval": true,
"tooltipHideZeroFalse": true,
"tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)",
"tooltipBackgroundBlur": 4,
"animation": {

13
application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntry.java

@ -16,13 +16,16 @@
package org.thingsboard.server.service.cf.ctx.state;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicKvEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.util.ProtoUtils;
@ -90,7 +93,15 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
@Override
public TbelCfArg toTbelCfArg() {
return new TbelCfSingleValueArg(ts, kvEntryValue.getValue());
Object value = kvEntryValue.getValue();
if (kvEntryValue instanceof JsonDataEntry) {
try {
value = JacksonUtil.readValue(kvEntryValue.getValueAsString(), new TypeReference<>() {
});
} catch (Exception e) {
}
}
return new TbelCfSingleValueArg(ts, value);
}
@Override

33
application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java

@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.notification.NotificationRequestStatus
import org.thingsboard.server.common.data.notification.info.NotificationInfo;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger.DeduplicationStrategy;
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.plugin.ComponentLifecycleEvent;
@ -66,8 +67,8 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
private final NotificationDeduplicationService deduplicationService;
private final PartitionService partitionService;
private final RateLimitService rateLimitService;
@Autowired @Lazy
private NotificationCenter notificationCenter;
@Lazy
private final NotificationCenter notificationCenter;
private final NotificationExecutorService notificationExecutor;
private final Map<NotificationRuleTriggerType, NotificationRuleTriggerProcessor> triggerProcessors = new EnumMap<>(NotificationRuleTriggerType.class);
@ -82,14 +83,11 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
if (enabledRules.isEmpty()) {
return;
}
if (trigger.deduplicate()) {
enabledRules = new ArrayList<>(enabledRules);
enabledRules.removeIf(rule -> deduplicationService.alreadyProcessed(trigger, rule));
}
final List<NotificationRule> rules = enabledRules;
for (NotificationRule rule : rules) {
List<NotificationRule> rulesToProcess = filterNotificationRules(trigger, enabledRules);
for (NotificationRule rule : rulesToProcess) {
try {
processNotificationRule(rule, trigger);
processNotificationRule(rule, trigger, DeduplicationStrategy.ONLY_MATCHING.equals(trigger.getDeduplicationStrategy()));
} catch (Throwable e) {
log.error("Failed to process notification rule {} for trigger type {} with trigger object {}", rule.getId(), rule.getTriggerType(), trigger, e);
}
@ -100,7 +98,20 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
});
}
private void processNotificationRule(NotificationRule rule, NotificationRuleTrigger trigger) {
private List<NotificationRule> filterNotificationRules(NotificationRuleTrigger trigger, List<NotificationRule> enabledRules) {
List<NotificationRule> rulesToProcess = new ArrayList<>(enabledRules);
rulesToProcess.removeIf(rule -> switch (trigger.getDeduplicationStrategy()) {
case ONLY_MATCHING -> {
boolean matched = matchesFilter(trigger, rule.getTriggerConfig());
yield !matched || deduplicationService.alreadyProcessed(trigger, rule);
}
case ALL -> deduplicationService.alreadyProcessed(trigger, rule);
default -> false;
});
return rulesToProcess;
}
private void processNotificationRule(NotificationRule rule, NotificationRuleTrigger trigger, boolean alreadyMatched) {
NotificationRuleTriggerConfig triggerConfig = rule.getTriggerConfig();
log.debug("Processing notification rule '{}' for trigger type {}", rule.getName(), rule.getTriggerType());
@ -114,7 +125,7 @@ public class DefaultNotificationRuleProcessor implements NotificationRuleProcess
return;
}
if (matchesFilter(trigger, triggerConfig)) {
if (alreadyMatched || matchesFilter(trigger, triggerConfig)) {
if (!rateLimitService.checkRateLimit(LimitedApi.NOTIFICATION_REQUESTS_PER_RULE, rule.getTenantId(), rule.getId())) {
log.debug("[{}] Rate limit for notification requests per rule was exceeded (rule '{}')", rule.getTenantId(), rule.getName());
return;

7
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ResourcesShortageTriggerProcessor.java

@ -39,7 +39,12 @@ public class ResourcesShortageTriggerProcessor implements NotificationRuleTrigge
@Override
public RuleOriginatedNotificationInfo constructNotificationInfo(ResourcesShortageTrigger trigger) {
return ResourcesShortageNotificationInfo.builder().resource(trigger.getResource().name()).usage(trigger.getUsage()).build();
return ResourcesShortageNotificationInfo.builder()
.resource(trigger.getResource().name())
.usage(trigger.getUsage())
.serviceId(trigger.getServiceId())
.serviceType(trigger.getServiceType())
.build();
}
@Override

25
application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java

@ -185,9 +185,14 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
long ts = System.currentTimeMillis();
List<SystemInfoData> clusterSystemData = getSystemData(serviceInfoProvider.getServiceInfo());
clusterSystemData.forEach(data -> {
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.CPU).usage(data.getCpuUsage()).build());
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.RAM).usage(data.getMemoryUsage()).build());
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.STORAGE).usage(data.getDiscUsage()).build());
Arrays.stream(Resource.values()).forEach(resource -> {
notificationRuleProcessor.process(ResourcesShortageTrigger.builder()
.resource(resource)
.serviceId(data.getServiceId())
.serviceType(data.getServiceType())
.usage(extractResourceUsage(data, resource))
.build());
});
});
BasicTsKvEntry clusterDataKv = new BasicTsKvEntry(ts, new JsonDataEntry("clusterSystemData", JacksonUtil.toString(clusterSystemData)));
doSave(Arrays.asList(new BasicTsKvEntry(ts, new BooleanDataEntry("clusterMode", true)), clusterDataKv));
@ -200,17 +205,17 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
getCpuUsage().ifPresent(v -> {
long value = (long) v;
tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuUsage", value)));
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.CPU).usage(value).build());
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.CPU).usage(value).serviceId(serviceInfoProvider.getServiceId()).serviceType(serviceInfoProvider.getServiceType()).build());
});
getMemoryUsage().ifPresent(v -> {
long value = (long) v;
tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("memoryUsage", value)));
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.RAM).usage(value).build());
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.RAM).usage(value).serviceId(serviceInfoProvider.getServiceId()).serviceType(serviceInfoProvider.getServiceType()).build());
});
getDiscSpaceUsage().ifPresent(v -> {
long value = (long) v;
tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("discUsage", value)));
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.STORAGE).usage(value).build());
notificationRuleProcessor.process(ResourcesShortageTrigger.builder().resource(Resource.STORAGE).usage(value).serviceId(serviceInfoProvider.getServiceId()).serviceType(serviceInfoProvider.getServiceType()).build());
});
getCpuCount().ifPresent(v -> tsList.add(new BasicTsKvEntry(ts, new LongDataEntry("cpuCount", (long) v))));
@ -258,6 +263,14 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
return infoData;
}
private Long extractResourceUsage(SystemInfoData info, Resource resource) {
return switch (resource) {
case CPU -> info.getCpuUsage();
case RAM -> info.getMemoryUsage();
case STORAGE -> info.getDiscUsage();
};
}
@PreDestroy
private void destroy() {
if (scheduler != null) {

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

@ -36,6 +36,7 @@ import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.exception.RateLimitExceededException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
@ -52,7 +53,6 @@ import org.thingsboard.server.common.msg.tools.TbRateLimitsException;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.util.TenantRateLimitException;
import org.thingsboard.server.exception.UnauthorizedException;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -742,7 +742,7 @@ public class DefaultWebSocketService implements WebSocketService {
@Override
public void onFailure(Throwable e) {
if (e instanceof TenantRateLimitException || e.getCause() instanceof TenantRateLimitException) {
if (e instanceof RateLimitExceededException || e.getCause() instanceof RateLimitExceededException) {
log.trace("[{}] Tenant rate limit detected for subscription: [{}]:{}", sessionRef.getSecurityCtx().getTenantId(), entityId, cmd);
} else {
log.info(FAILED_TO_FETCH_DATA, e);

39
application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SingleValueArgumentEntryTest.java

@ -17,8 +17,15 @@ package org.thingsboard.server.service.cf.ctx.state;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.thingsboard.script.api.tbel.TbelCfArg;
import org.thingsboard.script.api.tbel.TbelCfSingleValueArg;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ -73,4 +80,34 @@ public class SingleValueArgumentEntryTest {
void testUpdateEntryWhenValueWasNotChanged() {
assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 11L), 364L))).isTrue();
}
}
@Test
void testToTbelCfArgWhenJsonIsObject() {
entry = new SingleValueArgumentEntry(ts, new JsonDataEntry("key", "{\"test\": 10}"), 370L);
TbelCfArg tbelCfArg = entry.toTbelCfArg();
assertThat(tbelCfArg).isNotNull();
assertThat(tbelCfArg).isInstanceOf(TbelCfSingleValueArg.class);
TbelCfSingleValueArg singleValueArg = (TbelCfSingleValueArg) tbelCfArg;
assertThat(singleValueArg.getValue()).isInstanceOf(Map.class);
Map<String, Integer> expectedMap = Map.of("test", 10);
assertThat(singleValueArg.getValue()).isEqualTo(expectedMap);
}
@Test
void testToTbelCfArgWhenJsonIsArray() {
entry = new SingleValueArgumentEntry(ts, new JsonDataEntry("key", "[{\"test\": 10}, {\"test2\": 20}]"), 371L);
TbelCfArg tbelCfArg = entry.toTbelCfArg();
assertThat(tbelCfArg).isNotNull();
assertThat(tbelCfArg).isInstanceOf(TbelCfSingleValueArg.class);
TbelCfSingleValueArg singleValueArg = (TbelCfSingleValueArg) tbelCfArg;
assertThat(singleValueArg.getValue()).isInstanceOf(List.class);
List<Map<String, Integer>> expectedList = new ArrayList<>();
expectedList.add(Map.of("test", 10));
expectedList.add(Map.of("test2", 20));
assertThat(singleValueArg.getValue()).isEqualTo(expectedList);
}
}

6
application/src/test/java/org/thingsboard/server/service/job/JobManagerTest.java

@ -89,7 +89,7 @@ public class JobManagerTest extends AbstractControllerTest {
@Test
public void testSubmitJob_allTasksSuccessful() {
int tasksCount = 5;
int tasksCount = 7;
JobId jobId = submitJob(DummyJobConfiguration.builder()
.successfulTasksCount(tasksCount)
.taskProcessingTimeMs(1000)
@ -154,10 +154,10 @@ public class JobManagerTest extends AbstractControllerTest {
@Test
public void testCancelJob_whileRunning() throws Exception {
int tasksCount = 100;
int tasksCount = 200;
JobId jobId = submitJob(DummyJobConfiguration.builder()
.successfulTasksCount(tasksCount)
.taskProcessingTimeMs(100)
.taskProcessingTimeMs(50)
.build()).getId();
Thread.sleep(500);

40
application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java

@ -825,6 +825,8 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
notificationRuleProcessor.process(ResourcesShortageTrigger.builder()
.resource(Resource.RAM)
.usage(15L)
.serviceType("serviceType")
.serviceId("serviceId")
.build());
TimeUnit.MILLISECONDS.sleep(300);
}
@ -837,10 +839,48 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
notificationRuleProcessor.process(ResourcesShortageTrigger.builder()
.resource(Resource.RAM)
.usage(5L)
.serviceType("serviceType")
.serviceId("serviceId")
.build());
await("").atMost(5, TimeUnit.SECONDS).untilAsserted(() -> assertThat(getMyNotifications(false, 100)).size().isOne());
}
@Test
public void testNotificationsResourcesShortage_whenThresholdChangeToMatchingFilter_thenSendNotification() throws Exception {
loginSysAdmin();
ResourcesShortageNotificationRuleTriggerConfig triggerConfig = ResourcesShortageNotificationRuleTriggerConfig.builder()
.ramThreshold(1f)
.cpuThreshold(1f)
.storageThreshold(1f)
.build();
NotificationRule rule = createNotificationRule(triggerConfig, "Warning: ${resource} shortage", "${resource} shortage", createNotificationTarget(tenantAdminUserId).getId());
loginTenantAdmin();
Method method = DefaultSystemInfoService.class.getDeclaredMethod("saveCurrentMonolithSystemInfo");
method.setAccessible(true);
method.invoke(systemInfoService);
TimeUnit.SECONDS.sleep(5);
assertThat(getMyNotifications(false, 100)).size().isZero();
loginSysAdmin();
triggerConfig = ResourcesShortageNotificationRuleTriggerConfig.builder()
.ramThreshold(0.01f)
.cpuThreshold(1f)
.storageThreshold(1f)
.build();
rule.setTriggerConfig(triggerConfig);
saveNotificationRule(rule);
loginTenantAdmin();
method.invoke(systemInfoService);
await().atMost(10, TimeUnit.SECONDS).until(() -> getMyNotifications(false, 100).size() == 1);
Notification notification = getMyNotifications(false, 100).get(0);
assertThat(notification.getSubject()).isEqualTo("Warning: RAM shortage");
assertThat(notification.getText()).isEqualTo("RAM shortage");
}
@Test
public void testNotificationRuleDisabling() throws Exception {
EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig();

1
common/data/src/main/java/org/thingsboard/server/common/data/SystemInfoData.java

@ -20,6 +20,7 @@ import lombok.Data;
@Data
public class SystemInfoData {
@Schema(description = "Service Id.")
private String serviceId;
@Schema(description = "Service type.")

7
common/data/src/main/java/org/thingsboard/server/common/data/edqs/EdqsState.java

@ -20,7 +20,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.commons.lang3.BooleanUtils;
import static org.apache.commons.lang3.BooleanUtils.toBooleanDefaultIfNull;
@Getter
@NoArgsConstructor
@ -34,14 +35,14 @@ public class EdqsState {
private EdqsApiMode apiMode;
public boolean updateEdqsReady(boolean ready) {
boolean changed = BooleanUtils.toBooleanDefaultIfNull(this.edqsReady, false) != ready;
boolean changed = toBooleanDefaultIfNull(this.edqsReady, false) != ready;
this.edqsReady = ready;
return changed;
}
@JsonIgnore
public boolean isApiReady() {
return edqsReady && syncStatus == EdqsSyncStatus.FINISHED;
return toBooleanDefaultIfNull(edqsReady, false) && syncStatus == EdqsSyncStatus.FINISHED;
}
@JsonIgnore

11
common/data/src/main/java/org/thingsboard/server/common/data/job/task/DummyTaskResult.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.common.data.job.task;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@ -25,22 +26,25 @@ import org.thingsboard.server.common.data.job.JobType;
@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@SuperBuilder
@ToString(callSuper = true)
public class DummyTaskResult extends TaskResult {
private DummyTaskFailure failure;
@Builder
private DummyTaskResult(boolean success, boolean discarded, DummyTaskFailure failure) {
super(success, discarded);
this.failure = failure;
}
public static DummyTaskResult success(DummyTask task) {
return DummyTaskResult.builder()
.key(task.getKey())
.success(true)
.build();
}
public static DummyTaskResult failed(DummyTask task, Throwable error) {
return DummyTaskResult.builder()
.key(task.getKey())
.failure(DummyTaskFailure.builder()
.error(error.getMessage())
.number(task.getNumber())
@ -51,7 +55,6 @@ public class DummyTaskResult extends TaskResult {
public static DummyTaskResult discarded(DummyTask task) {
return DummyTaskResult.builder()
.key(task.getKey())
.discarded(true)
.build();
}

10
common/data/src/main/java/org/thingsboard/server/common/data/job/task/TaskResult.java

@ -20,16 +20,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.thingsboard.server.common.data.job.JobType;
@Data
@AllArgsConstructor
@NoArgsConstructor
@SuperBuilder
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "jobType")
@JsonSubTypes({
@ -40,6 +36,12 @@ public abstract class TaskResult {
private String key;
private boolean success;
private boolean discarded;
private long finishTs;
protected TaskResult(boolean success, boolean discarded) {
this.success = success;
this.discarded = discarded;
}
@JsonIgnore
public abstract JobType getJobType();

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

@ -43,6 +43,7 @@ public enum LimitedApi {
RateLimitUtil.merge(
DefaultTenantProfileConfiguration::getCassandraWriteQueryTenantCoreRateLimits,
DefaultTenantProfileConfiguration::getCassandraWriteQueryTenantRuleEngineRateLimits), "Monolith telemetry Cassandra write queries", true),
CASSANDRA_QUERIES(null, true), // left for backward compatibility with RateLimitsNotificationInfo
EDGE_EVENTS(DefaultTenantProfileConfiguration::getEdgeEventRateLimits, "Edge events", true),
EDGE_EVENTS_PER_EDGE(DefaultTenantProfileConfiguration::getEdgeEventRateLimitsPerEdge, "Edge events per edge", false),
EDGE_UPLINK_MESSAGES(DefaultTenantProfileConfiguration::getEdgeUplinkMessagesRateLimits, "Edge uplink messages", true),

6
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/ResourcesShortageNotificationInfo.java

@ -30,12 +30,16 @@ public class ResourcesShortageNotificationInfo implements RuleOriginatedNotifica
private String resource;
private Long usage;
private String serviceId;
private String serviceType;
@Override
public Map<String, String> getTemplateData() {
return Map.of(
"resource", resource,
"usage", String.valueOf(usage)
"usage", String.valueOf(usage),
"serviceId", serviceId,
"serviceType", serviceType
);
}

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

@ -23,12 +23,16 @@ 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.io.Serial;
import java.util.concurrent.TimeUnit;
@Data
@Builder
public class EdgeCommunicationFailureTrigger implements NotificationRuleTrigger {
@Serial
private static final long serialVersionUID = 2918443863787603524L;
private final TenantId tenantId;
private final CustomerId customerId;
private final EdgeId edgeId;
@ -37,8 +41,8 @@ public class EdgeCommunicationFailureTrigger implements NotificationRuleTrigger
private final String error;
@Override
public boolean deduplicate() {
return true;
public DeduplicationStrategy getDeduplicationStrategy() {
return DeduplicationStrategy.ALL;
}
@Override
@ -60,4 +64,5 @@ public class EdgeCommunicationFailureTrigger implements NotificationRuleTrigger
public EntityId getOriginatorEntityId() {
return edgeId;
}
}

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

@ -23,12 +23,16 @@ 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.io.Serial;
import java.util.concurrent.TimeUnit;
@Data
@Builder
public class EdgeConnectionTrigger implements NotificationRuleTrigger {
@Serial
private static final long serialVersionUID = -261939829962721957L;
private final TenantId tenantId;
private final CustomerId customerId;
private final EdgeId edgeId;
@ -36,8 +40,8 @@ public class EdgeConnectionTrigger implements NotificationRuleTrigger {
private final String edgeName;
@Override
public boolean deduplicate() {
return true;
public DeduplicationStrategy getDeduplicationStrategy() {
return DeduplicationStrategy.ALL;
}
@Override
@ -59,4 +63,5 @@ public class EdgeConnectionTrigger implements NotificationRuleTrigger {
public EntityId getOriginatorEntityId() {
return edgeId;
}
}

9
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NewPlatformVersionTrigger.java

@ -22,10 +22,15 @@ 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.io.Serial;
@Data
@Builder
public class NewPlatformVersionTrigger implements NotificationRuleTrigger {
@Serial
private static final long serialVersionUID = 3298785969736390092L;
private final UpdateMessage updateInfo;
@Override
@ -45,8 +50,8 @@ public class NewPlatformVersionTrigger implements NotificationRuleTrigger {
@Override
public boolean deduplicate() {
return true;
public DeduplicationStrategy getDeduplicationStrategy() {
return DeduplicationStrategy.ALL;
}
@Override

11
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/NotificationRuleTrigger.java

@ -29,9 +29,8 @@ public interface NotificationRuleTrigger extends Serializable {
EntityId getOriginatorEntityId();
default boolean deduplicate() {
return false;
default DeduplicationStrategy getDeduplicationStrategy() {
return DeduplicationStrategy.NONE;
}
default String getDeduplicationKey() {
@ -43,4 +42,10 @@ public interface NotificationRuleTrigger extends Serializable {
return 0;
}
enum DeduplicationStrategy {
NONE,
ALL,
ONLY_MATCHING
}
}

8
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RateLimitsTrigger.java

@ -22,12 +22,16 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.notification.rule.trigger.config.NotificationRuleTriggerType;
import java.io.Serial;
import java.util.concurrent.TimeUnit;
@Data
@Builder
public class RateLimitsTrigger implements NotificationRuleTrigger {
@Serial
private static final long serialVersionUID = -4423112145409424886L;
private final TenantId tenantId;
private final LimitedApi api;
private final EntityId limitLevel;
@ -45,8 +49,8 @@ public class RateLimitsTrigger implements NotificationRuleTrigger {
@Override
public boolean deduplicate() {
return true;
public DeduplicationStrategy getDeduplicationStrategy() {
return DeduplicationStrategy.ALL;
}
@Override

8
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ResourcesShortageTrigger.java

@ -33,6 +33,8 @@ public class ResourcesShortageTrigger implements NotificationRuleTrigger {
private Resource resource;
private Long usage;
private String serviceId;
private String serviceType;
@Override
public TenantId getTenantId() {
@ -45,13 +47,13 @@ public class ResourcesShortageTrigger implements NotificationRuleTrigger {
}
@Override
public boolean deduplicate() {
return true;
public DeduplicationStrategy getDeduplicationStrategy() {
return DeduplicationStrategy.ONLY_MATCHING;
}
@Override
public String getDeduplicationKey() {
return resource.name();
return String.join(":", resource.name(), serviceId, serviceType);
}
@Override

1
common/edqs/src/main/java/org/thingsboard/server/edqs/processor/EdqsProcessor.java

@ -277,6 +277,7 @@ public class EdqsProcessor implements TbQueueHandler<TbProtoQueueMsg<ToEdqsMsg>,
eventConsumer.awaitStop();
responseTemplate.stop();
stateService.stop();
versionsStore.shutdown();
}
}

1
common/edqs/src/main/java/org/thingsboard/server/edqs/state/KafkaEdqsStateService.java

@ -224,6 +224,7 @@ public class KafkaEdqsStateService implements EdqsStateService {
stateConsumer.awaitStop();
eventsToBackupConsumer.stop();
stateProducer.stop();
versionsStore.shutdown();
}
}

51
common/edqs/src/main/java/org/thingsboard/server/edqs/util/VersionsStore.java

@ -15,31 +15,35 @@
*/
package org.thingsboard.server.edqs.util;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.edqs.EdqsObjectKey;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@Slf4j
public class VersionsStore {
private final Cache<EdqsObjectKey, Long> versions;
private final ConcurrentMap<EdqsObjectKey, TimedValue> versions = new ConcurrentHashMap<>();
private final long expirationMillis;
private final ScheduledExecutorService cleaner = Executors.newSingleThreadScheduledExecutor();
public VersionsStore(int ttlMinutes) {
this.versions = Caffeine.newBuilder()
.expireAfterWrite(ttlMinutes, TimeUnit.MINUTES)
.build();
this.expirationMillis = TimeUnit.MINUTES.toMillis(ttlMinutes);
startCleanupTask();
}
public boolean isNew(EdqsObjectKey key, Long version) {
AtomicBoolean isNew = new AtomicBoolean(false);
versions.asMap().compute(key, (k, prevVersion) -> {
if (prevVersion == null || prevVersion <= version) {
versions.compute(key, (k, prevVersion) -> {
if (prevVersion == null || prevVersion.value <= version) {
isNew.set(true);
return version;
return new TimedValue(version);
} else {
log.debug("[{}] Version {} is outdated, the latest is {}", key, version, prevVersion);
return prevVersion;
@ -48,4 +52,33 @@ public class VersionsStore {
return isNew.get();
}
private void startCleanupTask() {
cleaner.scheduleAtFixedRate(() -> {
try {
long now = System.currentTimeMillis();
for (Map.Entry<EdqsObjectKey, TimedValue> entry : versions.entrySet()) {
if (now - entry.getValue().lastUpdated > expirationMillis) {
versions.remove(entry.getKey(), entry.getValue());
}
}
} catch (Exception e) {
log.error("Cleanup task failed", e);
}
}, expirationMillis, expirationMillis, TimeUnit.MILLISECONDS);
}
public void shutdown() {
cleaner.shutdown();
}
private static class TimedValue {
private final long lastUpdated;
private final long value;
public TimedValue(long value) {
this.value = value;
this.lastUpdated = System.currentTimeMillis();
}
}
}

3
common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java

@ -22,6 +22,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.JavaSerDesUtil;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTrigger.DeduplicationStrategy;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
@ -47,7 +48,7 @@ public class RemoteNotificationRuleProcessor implements NotificationRuleProcesso
@Override
public void process(NotificationRuleTrigger trigger) {
try {
if (trigger.deduplicate() && deduplicationService.alreadyProcessed(trigger)) {
if (!DeduplicationStrategy.NONE.equals(trigger.getDeduplicationStrategy()) && deduplicationService.alreadyProcessed(trigger)) {
return;
}

2
common/queue/src/main/java/org/thingsboard/server/queue/task/TaskProcessor.java

@ -232,6 +232,8 @@ public abstract class TaskProcessor<T extends Task<R>, R extends TaskResult> {
}
private void reportTaskResult(T task, R result) {
result.setKey(task.getKey());
result.setFinishTs(System.currentTimeMillis());
statsService.reportTaskResult(task.getTenantId(), task.getJobId(), result);
}

10
dao/src/main/java/org/thingsboard/server/dao/job/DefaultJobService.java

@ -69,7 +69,6 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
job.setStatus(QUEUED);
} else {
job.setStatus(PENDING);
job.getResult().setStartTs(System.currentTimeMillis());
}
return saveJob(tenantId, job, true, null);
}
@ -125,6 +124,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
}
boolean publishEvent = false;
long lastFinishTs = 0;
for (TaskResult taskResult : jobStats.getTaskResults()) {
if (!taskResult.getKey().equals(job.getConfiguration().getTasksKey())) {
log.debug("Ignoring task result {} with outdated key {}", taskResult, job.getConfiguration().getTasksKey());
@ -140,6 +140,9 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
publishEvent = true;
}
}
if (taskResult.getFinishTs() > lastFinishTs) {
lastFinishTs = taskResult.getFinishTs();
}
}
if (job.getStatus() == RUNNING) {
@ -153,7 +156,7 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
job.setStatus(COMPLETED);
publishEvent = true;
}
result.setFinishTs(System.currentTimeMillis());
result.setFinishTs(lastFinishTs);
job.getConfiguration().setToReprocess(null);
}
}
@ -166,6 +169,9 @@ public class DefaultJobService extends AbstractEntityService implements JobServi
if (!Job.SUPPORTED_ENTITY_TYPES.contains(job.getEntityId().getEntityType())) {
throw new IllegalArgumentException("Unsupported entity type " + job.getEntityId().getEntityType());
}
if (job.getStatus() == PENDING) {
job.getResult().setStartTs(System.currentTimeMillis());
}
job = jobDao.save(tenantId, job);
if (publishEvent) {

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

@ -33,7 +33,6 @@ import org.thingsboard.server.common.data.notification.rule.DefaultNotificationR
import org.thingsboard.server.common.data.notification.rule.EscalatedNotificationRuleRecipientsConfig;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.ResourcesShortageTrigger.Resource;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmAssignmentNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmCommentNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.config.AlarmNotificationRuleTriggerConfig;
@ -377,7 +376,7 @@ public class DefaultNotifications {
public static final DefaultNotification resourcesShortage = DefaultNotification.builder()
.name("Resources shortage notification")
.type(NotificationType.RESOURCES_SHORTAGE)
.subject("Warning: ${resource} shortage")
.subject("Warning: ${resource} shortage for ${serviceId}")
.text("${resource} usage is at ${usage}%.")
.icon("warning")
.rule(DefaultRule.builder()

8
dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java

@ -205,8 +205,8 @@ public class BaseTimeseriesService implements TimeseriesService {
ListenableFuture<Integer> dpsFuture = saveTs ? Futures.transform(Futures.allAsList(tsFutures), SUM_ALL_INTEGERS, MoreExecutors.directExecutor()) : Futures.immediateFuture(0);
ListenableFuture<List<Long>> versionsFuture = saveLatest ? Futures.allAsList(latestFutures) : Futures.immediateFuture(null);
return Futures.whenAllComplete(dpsFuture, versionsFuture).call(() -> {
Integer dataPoints = Futures.getUnchecked(dpsFuture);
List<Long> versions = Futures.getUnchecked(versionsFuture);
Integer dataPoints = dpsFuture.get();
List<Long> versions = versionsFuture.get();
return TimeseriesSaveResult.of(dataPoints, versions);
}, MoreExecutors.directExecutor());
}
@ -298,13 +298,13 @@ public class BaseTimeseriesService implements TimeseriesService {
long interval = query.getInterval();
if (interval < 1) {
throw new IncorrectParameterException("Invalid TsKvQuery: 'interval' must be greater than 0, but got " + interval +
". Please check your query parameters and ensure 'endTs' is greater than 'startTs' or increase 'interval'.");
". Please check your query parameters and ensure 'endTs' is greater than 'startTs' or increase 'interval'.");
}
long step = Math.max(interval, 1000);
long intervalCounts = (query.getEndTs() - query.getStartTs()) / step;
if (intervalCounts > maxTsIntervals || intervalCounts < 0) {
throw new IncorrectParameterException("Incorrect TsKvQuery. Number of intervals is to high - " + intervalCounts + ". " +
"Please increase 'interval' parameter for your query or reduce the time range of the query.");
"Please increase 'interval' parameter for your query or reduce the time range of the query.");
}
}
}

11
dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java

@ -32,6 +32,7 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.common.data.exception.RateLimitExceededException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.msg.queue.ServiceType;
@ -66,7 +67,7 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
private final long maxWaitTime;
private final long pollMs;
private final String bufferName;
private final String bufferName;
private final BlockingQueue<AsyncTaskContext<T, V>> queue;
private final ExecutorService dispatcherExecutor;
private final ExecutorService callbackExecutor;
@ -124,7 +125,7 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
if (!rateLimitService.checkRateLimit(myLimitedApi, tenantId, tenantId, true)) {
stats.incrementRateLimitedTenant(tenantId);
stats.getTotalRateLimited().increment();
settableFuture.setException(new TenantRateLimitException());
settableFuture.setException(new RateLimitExceededException(myLimitedApi));
perTenantLimitReached = true;
}
} else if (tenantId == null) {
@ -299,9 +300,9 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
.count();
if (queueSize > 0
|| rateLimitedTenantsCount > 0
|| concurrencyLevel.get() > 0
|| stats.getStatsCounters().stream().anyMatch(counter -> counter.get() > 0)
|| rateLimitedTenantsCount > 0
|| concurrencyLevel.get() > 0
|| stats.getStatsCounters().stream().anyMatch(counter -> counter.get() > 0)
) {
StringBuilder statsBuilder = new StringBuilder();

19
dao/src/main/java/org/thingsboard/server/dao/util/TenantRateLimitException.java

@ -1,19 +0,0 @@
/**
* Copyright © 2016-2025 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.util;
public class TenantRateLimitException extends Exception {
}

16
msa/black-box-tests/src/test/java/org/thingsboard/server/msa/connectivity/MqttClientTest.java

@ -556,22 +556,6 @@ public class MqttClientTest extends AbstractContainerTest {
assertThat(provisionResponse.get("status").asText()).isEqualTo("NOT_FOUND");
}
@Test
public void regularDisconnect() throws Exception {
DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());
MqttMessageListener listener = new MqttMessageListener();
MqttClient mqttClient = getMqttClient(deviceCredentials, listener, MqttVersion.MQTT_5);
final List<Byte> returnCodeByteValue = new ArrayList<>();
MqttClientCallback callbackForDisconnectWithReturnCode = getCallbackWrapperForDisconnectWithReturnCode(returnCodeByteValue);
mqttClient.setCallback(callbackForDisconnectWithReturnCode);
mqttClient.disconnect();
Thread.sleep(1000);
assertThat(returnCodeByteValue.size()).isEqualTo(1);
MqttReasonCodes.Disconnect returnCode = MqttReasonCodes.Disconnect.valueOf(returnCodeByteValue.get(0));
assertThat(returnCode).isEqualTo(MqttReasonCodes.Disconnect.NORMAL_DISCONNECT);
}
@Test
public void clientSessionTakenOverDisconnect() throws Exception {
DeviceCredentials deviceCredentials = testRestClient.getDeviceCredentialsByDeviceId(device.getId());

19
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java

@ -96,6 +96,8 @@ final class MqttClientImpl implements MqttClient {
private final ListeningExecutor handlerExecutor;
private final static int DISCONNECT_FALLBACK_DELAY_SECS = 1;
/**
* Construct the MqttClientImpl with default config
*/
@ -456,16 +458,25 @@ final class MqttClientImpl implements MqttClient {
@Override
public void disconnect() {
if (disconnected) {
return;
}
log.trace("[{}] Disconnecting from server", channel != null ? channel.id() : "UNKNOWN");
disconnected = true;
if (this.channel != null) {
MqttMessage message = new MqttMessage(new MqttFixedHeader(MqttMessageType.DISCONNECT, false, MqttQoS.AT_MOST_ONCE, false, 0));
ChannelFuture channelFuture = this.sendAndFlushPacket(message);
sendAndFlushPacket(message).addListener((ChannelFutureListener) future -> {
future.channel().close();
disconnected = true;
});
eventLoop.schedule(() -> {
if (!channelFuture.isDone()) {
if (channel.isOpen()) {
log.trace("[{}] Channel still open after {} second; forcing close now", channel.id(), DISCONNECT_FALLBACK_DELAY_SECS);
this.channel.close();
disconnected = true;
}
}, 500, TimeUnit.MILLISECONDS);
}, DISCONNECT_FALLBACK_DELAY_SECS, TimeUnit.SECONDS);
}
}

20
netty-mqtt/src/test/java/org/thingsboard/mqtt/MqttClientTest.java

@ -119,6 +119,26 @@ class MqttClientTest {
assertThat(client.isConnected()).isTrue();
}
@Test
void testDisconnectFromBroker() {
// GIVEN
var clientConfig = new MqttClientConfig();
clientConfig.setOwnerId("Test[Disconnect]");
clientConfig.setClientId("disconnect");
client = MqttClient.create(clientConfig, null, handlerExecutor);
connect(broker.getHost(), broker.getMqttPort());
// WHEN
client.disconnect();
// THEN
Awaitility.await("waiting for client to disconnect")
.atMost(Duration.ofSeconds(5))
.untilAsserted(() -> assertThat(client.isConnected()).isFalse());
}
@Test
void testDisconnectDueToKeepAliveIfNoActivity() {
// GIVEN

11
ui-ngx/src/app/core/http/ota-package.service.ts

@ -129,15 +129,16 @@ export class OtaPackageService {
}
return forkJoin(tasks).pipe(
mergeMap(([deviceFirmwareUpdate, deviceSoftwareUpdate]) => {
let text = '';
const lines: string[] = [];
if (deviceFirmwareUpdate > 0) {
text += this.translate.instant('ota-update.change-firmware', {count: deviceFirmwareUpdate});
lines.push(this.translate.instant('ota-update.change-firmware', {count: deviceFirmwareUpdate}));
}
if (deviceSoftwareUpdate > 0) {
text += text.length ? ' ' : '';
text += this.translate.instant('ota-update.change-software', {count: deviceSoftwareUpdate});
lines.push(this.translate.instant('ota-update.change-software', {count: deviceSoftwareUpdate}));
}
return text !== '' ? this.dialogService.confirm('', text, null, this.translate.instant('common.proceed')) : of(true);
return lines.length
? this.dialogService.confirm(this.translate.instant('ota-update.change-ota-setting-title'), lines.join('<br/>'), null, this.translate.instant('common.proceed'))
: of(true);
})
);
}

2
ui-ngx/src/app/modules/home/components/dashboard-page/edit-widget.component.scss

@ -21,7 +21,7 @@
right: 0;
bottom: 0;
background: #fff;
z-index: 5;
z-index: 100;
}
.widget-preview-section {
position: absolute;

18
ui-ngx/src/app/modules/home/components/dashboard-page/states/manage-dashboard-states-dialog.component.ts

@ -14,7 +14,16 @@
/// limitations under the License.
///
import { AfterViewInit, Component, ElementRef, Inject, OnInit, SkipSelf, ViewChild } from '@angular/core';
import {
AfterViewInit,
Component,
ElementRef,
Inject,
OnInit,
SecurityContext,
SkipSelf,
ViewChild
} from '@angular/core';
import { ErrorStateMatcher } from '@angular/material/core';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { Store } from '@ngrx/store';
@ -42,6 +51,7 @@ import {
} from '@home/components/dashboard-page/states/dashboard-state-dialog.component';
import { UtilsService } from '@core/services/utils.service';
import { Widget } from '@shared/models/widget.models';
import { DomSanitizer } from '@angular/platform-browser';
export interface ManageDashboardStatesDialogData {
states: {[id: string]: DashboardState };
@ -87,7 +97,8 @@ export class ManageDashboardStatesDialogComponent
private translate: TranslateService,
private dialogs: DialogService,
private utils: UtilsService,
private dialog: MatDialog) {
private dialog: MatDialog,
private sanitizer: DomSanitizer) {
super(store, router, dialogRef);
this.states = this.data.states;
@ -148,7 +159,8 @@ export class ManageDashboardStatesDialogComponent
}
const title = this.translate.instant('dashboard.delete-state-title');
const content = this.translate.instant('dashboard.delete-state-text', {stateName: state.name});
this.dialogs.confirm(title, content, this.translate.instant('action.no'),
const safeContent = this.sanitizer.sanitize(SecurityContext.HTML, content);
this.dialogs.confirm(title, safeContent, this.translate.instant('action.no'),
this.translate.instant('action.yes')).subscribe(
(res) => {
if (res) {

2
ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.ts

@ -103,7 +103,7 @@ export class DeviceProfileTransportConfigurationComponent implements ControlValu
delete configuration.type;
}
setTimeout(() => {
this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: false});
this.deviceProfileTransportConfigurationFormGroup.patchValue({configuration}, {emitEvent: this.isAdd});
}, 0);
}

6
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-instances.component.scss

@ -31,4 +31,10 @@
.mat-expansion-panel-header-title {
margin-right: 0;
}
&::ng-deep {
.mat-content.mat-content-hide-toggle {
margin-right: 0;
}
}
}

2
ui-ngx/src/app/modules/home/components/profile/device/lwm2m/lwm2m-observe-attr-telemetry-resources.component.html

@ -49,7 +49,7 @@
</mat-checkbox>
</div>
<div class="flex max-w-10% flex-full items-center justify-center">
<mat-checkbox class="max-w-10% flex-full" formControlName="observe" color="primary"
<mat-checkbox formControlName="observe" color="primary"
matTooltip="{{ 'device-profile.lwm2m.edit-observe-select' | translate }}"
[matTooltipDisabled]="disabled || !isDisabledObserve($index)"
matTooltipPosition="above">

1
ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.ts

@ -146,6 +146,7 @@ export class WidgetActionDialogComponent extends DialogComponent<WidgetActionDia
if (this.widgetActionFormGroup.get('actionSourceId').value === 'headerButton') {
this.widgetActionFormGroup.get('buttonType').enable({emitEvent: false});
this.widgetActionFormGroup.get('buttonColor').enable({emitEvent: false});
this.widgetActionFormGroup.get('customButtonStyle').enable({emitEvent: false});
this.widgetHeaderButtonValidators(true);
}
this.widgetActionFormGroup.get('actionSourceId').valueChanges.pipe(

6
ui-ngx/src/app/modules/home/components/widget/lib/analogue-gauge.models.ts

@ -285,10 +285,10 @@ function getValueDec(ctx: WidgetContext, _settings: AnalogueGaugeSettings): numb
if (ctx.data && ctx.data[0]) {
dataKey = ctx.data[0].dataKey;
}
if (dataKey && isDefined(dataKey.decimals)) {
if (dataKey && isDefinedAndNotNull(dataKey.decimals)) {
return dataKey.decimals;
} else {
return isDefinedAndNotNull(ctx.decimals) ? ctx.decimals : 0;
return ctx.decimals ?? 0;
}
}
@ -300,6 +300,6 @@ function getUnits(ctx: WidgetContext, settings: AnalogueGaugeSettings): TbUnit {
if (dataKey?.units) {
return dataKey.units;
} else {
return isDefinedAndNotNull(settings.units) ? settings.units : ctx.units;
return settings.units ?? ctx.units;
}
}

11
ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.component.ts

@ -32,7 +32,8 @@ import {
ComponentStyle,
getDataKey,
overlayStyle,
textStyle
textStyle,
ValueFormatProcessor
} from '@shared/models/widget-settings.models';
import { isDefinedAndNotNull } from '@core/utils';
import {
@ -113,11 +114,17 @@ export class RangeChartWidgetComponent implements OnInit, OnDestroy, AfterViewIn
this.units = unitService.getTargetUnitSymbol(units);
this.unitConvertor = unitService.geUnitConverter(units);
const valueFormat = ValueFormatProcessor.fromSettings(this.ctx.$injector, {
units,
decimals: this.decimals,
ignoreUnitSymbol: true
});
this.backgroundStyle$ = backgroundStyle(this.settings.background, this.imagePipe, this.sanitizer);
this.overlayStyle = overlayStyle(this.settings.background.overlay);
this.padding = this.settings.background.overlay.enabled ? undefined : this.settings.padding;
this.rangeItems = toRangeItems(this.settings.rangeColors, this.unitConvertor);
this.rangeItems = toRangeItems(this.settings.rangeColors, valueFormat);
this.visibleRangeItems = this.rangeItems.filter(item => item.visible);
this.showLegend = this.settings.showLegend && !!this.rangeItems.length;

7
ui-ngx/src/app/modules/home/components/widget/lib/chart/range-chart-widget.models.ts

@ -22,6 +22,7 @@ import {
Font,
simpleDateFormat,
sortedColorRange,
ValueFormatProcessor,
ValueSourceType
} from '@shared/models/widget-settings.models';
import { LegendPosition } from '@shared/models/widget.models';
@ -291,21 +292,21 @@ export const rangeChartTimeSeriesKeySettings = (settings: RangeChartWidgetSettin
}
});
export const toRangeItems = (colorRanges: Array<ColorRange>, convertValue: (x: number) => number): RangeItem[] => {
export const toRangeItems = (colorRanges: Array<ColorRange>, valueFormat: ValueFormatProcessor): RangeItem[] => {
const rangeItems: RangeItem[] = [];
let counter = 0;
const ranges = sortedColorRange(filterIncludingColorRanges(colorRanges)).filter(r => isNumber(r.from) || isNumber(r.to));
for (let i = 0; i < ranges.length; i++) {
const range = ranges[i];
let from = range.from;
const to = isDefinedAndNotNull(range.to) ? convertValue(range.to) : range.to;
const to = isDefinedAndNotNull(range.to) ? Number(valueFormat.format(range.to)) : range.to;
if (i > 0) {
const prevRange = ranges[i - 1];
if (isNumber(prevRange.to) && isNumber(from) && from < prevRange.to) {
from = prevRange.to;
}
}
from = isDefinedAndNotNull(from) ? convertValue(from) : from;
from = isDefinedAndNotNull(from) ? Number(valueFormat.format(from)) : from;
rangeItems.push(
{
index: counter++,

9
ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-tooltip.models.ts

@ -17,9 +17,7 @@
import { isFunction } from '@core/utils';
import { FormattedData } from '@shared/models/widget.models';
import { DateFormatProcessor, DateFormatSettings, Font } from '@shared/models/widget-settings.models';
import {
TimeSeriesChartDataItem,
} from '@home/components/widget/lib/chart/time-series-chart.models';
import { TimeSeriesChartDataItem } from '@home/components/widget/lib/chart/time-series-chart.models';
import { Renderer2, SecurityContext } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { CallbackDataParams } from 'echarts/types/dist/shared';
@ -104,6 +102,9 @@ export class TimeSeriesChartTooltip {
if (!tooltipParams.items.length && !tooltipParams.comparisonItems.length) {
return null;
}
if (this.settings.tooltipHideZeroFalse && !tooltipParams.items.some(value => value.param.value[1] && value.param.value[1] !== 'false')) {
return undefined;
}
const tooltipElement: HTMLElement = this.renderer.createElement('div');
this.renderer.setStyle(tooltipElement, 'display', 'flex');
@ -130,7 +131,7 @@ export class TimeSeriesChartTooltip {
this.renderer.appendChild(tooltipItemsElement, this.constructTooltipDateElement(items[0].param, interval));
}
for (const item of items) {
if (!this.settings.tooltipHideZeroFalse || item.param.value[1]) {
if (!this.settings.tooltipHideZeroFalse || (item.param.value[1] && item.param.value[1] !== 'false')) {
this.renderer.appendChild(tooltipItemsElement, this.constructTooltipSeriesElement(item));
}
}

1
ui-ngx/src/app/modules/home/components/widget/lib/digital-gauge.ts

@ -125,6 +125,7 @@ export class TbCanvasDigitalGauge {
this.barColorProcessor = ColorProcessor.fromSettings(settings.barColor, this.ctx);
this.valueFormat = ValueFormatProcessor.fromSettings(this.ctx.$injector, {
units: this.localSettings.units,
decimals: this.localSettings.decimals,
ignoreUnitSymbol: true
});

9
ui-ngx/src/app/modules/home/pages/device-profile/device-profile-tabs.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, DestroyRef } from '@angular/core';
import { Component, DestroyRef, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { EntityTabsComponent } from '../../components/entity/entity-tabs.component';
@ -31,7 +31,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
templateUrl: './device-profile-tabs.component.html',
styleUrls: []
})
export class DeviceProfileTabsComponent extends EntityTabsComponent<DeviceProfile> {
export class DeviceProfileTabsComponent extends EntityTabsComponent<DeviceProfile> implements OnInit {
deviceTransportTypes = Object.values(DeviceTransportType);
@ -55,4 +55,9 @@ export class DeviceProfileTabsComponent extends EntityTabsComponent<DeviceProfil
});
}
protected setEntity(entity: DeviceProfile) {
this.isTransportTypeChanged = false;
super.setEntity(entity);
}
}

6
ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.html

@ -62,6 +62,12 @@
*ngIf="actionButtonConfigForm.get('link').hasError('required')">
{{ 'notification.link-required' | translate }}
</mat-error>
<mat-error
*ngIf="actionButtonConfigForm.get('link').hasError('maxlength')">
{{ 'notification.link-max-length' | translate :
{length: actionButtonConfigForm.get('link').getError('maxlength').requiredLength}
}}
</mat-error>
</mat-form-field>
<ng-template #dashboardSelector>
<tb-dashboard-autocomplete

2
ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-action-button-configuration.component.ts

@ -84,7 +84,7 @@ export class NotificationActionButtonConfigurationComponent implements ControlVa
enabled: [false],
text: [{value: '', disabled: true}, [Validators.required, Validators.maxLength(50)]],
linkType: [ActionButtonLinkType.LINK],
link: [{value: '', disabled: true}, Validators.required],
link: [{value: '', disabled: true}, [Validators.required, Validators.maxLength(300)]],
dashboardId: [{value: null, disabled: true}, Validators.required],
dashboardState: [{value: null, disabled: true}],
setEntityIdInState: [{value: true, disabled: true}]

15
ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.html

@ -44,6 +44,11 @@
<mat-error *ngIf="templateConfigurationForm.get('WEB.subject').hasError('required')">
{{ 'notification.subject-required' | translate }}
</mat-error>
<mat-error *ngIf="templateConfigurationForm.get('WEB.subject').hasError('maxlength')">
{{'notification.subject-max-length' | translate :
{length: templateConfigurationForm.get('WEB.subject').getError('maxlength').requiredLength}
}}
</mat-error>
</mat-form-field>
<mat-form-field class="mat-block">
<mat-label translate>notification.message</mat-label>
@ -56,6 +61,11 @@
<mat-error *ngIf="templateConfigurationForm.get('WEB.body').hasError('required')">
{{ 'notification.message-required' | translate }}
</mat-error>
<mat-error *ngIf="templateConfigurationForm.get('WEB.body').hasError('maxlength')">
{{ 'notification.message-max-length' | translate :
{length: templateConfigurationForm.get('WEB.body').getError('maxlength').requiredLength}
}}
</mat-error>
</mat-form-field>
<section formGroupName="additionalConfig" class="tb-form-panel no-padding no-border">
<div class="tb-form-row space-between" formGroupName="icon">
@ -194,6 +204,11 @@
<mat-error *ngIf="templateConfigurationForm.get('EMAIL.subject').hasError('required')">
{{ 'notification.subject-required' | translate }}
</mat-error>
<mat-error *ngIf="templateConfigurationForm.get('EMAIL.subject').hasError('maxlength')">
{{'notification.subject-max-length' | translate :
{length: templateConfigurationForm.get('EMAIL.subject').getError('maxlength').requiredLength}
}}
</mat-error>
</mat-form-field>
<mat-label class="tb-title tb-required"
[class.tb-error]="(interacted || templateConfigurationForm.get('EMAIL.body').touched) && templateConfigurationForm.get('EMAIL.body').hasError('required')"

6
ui-ngx/src/app/modules/home/pages/notification/template/configuration/notification-template-configuration.component.ts

@ -226,8 +226,8 @@ export class NotificationTemplateConfigurationComponent implements OnDestroy, Co
switch (deliveryMethod) {
case NotificationDeliveryMethod.WEB:
deliveryMethodForm = this.fb.group({
subject: ['', Validators.required],
body: ['', Validators.required],
subject: ['', [Validators.required, Validators.maxLength(150)]],
body: ['', [Validators.required, Validators.maxLength(250)]],
additionalConfig: this.fb.group({
icon: this.fb.group({
enabled: [false],
@ -252,7 +252,7 @@ export class NotificationTemplateConfigurationComponent implements OnDestroy, Co
break;
case NotificationDeliveryMethod.EMAIL:
deliveryMethodForm = this.fb.group({
subject: ['', Validators.required],
subject: ['', [Validators.required, Validators.maxLength(250)]],
body: ['', Validators.required]
});
break;

2
ui-ngx/src/assets/dashboard/api_usage.json

@ -7885,6 +7885,7 @@
},
"tooltipDateColor": "rgba(0, 0, 0, 0.76)",
"tooltipDateInterval": true,
"tooltipHideZeroFalse": true,
"tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)",
"tooltipBackgroundBlur": 4,
"animation": {
@ -8293,6 +8294,7 @@
},
"tooltipDateColor": "rgba(0, 0, 0, 0.76)",
"tooltipDateInterval": true,
"tooltipHideZeroFalse": true,
"tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)",
"tooltipBackgroundBlur": 4,
"animation": {

6
ui-ngx/src/assets/help/en_US/notification/resources_shortage.md

@ -9,8 +9,10 @@ See the available types and parameters below:
Available template parameters:
* `resource` - the resource name;
* `usage` - the resource usage value;
* `resource` - the resource name (e.g., "CPU", "RAM", "STORAGE");
* `usage` - the current usage value of the resource;
* `serviceId` - the service id (convenient in cluster setup);
* `serviceType` - the service type (convenient in cluster setup);
Parameter names must be wrapped using `${...}`. For example: `${resource}`.
You may also modify the value of the parameter with one of the suffixes:

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

@ -3963,6 +3963,7 @@
"input-fields-support-templatization": "Input fields support templatization.",
"link": "Link",
"link-required": "Link is required",
"link-max-length": "Link should be less than or equal to {{ length }} characters",
"link-type": {
"dashboard": "Open dashboard",
"link": "Open URL link"
@ -4167,6 +4168,7 @@
"checksum-copied-message": "Package checksum has been copied to clipboard",
"change-firmware": "Change of the firmware may cause update of { count, plural, =1 {1 device} other {# devices} }.",
"change-software": "Change of the software may cause update of { count, plural, =1 {1 device} other {# devices} }.",
"change-ota-setting-title": "Are you sure you want to change OTA settings?",
"chose-compatible-device-profile": "The uploaded package will be available only for devices with the chosen profile.",
"chose-firmware-distributed-device": "Choose firmware that will be distributed to the devices",
"chose-software-distributed-device": "Choose software that will be distributed to the devices",

24
ui-ngx/src/scss/animations.scss

@ -16,15 +16,23 @@
@keyframes tbMoveFromTopFade {
from {
opacity: 0;
transform: translate(0, -100%);
}
to {
opacity: 1;
transform: translate(0, 0);
}
}
@keyframes tbMoveToTopFade {
from {
opacity: 1;
transform: translate(0, 0);
}
to {
opacity: 0;
transform: translate(0, -100%);
}
}
@ -32,15 +40,23 @@
@keyframes tbMoveFromBottomFade {
from {
opacity: 0;
transform: translate(0, 100%);
}
to {
opacity: 1;
transform: translate(0, 0);
}
}
@keyframes tbMoveToBottomFade {
from {
opacity: 1;
transform: translate(0, 0);
}
to {
opacity: 0;
transform: translate(0, 150%);
}
}

Loading…
Cancel
Save