From 57024c223e7ef682fdb6c2cc4a89767c4186f4b0 Mon Sep 17 00:00:00 2001 From: Paolo Cristiani <42511852+pgrisu@users.noreply.github.com> Date: Tue, 13 May 2025 14:44:27 +0200 Subject: [PATCH 001/216] feat: add tooltip stacked total option in time series chart settings --- ...e-series-chart-basic-config.component.html | 5 +++ ...ime-series-chart-basic-config.component.ts | 10 ++++- .../chart/time-series-chart-tooltip.models.ts | 45 ++++++++++++++++++- .../lib/chart/time-series-chart.models.ts | 1 + ...eries-chart-widget-settings.component.html | 5 +++ ...-series-chart-widget-settings.component.ts | 9 +++- .../assets/locale/locale.constant-en_US.json | 1 + 7 files changed, 73 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html index 61c9f89c7c..8d1b5eaad7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html @@ -311,6 +311,11 @@ +
+ + {{ 'tooltip.show-stack-total' | translate }} + +
{{ 'tooltip.background-color' | translate }}
+
+ + {{ 'tooltip.show-stack-total' | translate }} + +
{{ 'tooltip.background-color' | translate }}
Date: Wed, 14 May 2025 18:11:20 +0200 Subject: [PATCH 002/216] Add show-total legend setting to latest-chart widgets --- .../basic/chart/latest-chart-basic-config.component.html | 3 +++ .../basic/chart/latest-chart-basic-config.component.ts | 4 ++++ .../components/widget/lib/chart/latest-chart.component.ts | 5 ++++- .../home/components/widget/lib/chart/latest-chart.models.ts | 2 ++ .../chart/latest-chart-widget-settings.component.html | 3 +++ .../settings/chart/latest-chart-widget-settings.component.ts | 3 +++ 6 files changed, 19 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html index 4a9569fb29..4601eaa4f9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html @@ -143,6 +143,9 @@
+ + {{ 'legend.show-total' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.ts index 9890895938..1a75f95d2f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.ts @@ -176,6 +176,7 @@ export abstract class LatestChartBasicConfigComponent !item.total); } legendLabelStyle: ComponentStyle; @@ -92,6 +93,7 @@ export class LatestChartComponent implements OnInit, OnDestroy, AfterViewInit { private shapeResize$: ResizeObserver; private legendHorizontal: boolean; + private legendShowTotal: boolean; private latestChart: TbLatestChart; @@ -119,6 +121,7 @@ export class LatestChartComponent implements OnInit, OnDestroy, AfterViewInit { this.legendValueStyle = textStyle(this.settings.legendValueFont); this.disabledLegendValueStyle = textStyle(this.settings.legendValueFont); this.legendValueStyle.color = this.settings.legendValueColor; + this.legendShowTotal = this.settings.legendShowTotal; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.models.ts index 931d3c52ed..9184bc59e7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.models.ts @@ -103,6 +103,7 @@ export interface LatestChartWidgetSettings extends LatestChartSettings { legendLabelColor: string; legendValueFont: Font; legendValueColor: string; + legendShowTotal: boolean; background: BackgroundSettings; padding: string; } @@ -129,6 +130,7 @@ export const latestChartWidgetDefaultSettings: LatestChartWidgetSettings = { lineHeight: '20px' }, legendValueColor: 'rgba(0, 0, 0, 0.87)', + legendShowTotal: true, background: { type: BackgroundType.color, color: '#fff', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html index 04476e69e2..5246747c20 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html @@ -61,6 +61,9 @@ + + {{ 'legend.show-total' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts index 1d7baebd33..b67a1d85f5 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.ts @@ -134,6 +134,7 @@ export abstract class LatestChartWidgetSettingsComponent Date: Thu, 10 Jul 2025 14:19:39 +0300 Subject: [PATCH 003/216] geofencing cf init commit --- ...CalculatedFieldEntityMessageProcessor.java | 27 +- .../service/cf/CalculatedFieldResult.java | 10 + ...faultCalculatedFieldProcessingService.java | 92 ++++++- .../service/cf/ctx/state/ArgumentEntry.java | 9 +- .../cf/ctx/state/ArgumentEntryType.java | 2 +- .../cf/ctx/state/CalculatedFieldCtx.java | 6 +- .../cf/ctx/state/CalculatedFieldState.java | 3 +- .../cf/ctx/state/GeofencingArgumentEntry.java | 85 ++++++ .../state/GeofencingCalculatedFieldState.java | 243 ++++++++++++++++++ .../ctx/state/ScriptCalculatedFieldState.java | 4 +- .../ctx/state/SimpleCalculatedFieldState.java | 4 +- .../server/utils/CalculatedFieldUtils.java | 4 + .../common/data/cf/CalculatedFieldType.java | 2 +- .../data/cf/configuration/Argument.java | 2 + .../CFArgumentDynamicSourceType.java | 22 ++ .../CalculatedFieldConfiguration.java | 3 +- .../CfArgumentDynamicSourceConfiguration.java | 39 +++ ...eofencingCalculatedFieldConfiguration.java | 31 +++ ...lationQueryDynamicSourceConfiguration.java | 57 ++++ .../util/geo/CirclePerimeterDefinition.java | 40 +++ .../common/util/geo/PerimeterDefinition.java | 39 +++ .../util/geo/PolygonPerimeterDefinition.java | 35 +++ 22 files changed, 739 insertions(+), 20 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java create mode 100644 application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java create mode 100644 common/util/src/main/java/org/thingsboard/common/util/geo/CirclePerimeterDefinition.java create mode 100644 common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinition.java create mode 100644 common/util/src/main/java/org/thingsboard/common/util/geo/PolygonPerimeterDefinition.java diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 35539834c3..75582ef4ba 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -288,17 +288,34 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM boolean stateSizeChecked = false; try { if (ctx.isInitialized() && state.isReady()) { - CalculatedFieldResult calculationResult = state.performCalculation(ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS); + List calculationResults = state.performCalculation(ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS); state.checkStateSize(ctxId, ctx.getMaxStateSize()); stateSizeChecked = true; if (state.isSizeOk()) { - if (!calculationResult.isEmpty()) { - cfService.pushMsgToRuleEngine(tenantId, entityId, calculationResult, cfIdList, callback); - } else { + if (calculationResults.isEmpty()) { callback.onSuccess(); + } else { + TbCallback effectiveCallback = calculationResults.size() > 1 ? + new MultipleTbCallback(calculationResults.size(), callback) : callback; + + for (CalculatedFieldResult calculationResult : calculationResults) { + if (calculationResult.isEmpty()) { + effectiveCallback.onSuccess(); + } else { + cfService.pushMsgToRuleEngine(tenantId, entityId, calculationResult, cfIdList, callback); + } + } } if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) { - systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, calculationResult.getResult().toString(), null); + if (calculationResults.isEmpty()) { + systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, + state.getArguments(), tbMsgId, tbMsgType, null, null); + } else { + for (CalculatedFieldResult calculationResult : calculationResults) { + systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, + state.getArguments(), tbMsgId, tbMsgType, calculationResult.getResultAsString(), null); + } + } } } } else { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java index 49acf6917c..7bec9ae964 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java @@ -34,4 +34,14 @@ public final class CalculatedFieldResult { (result.isTextual() && result.asText().isEmpty()); } + public String getResultAsString() { + if (result == null) { + return null; + } + if (result.isTextual()) { + return result.asText(); + } + return result.toString(); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index f2a6916751..9d75692718 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -32,12 +32,16 @@ import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.Aggregation; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; import org.thingsboard.server.common.data.kv.BasicTsKvEntry; @@ -55,6 +59,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto; @@ -70,6 +75,7 @@ import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.GeofencingCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; @@ -86,6 +92,10 @@ import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import static org.thingsboard.server.common.data.DataConstants.SCOPE; +import static org.thingsboard.server.service.cf.ctx.state.GeofencingCalculatedFieldState.ENTITY_ID_LATITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.service.cf.ctx.state.GeofencingCalculatedFieldState.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.service.cf.ctx.state.GeofencingCalculatedFieldState.RESTRICTED_ZONES_ARGUMENT_KEY; +import static org.thingsboard.server.service.cf.ctx.state.GeofencingCalculatedFieldState.SAVE_ZONES_ARGUMENT_KEY; import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto; @TbRuleEngineComponent @@ -99,6 +109,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP private final TbClusterService clusterService; private final ApiLimitService apiLimitService; private final PartitionService partitionService; + private final RelationService relationService; private ListeningExecutorService calculatedFieldCallbackExecutor; @@ -118,11 +129,29 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP @Override public ListenableFuture fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId) { Map> argFutures = new HashMap<>(); - for (var entry : ctx.getArguments().entrySet()) { - var argEntityId = entry.getValue().getRefEntityId() != null ? entry.getValue().getRefEntityId() : entityId; - var argValueFuture = fetchKvEntry(ctx.getTenantId(), argEntityId, entry.getValue()); - argFutures.put(entry.getKey(), argValueFuture); + + if (ctx.getCalculatedField().getType().equals(CalculatedFieldType.GEOFENCING)) { + // Ignoring any other arguments except ENTITY_ID_LATITUDE_ARGUMENT_KEY, + // ENTITY_ID_LONGITUDE_ARGUMENT_KEY, SAVE_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY. + for (var entry : ctx.getArguments().entrySet()) { + switch (entry.getKey()) { + case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> + argFutures.put(entry.getKey(), fetchKvEntry(ctx.getTenantId(), resolveEntityId(entityId, entry), entry.getValue())); + case SAVE_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY -> { + var resolvedEntityIdsFuture = resolveGeofencingEntityIds(ctx.getTenantId(), entityId, entry); + argFutures.put(entry.getKey(), Futures.transformAsync(resolvedEntityIdsFuture, resolvedEntityIds -> + fetchGeofencingKvEntry(ctx.getTenantId(), resolvedEntityIds, entry.getValue()), MoreExecutors.directExecutor())); + } + } + } + } else { + for (var entry : ctx.getArguments().entrySet()) { + var argEntityId = resolveEntityId(entityId, entry); + var argValueFuture = fetchKvEntry(ctx.getTenantId(), argEntityId, entry.getValue()); + argFutures.put(entry.getKey(), argValueFuture); + } } + return Futures.whenAllComplete(argFutures.values()).call(() -> { var result = createStateByType(ctx); result.updateState(ctx, argFutures.entrySet().stream() @@ -145,7 +174,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP public Map fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map arguments) { Map> argFutures = new HashMap<>(); for (var entry : arguments.entrySet()) { - var argEntityId = entry.getValue().getRefEntityId() != null ? entry.getValue().getRefEntityId() : entityId; + var argEntityId = resolveEntityId(entityId, entry); var argValueFuture = fetchKvEntry(tenantId, argEntityId, entry.getValue()); argFutures.put(entry.getKey(), argValueFuture); } @@ -241,6 +270,58 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP return builder.build(); } + + private EntityId resolveEntityId(EntityId entityId, Entry entry) { + return entry.getValue().getRefEntityId() != null ? entry.getValue().getRefEntityId() : entityId; + } + + private ListenableFuture> resolveGeofencingEntityIds(TenantId tenantId, EntityId entityId, Entry entry) { + Argument value = entry.getValue(); + if (value.getRefEntityId() != null) { + return Futures.immediateFuture(List.of(value.getRefEntityId())); + } + var refDynamicSource = value.getRefDynamicSource(); + if (refDynamicSource == null) { + return Futures.immediateFuture(List.of(entityId)); + } + return switch (value.getRefDynamicSource()) { + case RELATION_QUERY -> { + var relationQueryDynamicSourceConfiguration = (RelationQueryDynamicSourceConfiguration) value.getRefDynamicSourceConfiguration(); + yield Futures.transform(relationService.findByQuery(tenantId, relationQueryDynamicSourceConfiguration.toEntityRelationsQuery(entityId)), + relationQueryDynamicSourceConfiguration::resolveEntityIds, MoreExecutors.directExecutor()); + } + }; + } + + private ListenableFuture fetchGeofencingKvEntry(TenantId tenantId, List geofencingEntities, Argument argument) { + // TODO: Should we handle any other case? + if (argument.getRefEntityKey().getType() != ArgumentType.ATTRIBUTE) { + throw new IllegalStateException("Unsupported argument key type: " + argument.getRefEntityKey().getType()); + } + + List>> kvFutures = geofencingEntities.stream() + .map(entityId -> { + var attributesFuture = attributesService.find( + tenantId, + entityId, + argument.getRefEntityKey().getScope(), + argument.getRefEntityKey().getKey() + ); + return Futures.transform(attributesFuture, resultOpt -> + Map.entry(entityId, resultOpt.orElseGet(() -> + new BaseAttributeKvEntry(createDefaultKvEntry(argument), System.currentTimeMillis(), 0L))), + calculatedFieldCallbackExecutor + ); + }).collect(Collectors.toList()); + + ListenableFuture>> allFutures = Futures.allAsList(kvFutures); + + return Futures.transform(allFutures, entries -> ArgumentEntry.createGeofencingValueArgument(entries.stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))), + calculatedFieldCallbackExecutor + ); + } + private ListenableFuture fetchKvEntry(TenantId tenantId, EntityId entityId, Argument argument) { return switch (argument.getRefEntityKey().getType()) { case TS_ROLLING -> fetchTsRolling(tenantId, entityId, argument); @@ -301,6 +382,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP return switch (ctx.getCfType()) { case SIMPLE -> new SimpleCalculatedFieldState(ctx.getArgNames()); case SCRIPT -> new ScriptCalculatedFieldState(ctx.getArgNames()); + case GEOFENCING -> new GeofencingCalculatedFieldState(ctx.getArgNames()); }; } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java index 83e10b8194..c7f830431b 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java @@ -19,10 +19,12 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import java.util.List; +import java.util.Map; @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -31,7 +33,8 @@ import java.util.List; ) @JsonSubTypes({ @JsonSubTypes.Type(value = SingleValueArgumentEntry.class, name = "SINGLE_VALUE"), - @JsonSubTypes.Type(value = TsRollingArgumentEntry.class, name = "TS_ROLLING") + @JsonSubTypes.Type(value = TsRollingArgumentEntry.class, name = "TS_ROLLING"), + @JsonSubTypes.Type(value = GeofencingArgumentEntry.class, name = "GEOFENCING") }) public interface ArgumentEntry { @@ -58,4 +61,8 @@ public interface ArgumentEntry { return new TsRollingArgumentEntry(kvEntries, limit, timeWindow); } + static ArgumentEntry createGeofencingValueArgument(Map entityIdkvEntryMap) { + return new GeofencingArgumentEntry(entityIdkvEntryMap); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntryType.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntryType.java index 68f973c7c1..876bfa2a3f 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntryType.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntryType.java @@ -16,5 +16,5 @@ package org.thingsboard.server.service.cf.ctx.state; public enum ArgumentEntryType { - SINGLE_VALUE, TS_ROLLING + SINGLE_VALUE, TS_ROLLING, GEOFENCING } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 2e3321eece..89f76bd482 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -59,6 +59,7 @@ public class CalculatedFieldCtx { private final Map arguments; private final Map mainEntityArguments; private final Map> linkedEntityArguments; + private final Map dynamicEntityArguments; private final List argNames; private Output output; private String expression; @@ -84,10 +85,13 @@ public class CalculatedFieldCtx { this.arguments = configuration.getArguments(); this.mainEntityArguments = new HashMap<>(); this.linkedEntityArguments = new HashMap<>(); + this.dynamicEntityArguments = new HashMap<>(); for (Map.Entry entry : arguments.entrySet()) { var refId = entry.getValue().getRefEntityId(); var refKey = entry.getValue().getRefEntityKey(); - if (refId == null || refId.equals(calculatedField.getEntityId())) { + if (refId == null && entry.getValue().getRefDynamicSource() != null) { + dynamicEntityArguments.put(refKey, entry.getKey()); + } else if (refId == null || refId.equals(calculatedField.getEntityId())) { mainEntityArguments.put(refKey, entry.getKey()); } else { linkedEntityArguments.computeIfAbsent(refId, key -> new HashMap<>()).put(refKey, entry.getKey()); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java index 0de354bbb0..dc98ed836c 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java @@ -34,6 +34,7 @@ import java.util.Map; @JsonSubTypes({ @JsonSubTypes.Type(value = SimpleCalculatedFieldState.class, name = "SIMPLE"), @JsonSubTypes.Type(value = ScriptCalculatedFieldState.class, name = "SCRIPT"), + @JsonSubTypes.Type(value = GeofencingCalculatedFieldState.class, name = "GEOFENCING"), }) public interface CalculatedFieldState { @@ -48,7 +49,7 @@ public interface CalculatedFieldState { boolean updateState(CalculatedFieldCtx ctx, Map argumentValues); - ListenableFuture performCalculation(CalculatedFieldCtx ctx); + ListenableFuture> performCalculation(CalculatedFieldCtx ctx); @JsonIgnore boolean isReady(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java new file mode 100644 index 0000000000..51f5d4fd4f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java @@ -0,0 +1,85 @@ +/** + * 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.service.cf.ctx.state; + +import lombok.Data; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.geo.PerimeterDefinition; +import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.KvEntry; + +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +// TODO: implement +@Data +public class GeofencingArgumentEntry implements ArgumentEntry { + + private Map geofencingIdToPerimeter; + private boolean forceResetPrevious; + + public GeofencingArgumentEntry(Map entityIdKvEntryMap) { + this.geofencingIdToPerimeter = toPerimetersMap(entityIdKvEntryMap); + } + + @Override + public ArgumentEntryType getType() { + return ArgumentEntryType.GEOFENCING; + } + + @Override + public Object getValue() { + return geofencingIdToPerimeter; + } + + @Override + public boolean updateEntry(ArgumentEntry entry) { + if (!(entry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) { + throw new IllegalArgumentException("Unsupported argument entry type for geofencing argument entry: " + entry.getType()); + } + if (Objects.equals(this.geofencingIdToPerimeter, geofencingArgumentEntry.getGeofencingIdToPerimeter())) { + return false; // No change + } + this.geofencingIdToPerimeter = geofencingArgumentEntry.getGeofencingIdToPerimeter(); + return true; + } + + @Override + public boolean isEmpty() { + return geofencingIdToPerimeter == null || geofencingIdToPerimeter.isEmpty(); + } + + @Override + public TbelCfArg toTbelCfArg() { + return null; + } + + private Map toPerimetersMap(Map entityIdKvEntryMap) { + return entityIdKvEntryMap.entrySet().stream().map(entry -> { + if (entry.getValue().getJsonValue().isEmpty()) { + return null; + } + String rawPerimeterValue = entry.getValue().getJsonValue().get(); + PerimeterDefinition perimeter = JacksonUtil.fromString(rawPerimeterValue, PerimeterDefinition.class); + return Map.entry(entry.getKey(), perimeter); + }) + .filter(Objects::nonNull) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java new file mode 100644 index 0000000000..11853e7823 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java @@ -0,0 +1,243 @@ +/** + * 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.service.cf.ctx.state; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import lombok.Data; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.geo.Coordinates; +import org.thingsboard.common.util.geo.PerimeterDefinition; +import org.thingsboard.rule.engine.geo.EntityGeofencingState; +import org.thingsboard.rule.engine.util.GpsGeofencingEvents; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Data +public class GeofencingCalculatedFieldState implements CalculatedFieldState { + + public static final String ENTITY_ID_LATITUDE_ARGUMENT_KEY = "latitude"; + public static final String ENTITY_ID_LONGITUDE_ARGUMENT_KEY = "longitude"; + public static final String SAVE_ZONES_ARGUMENT_KEY = "saveZones"; + public static final String RESTRICTED_ZONES_ARGUMENT_KEY = "restrictedZones"; + + private List requiredArguments; + private Map arguments; + private long latestTimestamp = -1; + + private Map saveZoneStates; + private Map restrictedZoneStates; + + public GeofencingCalculatedFieldState() { + this(List.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, SAVE_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY)); + } + + public GeofencingCalculatedFieldState(List argNames) { + this.requiredArguments = argNames; + this.arguments = new HashMap<>(); + this.saveZoneStates = new HashMap<>(); + this.restrictedZoneStates = new HashMap<>(); + } + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.GEOFENCING; + } + + @Override + public boolean updateState(CalculatedFieldCtx ctx, Map argumentValues) { + // TODO: Do I need to check argument for null? + if (arguments == null) { + arguments = new HashMap<>(); + } + + boolean stateUpdated = false; + + for (Map.Entry entry : argumentValues.entrySet()) { + String key = entry.getKey(); + ArgumentEntry newEntry = entry.getValue(); + + // TODO: Do I need to check argument size? + // checkArgumentSize(key, newEntry, ctx); + + ArgumentEntry existingEntry = arguments.get(key); + boolean entryUpdated; + + // TODO: What is force reset previos? + // if (existingEntry == null || newEntry.isForceResetPrevious()) { + + // fresh start of state. No entry exists yet. + if (existingEntry == null) { + switch (key) { + case ENTITY_ID_LATITUDE_ARGUMENT_KEY: + case ENTITY_ID_LONGITUDE_ARGUMENT_KEY: + if (!(newEntry instanceof SingleValueArgumentEntry singleValueArgumentEntry)) { + throw new IllegalArgumentException(key + " argument must be a single value argument."); + } + arguments.put(key, singleValueArgumentEntry); + entryUpdated = true; + break; + case SAVE_ZONES_ARGUMENT_KEY: + case RESTRICTED_ZONES_ARGUMENT_KEY: + if (!(newEntry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) { + throw new IllegalArgumentException(key + " argument must be a geofencing argument entry."); + } + arguments.put(key, geofencingArgumentEntry); + entryUpdated = true; + break; + default: + throw new IllegalArgumentException("Unsupported argument: " + key); + } + } else { + entryUpdated = switch (key) { + case ENTITY_ID_LATITUDE_ARGUMENT_KEY, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> existingEntry.updateEntry(newEntry); + case SAVE_ZONES_ARGUMENT_KEY, + RESTRICTED_ZONES_ARGUMENT_KEY -> { + // TODO: ensure zone cleanup working correctly. + boolean updated = existingEntry.updateEntry(newEntry); + if (updated) { + Map currentStates = + key.equals(SAVE_ZONES_ARGUMENT_KEY) ? saveZoneStates : restrictedZoneStates; + Set newZoneIds = ((GeofencingArgumentEntry) newEntry).getGeofencingIdToPerimeter().keySet(); + currentStates.keySet().removeIf(existingZoneId -> !newZoneIds.contains(existingZoneId)); + } + yield updated; + } + default -> throw new IllegalStateException("Unsupported argument: " + key); + }; + } + + if (entryUpdated) { + stateUpdated = true; + updateLastUpdateTimestamp(newEntry); + } + } + return stateUpdated; + } + + + @Override + public ListenableFuture> performCalculation(CalculatedFieldCtx ctx) { + List savedZonesStatesResults = updateSavedGeofencingZonesState(ctx); + List restrictedZonesStatesResults = updateRestrictedGeofencingZonesState(ctx); + + List allZoneStatesResults = + new ArrayList<>(savedZonesStatesResults.size() + restrictedZonesStatesResults.size()); + allZoneStatesResults.addAll(savedZonesStatesResults); + allZoneStatesResults.addAll(restrictedZonesStatesResults); + + return Futures.immediateFuture(allZoneStatesResults); + } + + @Override + public boolean isReady() { + return arguments.keySet().containsAll(requiredArguments) && + arguments.values().stream().noneMatch(ArgumentEntry::isEmpty); + } + + // TODO: implement + @Override + public boolean isSizeExceedsLimit() { + return false; + } + + // TODO: implement + @Override + public void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize) { + + } + + // TODO: implement + @Override + public void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx) { + + } + + private void updateLastUpdateTimestamp(ArgumentEntry entry) { + long newTs = this.latestTimestamp; + if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { + newTs = singleValueArgumentEntry.getTs(); + } + this.latestTimestamp = Math.max(this.latestTimestamp, newTs); + } + + private List updateSavedGeofencingZonesState(CalculatedFieldCtx ctx) { + return updateGeofencingZonesState(ctx, saveZoneStates, false); + } + + private List updateRestrictedGeofencingZonesState(CalculatedFieldCtx ctx) { + return updateGeofencingZonesState(ctx, restrictedZoneStates, true); + } + + // TODO: Ensure all cases are covered based on rule node logic. + private List updateGeofencingZonesState(CalculatedFieldCtx ctx, Map zoneStates, boolean restricted) { + var results = new ArrayList(); + + long stateSwitchTime = System.currentTimeMillis(); + double latitude = (double) arguments.get(ENTITY_ID_LATITUDE_ARGUMENT_KEY).getValue(); + double longitude = (double) arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY).getValue(); + Coordinates entityCoordinates = new Coordinates(latitude, longitude); + + String zoneKey = restricted ? RESTRICTED_ZONES_ARGUMENT_KEY : SAVE_ZONES_ARGUMENT_KEY; + GeofencingArgumentEntry zonesEntry = (GeofencingArgumentEntry) arguments.get(zoneKey); + + for (Map.Entry entry : zonesEntry.getGeofencingIdToPerimeter().entrySet()) { + EntityId zoneId = entry.getKey(); + PerimeterDefinition perimeter = entry.getValue(); + + boolean inside = perimeter.checkMatches(entityCoordinates); + + // Always present or created + EntityGeofencingState state = zoneStates.computeIfAbsent( + zoneId, id -> new EntityGeofencingState(false, 0L, false) + ); + + String event; + if (state.getStateSwitchTime() == 0L || state.isInside() != inside) { + // First state or transition (entered/left) + state.setInside(inside); + state.setStateSwitchTime(stateSwitchTime); + state.setStayed(false); + + event = inside ? GpsGeofencingEvents.ENTERED : GpsGeofencingEvents.LEFT; + } else { + // No transition + event = inside ? GpsGeofencingEvents.INSIDE : GpsGeofencingEvents.OUTSIDE; + } + + ObjectNode stateNode = JacksonUtil.newObjectNode(); + stateNode.put("entityId", ctx.getEntityId().toString()); + stateNode.put("zoneId", zoneId.getId().toString()); + stateNode.put("restricted", restricted); + stateNode.put("event", event); + + results.add(new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), stateNode)); + } + + return results; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java index 84dce627ae..b1095cf13f 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java @@ -53,7 +53,7 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState { } @Override - public ListenableFuture performCalculation(CalculatedFieldCtx ctx) { + public ListenableFuture> performCalculation(CalculatedFieldCtx ctx) { Map arguments = new LinkedHashMap<>(); List args = new ArrayList<>(ctx.getArgNames().size() + 1); args.add(new Object()); // first element is a ctx, but we will set it later; @@ -70,7 +70,7 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState { ListenableFuture resultFuture = ctx.getCalculatedFieldScriptEngine().executeJsonAsync(args.toArray()); Output output = ctx.getOutput(); return Futures.transform(resultFuture, - result -> new CalculatedFieldResult(output.getType(), output.getScope(), result), + result -> List.of(new CalculatedFieldResult(output.getType(), output.getScope(), result)), MoreExecutors.directExecutor() ); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java index 577ff80219..6a5ddb3c70 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java @@ -52,7 +52,7 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { } @Override - public ListenableFuture performCalculation(CalculatedFieldCtx ctx) { + public ListenableFuture> performCalculation(CalculatedFieldCtx ctx) { var expr = ctx.getCustomExpression().get(); for (Map.Entry entry : this.arguments.entrySet()) { @@ -76,7 +76,7 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { Object result = formatResult(expressionResult, output.getDecimalsByDefault()); JsonNode outputResult = createResultJson(ctx.isUseLatestTs(), output.getName(), result); - return Futures.immediateFuture(new CalculatedFieldResult(output.getType(), output.getScope(), outputResult)); + return Futures.immediateFuture(List.of(new CalculatedFieldResult(output.getType(), output.getScope(), outputResult))); } private Object formatResult(double expressionResult, Integer decimals) { diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java index 77080c28c8..d9a6248b96 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java @@ -32,6 +32,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.TsRollingArgumentPro import org.thingsboard.server.gen.transport.TransportProtos.TsValueProto; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.GeofencingCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; @@ -118,8 +119,11 @@ public class CalculatedFieldUtils { CalculatedFieldState state = switch (type) { case SIMPLE -> new SimpleCalculatedFieldState(); case SCRIPT -> new ScriptCalculatedFieldState(); + case GEOFENCING -> new GeofencingCalculatedFieldState(); }; + // TODO: add logic to restore geofencing state from proto + proto.getSingleValueArgumentsList().forEach(argProto -> state.getArguments().put(argProto.getArgName(), fromSingleValueArgumentProto(argProto))); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java index acef67a041..d4dd2c5812 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/CalculatedFieldType.java @@ -17,6 +17,6 @@ package org.thingsboard.server.common.data.cf; public enum CalculatedFieldType { - SIMPLE, SCRIPT + SIMPLE, SCRIPT, GEOFENCING } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java index e7daa70b1b..6fc8c46961 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java @@ -26,6 +26,8 @@ public class Argument { @Nullable private EntityId refEntityId; + private CFArgumentDynamicSourceType refDynamicSource; + private CfArgumentDynamicSourceConfiguration refDynamicSourceConfiguration; private ReferencedEntityKey refEntityKey; private String defaultValue; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java new file mode 100644 index 0000000000..bd2e9b0c00 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java @@ -0,0 +1,22 @@ +/** + * 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.common.data.cf.configuration; + +public enum CFArgumentDynamicSourceType { + + RELATION_QUERY + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java index ad3d4373ad..b713f9030d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java @@ -35,7 +35,8 @@ import java.util.Map; ) @JsonSubTypes({ @JsonSubTypes.Type(value = SimpleCalculatedFieldConfiguration.class, name = "SIMPLE"), - @JsonSubTypes.Type(value = ScriptCalculatedFieldConfiguration.class, name = "SCRIPT") + @JsonSubTypes.Type(value = ScriptCalculatedFieldConfiguration.class, name = "SCRIPT"), + @JsonSubTypes.Type(value = GeofencingCalculatedFieldConfiguration.class, name = "GEOFENCING") }) @JsonIgnoreProperties(ignoreUnknown = true) public interface CalculatedFieldConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java new file mode 100644 index 0000000000..3fe432917b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java @@ -0,0 +1,39 @@ +/** + * 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.common.data.cf.configuration; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = RelationQueryDynamicSourceConfiguration.class, name = "RELATION_QUERY"), +}) +@JsonIgnoreProperties(ignoreUnknown = true) +public interface CfArgumentDynamicSourceConfiguration { + + @JsonIgnore + CFArgumentDynamicSourceType getType(); + + default void validate() {} + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java new file mode 100644 index 0000000000..6c1450d713 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java @@ -0,0 +1,31 @@ +/** + * 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.common.data.cf.configuration; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; + +@Data +@EqualsAndHashCode(callSuper = true) +public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements CalculatedFieldConfiguration { + + @Override + public CalculatedFieldType getType() { + return CalculatedFieldType.GEOFENCING; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java new file mode 100644 index 0000000000..219fd068cd --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java @@ -0,0 +1,57 @@ +/** + * 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.common.data.cf.configuration; + +import lombok.Data; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationsQuery; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationsSearchParameters; + +import java.util.Collections; +import java.util.List; + +@Data +public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration { + + private int maxLevel; + private EntitySearchDirection direction; + private String relationType; + private List profiles; + + @Override + public CFArgumentDynamicSourceType getType() { + return CFArgumentDynamicSourceType.RELATION_QUERY; + } + + public EntityRelationsQuery toEntityRelationsQuery(EntityId rootEntityId) { + var entityRelationsQuery = new EntityRelationsQuery(); + entityRelationsQuery.setParameters(new RelationsSearchParameters(rootEntityId, direction, maxLevel, false)); + entityRelationsQuery.setFilters(Collections.singletonList(new RelationEntityTypeFilter(relationType, profiles))); + return entityRelationsQuery; + } + + public List resolveEntityIds(List relations) { + return switch (direction) { + case FROM -> relations.stream().map(EntityRelation::getTo).toList(); + case TO -> relations.stream().map(EntityRelation::getFrom).toList(); + }; + } + +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/CirclePerimeterDefinition.java b/common/util/src/main/java/org/thingsboard/common/util/geo/CirclePerimeterDefinition.java new file mode 100644 index 0000000000..33035b016e --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/CirclePerimeterDefinition.java @@ -0,0 +1,40 @@ +/** + * 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.common.util.geo; + +import lombok.Data; + +@Data +public class CirclePerimeterDefinition implements PerimeterDefinition { + + private Double centerLatitude; + private Double centerLongitude; + private Double range; + private RangeUnit rangeUnit; + + @Override + public PerimeterType getType() { + return PerimeterType.CIRCLE; + } + + @Override + public boolean checkMatches(Coordinates entityCoordinates) { + Coordinates perimeterCoordinates = new Coordinates(centerLatitude, centerLongitude); + return range > GeoUtil.distance(entityCoordinates, perimeterCoordinates, rangeUnit); + } + + +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinition.java b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinition.java new file mode 100644 index 0000000000..68191f1a17 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinition.java @@ -0,0 +1,39 @@ +/** + * 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.common.util.geo; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import java.io.Serializable; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = PolygonPerimeterDefinition.class, name = "POLYGON"), + @JsonSubTypes.Type(value = CirclePerimeterDefinition.class, name = "CIRCLE")}) +@JsonIgnoreProperties(ignoreUnknown = true) +public interface PerimeterDefinition extends Serializable { + + @JsonIgnore + PerimeterType getType(); + + boolean checkMatches(Coordinates entityCoordinates); +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/PolygonPerimeterDefinition.java b/common/util/src/main/java/org/thingsboard/common/util/geo/PolygonPerimeterDefinition.java new file mode 100644 index 0000000000..b2259b5b07 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/PolygonPerimeterDefinition.java @@ -0,0 +1,35 @@ +/** + * 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.common.util.geo; + +import lombok.Data; + +@Data +public class PolygonPerimeterDefinition implements PerimeterDefinition { + + private String polygonsDefinition; + + @Override + public PerimeterType getType() { + return PerimeterType.POLYGON; + } + + @Override + public boolean checkMatches(Coordinates entityCoordinates) { + return GeoUtil.contains(polygonsDefinition, entityCoordinates); + } + +} From 3491419dd9c552c9083ddc03720cb834a990c650 Mon Sep 17 00:00:00 2001 From: Ekaterina Chantsova Date: Mon, 14 Jul 2025 17:37:31 +0300 Subject: [PATCH 004/216] Timewindow: remove hide parameters from configuration if they are disabled --- .../timewindow-config-dialog.component.ts | 32 +++++++++++ .../src/app/shared/models/time/time.models.ts | 56 ++++++++++--------- 2 files changed, 61 insertions(+), 27 deletions(-) diff --git a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts index 1e1f831c4f..7d69f99603 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts @@ -506,6 +506,38 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On delete this.timewindow.history.disableCustomGroupInterval; } + if (!this.timewindow.hideAggregation) { + delete this.timewindow.hideAggregation; + } + if (!this.timewindow.hideAggInterval) { + delete this.timewindow.hideAggInterval; + } + if (!this.timewindow.hideTimezone) { + delete this.timewindow.hideTimezone; + } + + if (!this.timewindow.realtime.hideInterval) { + delete this.timewindow.realtime.hideInterval; + } + if (!this.timewindow.realtime.hideLastInterval) { + delete this.timewindow.realtime.hideLastInterval; + } + if (!this.timewindow.realtime.hideQuickInterval) { + delete this.timewindow.realtime.hideQuickInterval; + } + if (!this.timewindow.history.hideInterval) { + delete this.timewindow.history.hideInterval; + } + if (!this.timewindow.history.hideLastInterval) { + delete this.timewindow.history.hideLastInterval; + } + if (!this.timewindow.history.hideFixedInterval) { + delete this.timewindow.history.hideFixedInterval; + } + if (!this.timewindow.history.hideQuickInterval) { + delete this.timewindow.history.hideQuickInterval; + } + if (!this.aggregation) { delete this.timewindow.aggregation; } diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index c204cb6867..5442cb95ca 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -281,19 +281,12 @@ export const historyInterval = (timewindowMs: number): Timewindow => ({ export const defaultTimewindow = (timeService: TimeService): Timewindow => { const currentTime = moment().valueOf(); return { - displayValue: '', - hideAggregation: false, - hideAggInterval: false, - hideTimezone: false, selectedTab: TimewindowType.REALTIME, realtime: { realtimeType: RealtimeWindowType.LAST_INTERVAL, interval: SECOND, timewindowMs: MINUTE, quickInterval: QuickTimeInterval.CURRENT_DAY, - hideInterval: false, - hideLastInterval: false, - hideQuickInterval: false }, history: { historyType: HistoryWindowType.LAST_INTERVAL, @@ -304,10 +297,6 @@ export const defaultTimewindow = (timeService: TimeService): Timewindow => { endTimeMs: currentTime }, quickInterval: QuickTimeInterval.CURRENT_DAY, - hideInterval: false, - hideLastInterval: false, - hideFixedInterval: false, - hideQuickInterval: false }, aggregation: { type: AggregationType.AVG, @@ -331,34 +320,41 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO if (value.allowedAggTypes?.length) { model.allowedAggTypes = value.allowedAggTypes; } - model.hideAggregation = value.hideAggregation; - model.hideAggInterval = value.hideAggInterval; - model.hideTimezone = value.hideTimezone; + if (value.hideAggregation) { + model.hideAggregation = value.hideAggregation; + } + if (value.hideAggInterval) { + model.hideAggInterval = value.hideAggInterval; + } + if (value.hideTimezone) { + model.hideTimezone = value.hideTimezone; + } + model.selectedTab = getTimewindowType(value); // for backward compatibility - if (isDefinedAndNotNull((value as any).hideInterval)) { + if ((value as any).hideInterval) { model.realtime.hideInterval = (value as any).hideInterval; model.history.hideInterval = (value as any).hideInterval; delete (value as any).hideInterval; } - if (isDefinedAndNotNull((value as any).hideLastInterval)) { + if ((value as any).hideLastInterval) { model.realtime.hideLastInterval = (value as any).hideLastInterval; delete (value as any).hideLastInterval; } - if (isDefinedAndNotNull((value as any).hideQuickInterval)) { + if ((value as any).hideQuickInterval) { model.realtime.hideQuickInterval = (value as any).hideQuickInterval; delete (value as any).hideQuickInterval; } if (isDefined(value.realtime)) { - if (isDefinedAndNotNull(value.realtime.hideInterval)) { + if (value.realtime.hideInterval) { model.realtime.hideInterval = value.realtime.hideInterval; } - if (isDefinedAndNotNull(value.realtime.hideLastInterval)) { + if (value.realtime.hideLastInterval) { model.realtime.hideLastInterval = value.realtime.hideLastInterval; } - if (isDefinedAndNotNull(value.realtime.hideQuickInterval)) { + if (value.realtime.hideQuickInterval) { model.realtime.hideQuickInterval = value.realtime.hideQuickInterval; } if (value.realtime.disableCustomInterval) { @@ -392,16 +388,16 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO } } if (isDefined(value.history)) { - if (isDefinedAndNotNull(value.history.hideInterval)) { + if (value.history.hideInterval) { model.history.hideInterval = value.history.hideInterval; } - if (isDefinedAndNotNull(value.history.hideLastInterval)) { + if (value.history.hideLastInterval) { model.history.hideLastInterval = value.history.hideLastInterval; } - if (isDefinedAndNotNull(value.history.hideFixedInterval)) { + if (value.history.hideFixedInterval) { model.history.hideFixedInterval = value.history.hideFixedInterval; } - if (isDefinedAndNotNull(value.history.hideQuickInterval)) { + if (value.history.hideQuickInterval) { model.history.hideQuickInterval = value.history.hideQuickInterval; } if (value.history.disableCustomInterval) { @@ -1098,9 +1094,15 @@ export const cloneSelectedTimewindow = (timewindow: Timewindow): Timewindow => { if (timewindow.allowedAggTypes?.length) { cloned.allowedAggTypes = timewindow.allowedAggTypes; } - cloned.hideAggregation = timewindow.hideAggregation || false; - cloned.hideAggInterval = timewindow.hideAggInterval || false; - cloned.hideTimezone = timewindow.hideTimezone || false; + if (timewindow.hideAggregation) { + cloned.hideAggregation = timewindow.hideAggregation; + } + if (timewindow.hideAggInterval) { + cloned.hideAggInterval = timewindow.hideAggInterval; + } + if (timewindow.hideTimezone) { + cloned.hideTimezone = timewindow.hideTimezone; + } if (isDefined(timewindow.selectedTab)) { cloned.selectedTab = timewindow.selectedTab; } From 04eefbc29b369a4a84f3422322a47bc3d91cd77a Mon Sep 17 00:00:00 2001 From: dshvaika Date: Fri, 18 Jul 2025 12:35:34 +0300 Subject: [PATCH 005/216] updated tests due to changed method results for cf calculation --- .../state/ScriptCalculatedFieldStateTest.java | 6 ++++-- .../state/SimpleCalculatedFieldStateTest.java | 16 ++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java index 91eced64f6..15770d80f2 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java @@ -44,6 +44,7 @@ import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.service.cf.CalculatedFieldResult; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.UUID; @@ -124,9 +125,10 @@ public class ScriptCalculatedFieldStateTest { void testPerformCalculation() throws ExecutionException, InterruptedException { state.arguments = new HashMap<>(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry)); - CalculatedFieldResult result = state.performCalculation(ctx).get(); + List resultList = state.performCalculation(ctx).get(); - assertThat(result).isNotNull(); + assertThat(resultList).isNotNull().hasSize(1); + CalculatedFieldResult result = resultList.get(0); Output output = getCalculatedFieldConfig().getOutput(); assertThat(result.getType()).isEqualTo(output.getType()); assertThat(result.getScope()).isEqualTo(output.getScope()); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java index c1616a85db..b20abf8cdd 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java @@ -42,6 +42,7 @@ import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.service.cf.CalculatedFieldResult; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -134,9 +135,10 @@ public class SimpleCalculatedFieldStateTest { "key3", key3ArgEntry )); - CalculatedFieldResult result = state.performCalculation(ctx).get(); + List resultList = state.performCalculation(ctx).get(); - assertThat(result).isNotNull(); + assertThat(resultList).isNotNull().hasSize(1); + CalculatedFieldResult result = resultList.get(0); Output output = getCalculatedFieldConfig().getOutput(); assertThat(result.getType()).isEqualTo(output.getType()); assertThat(result.getScope()).isEqualTo(output.getScope()); @@ -164,9 +166,10 @@ public class SimpleCalculatedFieldStateTest { "key3", key3ArgEntry )); - CalculatedFieldResult result = state.performCalculation(ctx).get(); + List resultList = state.performCalculation(ctx).get(); - assertThat(result).isNotNull(); + assertThat(resultList).isNotNull().hasSize(1); + CalculatedFieldResult result = resultList.get(0); Output output = getCalculatedFieldConfig().getOutput(); assertThat(result.getType()).isEqualTo(output.getType()); assertThat(result.getScope()).isEqualTo(output.getScope()); @@ -185,9 +188,10 @@ public class SimpleCalculatedFieldStateTest { output.setDecimalsByDefault(3); ctx.setOutput(output); - CalculatedFieldResult result = state.performCalculation(ctx).get(); + List resultList = state.performCalculation(ctx).get(); - assertThat(result).isNotNull(); + assertThat(resultList).isNotNull().hasSize(1); + CalculatedFieldResult result = resultList.get(0); assertThat(result.getType()).isEqualTo(output.getType()); assertThat(result.getScope()).isEqualTo(output.getScope()); assertThat(result.getResult()).isEqualTo(JacksonUtil.valueToTree(Map.of("output", 49.546))); From 083078b7f584f20567668fc16ba2a58d27beb72d Mon Sep 17 00:00:00 2001 From: Ekaterina Chantsova Date: Fri, 18 Jul 2025 18:56:02 +0300 Subject: [PATCH 006/216] Timewindow: leave only selected realtime/history and aggregation parameters for saving and remove others from configuration --- .../core/services/dashboard-utils.service.ts | 3 +- .../timewindow-config-dialog.component.ts | 104 +++++++++--------- .../time/timewindow-panel.component.ts | 100 +++++++++-------- .../components/time/timewindow.component.ts | 3 +- .../src/app/shared/models/time/time.models.ts | 87 +++++++++++++-- 5 files changed, 192 insertions(+), 105 deletions(-) diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index 1511840c57..ecb3e65c61 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -295,7 +295,8 @@ export class DashboardUtilsService { widgetConfig.datasources = this.validateAndUpdateDatasources(widgetConfig.datasources); if (type === widgetType.latest) { const onlyHistoryTimewindow = datasourcesHasOnlyComparisonAggregation(widgetConfig.datasources); - widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true, onlyHistoryTimewindow, this.timeService); + widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true, + onlyHistoryTimewindow, this.timeService, false); } else if (type === widgetType.rpc) { if (widgetConfig.targetDeviceAliasIds && widgetConfig.targetDeviceAliasIds.length) { widgetConfig.targetDevice = { diff --git a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts index 7d69f99603..07e532cac0 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts @@ -152,73 +152,73 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On this.timewindowForm = this.fb.group({ selectedTab: [isDefined(this.timewindow.selectedTab) ? this.timewindow.selectedTab : TimewindowType.REALTIME], realtime: this.fb.group({ - realtimeType: [ isDefined(realtime?.realtimeType) ? this.timewindow.realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL ], - timewindowMs: [ isDefined(realtime?.timewindowMs) ? this.timewindow.realtime.timewindowMs : null ], - interval: [ isDefined(realtime?.interval) ? this.timewindow.realtime.interval : null ], - quickInterval: [ isDefined(realtime?.quickInterval) ? this.timewindow.realtime.quickInterval : null ], - disableCustomInterval: [ isDefinedAndNotNull(this.timewindow.realtime?.disableCustomInterval) - ? this.timewindow.realtime?.disableCustomInterval : false ], - disableCustomGroupInterval: [ isDefinedAndNotNull(this.timewindow.realtime?.disableCustomGroupInterval) - ? this.timewindow.realtime?.disableCustomGroupInterval : false ], - hideInterval: [ isDefinedAndNotNull(this.timewindow.realtime.hideInterval) - ? this.timewindow.realtime.hideInterval : false ], + realtimeType: [ isDefined(realtime?.realtimeType) ? realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL ], + timewindowMs: [ isDefined(realtime?.timewindowMs) ? realtime.timewindowMs : null ], + interval: [ isDefined(realtime?.interval) ? realtime.interval : null ], + quickInterval: [ isDefined(realtime?.quickInterval) ? realtime.quickInterval : null ], + disableCustomInterval: [ isDefinedAndNotNull(realtime?.disableCustomInterval) + ? realtime.disableCustomInterval : false ], + disableCustomGroupInterval: [ isDefinedAndNotNull(realtime?.disableCustomGroupInterval) + ? realtime.disableCustomGroupInterval : false ], + hideInterval: [ isDefinedAndNotNull(realtime?.hideInterval) + ? realtime.hideInterval : false ], hideLastInterval: [{ - value: isDefinedAndNotNull(this.timewindow.realtime.hideLastInterval) - ? this.timewindow.realtime.hideLastInterval : false, - disabled: this.timewindow.realtime.hideInterval + value: isDefinedAndNotNull(realtime?.hideLastInterval) + ? realtime.hideLastInterval : false, + disabled: realtime?.hideInterval }], hideQuickInterval: [{ - value: isDefinedAndNotNull(this.timewindow.realtime.hideQuickInterval) - ? this.timewindow.realtime.hideQuickInterval : false, - disabled: this.timewindow.realtime.hideInterval + value: isDefinedAndNotNull(realtime?.hideQuickInterval) + ? realtime.hideQuickInterval : false, + disabled: realtime?.hideInterval }], advancedParams: this.fb.group({ - allowedLastIntervals: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.allowedLastIntervals) - ? this.timewindow.realtime.advancedParams.allowedLastIntervals : null ], - allowedQuickIntervals: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.allowedQuickIntervals) - ? this.timewindow.realtime.advancedParams.allowedQuickIntervals : null ], - lastAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.lastAggIntervalsConfig) - ? this.timewindow.realtime.advancedParams.lastAggIntervalsConfig : null ], - quickAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.realtime?.advancedParams?.quickAggIntervalsConfig) - ? this.timewindow.realtime.advancedParams.quickAggIntervalsConfig : null ] + allowedLastIntervals: [ isDefinedAndNotNull(realtime?.advancedParams?.allowedLastIntervals) + ? realtime.advancedParams.allowedLastIntervals : null ], + allowedQuickIntervals: [ isDefinedAndNotNull(realtime?.advancedParams?.allowedQuickIntervals) + ? realtime.advancedParams.allowedQuickIntervals : null ], + lastAggIntervalsConfig: [ isDefinedAndNotNull(realtime?.advancedParams?.lastAggIntervalsConfig) + ? realtime.advancedParams.lastAggIntervalsConfig : null ], + quickAggIntervalsConfig: [ isDefinedAndNotNull(realtime?.advancedParams?.quickAggIntervalsConfig) + ? realtime.advancedParams.quickAggIntervalsConfig : null ] }) }), history: this.fb.group({ - historyType: [ isDefined(history?.historyType) ? this.timewindow.history.historyType : HistoryWindowType.LAST_INTERVAL ], - timewindowMs: [ isDefined(history?.timewindowMs) ? this.timewindow.history.timewindowMs : null ], - interval: [ isDefined(history?.interval) ? this.timewindow.history.interval : null ], - fixedTimewindow: [ isDefined(history?.fixedTimewindow) ? this.timewindow.history.fixedTimewindow : null ], - quickInterval: [ isDefined(history?.quickInterval) ? this.timewindow.history.quickInterval : null ], - disableCustomInterval: [ isDefinedAndNotNull(this.timewindow.history?.disableCustomInterval) - ? this.timewindow.history?.disableCustomInterval : false ], - disableCustomGroupInterval: [ isDefinedAndNotNull(this.timewindow.history?.disableCustomGroupInterval) - ? this.timewindow.history?.disableCustomGroupInterval : false ], - hideInterval: [ isDefinedAndNotNull(this.timewindow.history.hideInterval) - ? this.timewindow.history.hideInterval : false ], + historyType: [ isDefined(history?.historyType) ? history.historyType : HistoryWindowType.LAST_INTERVAL ], + timewindowMs: [ isDefined(history?.timewindowMs) ? history.timewindowMs : null ], + interval: [ isDefined(history?.interval) ? history.interval : null ], + fixedTimewindow: [ isDefined(history?.fixedTimewindow) ? history.fixedTimewindow : null ], + quickInterval: [ isDefined(history?.quickInterval) ? history.quickInterval : null ], + disableCustomInterval: [ isDefinedAndNotNull(history?.disableCustomInterval) + ? history.disableCustomInterval : false ], + disableCustomGroupInterval: [ isDefinedAndNotNull(history?.disableCustomGroupInterval) + ? history.disableCustomGroupInterval : false ], + hideInterval: [ isDefinedAndNotNull(history?.hideInterval) + ? history.hideInterval : false ], hideLastInterval: [{ - value: isDefinedAndNotNull(this.timewindow.history.hideLastInterval) - ? this.timewindow.history.hideLastInterval : false, - disabled: this.timewindow.history.hideInterval + value: isDefinedAndNotNull(history?.hideLastInterval) + ? history.hideLastInterval : false, + disabled: history?.hideInterval }], hideQuickInterval: [{ - value: isDefinedAndNotNull(this.timewindow.history.hideQuickInterval) - ? this.timewindow.history.hideQuickInterval : false, - disabled: this.timewindow.history.hideInterval + value: isDefinedAndNotNull(history?.hideQuickInterval) + ? history.hideQuickInterval : false, + disabled: history?.hideInterval }], hideFixedInterval: [{ - value: isDefinedAndNotNull(this.timewindow.history.hideFixedInterval) - ? this.timewindow.history.hideFixedInterval : false, - disabled: this.timewindow.history.hideInterval + value: isDefinedAndNotNull(history?.hideFixedInterval) + ? history.hideFixedInterval : false, + disabled: history?.hideInterval }], advancedParams: this.fb.group({ - allowedLastIntervals: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.allowedLastIntervals) - ? this.timewindow.history.advancedParams.allowedLastIntervals : null ], - allowedQuickIntervals: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.allowedQuickIntervals) - ? this.timewindow.history.advancedParams.allowedQuickIntervals : null ], - lastAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.lastAggIntervalsConfig) - ? this.timewindow.history.advancedParams.lastAggIntervalsConfig : null ], - quickAggIntervalsConfig: [ isDefinedAndNotNull(this.timewindow.history?.advancedParams?.quickAggIntervalsConfig) - ? this.timewindow.history.advancedParams.quickAggIntervalsConfig : null ] + allowedLastIntervals: [ isDefinedAndNotNull(history?.advancedParams?.allowedLastIntervals) + ? history.advancedParams.allowedLastIntervals : null ], + allowedQuickIntervals: [ isDefinedAndNotNull(history?.advancedParams?.allowedQuickIntervals) + ? history.advancedParams.allowedQuickIntervals : null ], + lastAggIntervalsConfig: [ isDefinedAndNotNull(history?.advancedParams?.lastAggIntervalsConfig) + ? history.advancedParams.lastAggIntervalsConfig : null ], + quickAggIntervalsConfig: [ isDefinedAndNotNull(history?.advancedParams?.quickAggIntervalsConfig) + ? history.advancedParams.quickAggIntervalsConfig : null ] }) }), aggregation: this.fb.group({ diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts index d7a0d44013..9831ec0599 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts @@ -27,12 +27,14 @@ import { } from '@angular/core'; import { AggregationType, + clearTimewindowConfig, currentHistoryTimewindow, currentRealtimeTimewindow, historyAllowedAggIntervals, HistoryWindowType, historyWindowTypeTranslations, Interval, + MINUTE, QuickTimeInterval, realtimeAllowedAggIntervals, RealtimeWindowType, @@ -167,14 +169,14 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O }); } - if ((this.isEdit || !this.timewindow.realtime.hideLastInterval) && !this.quickIntervalOnly) { + if ((this.isEdit || !this.timewindow.realtime?.hideLastInterval) && !this.quickIntervalOnly) { this.realtimeTimewindowOptions.push({ name: this.translate.instant(realtimeWindowTypeTranslations.get(RealtimeWindowType.LAST_INTERVAL)), value: this.realtimeTypes.LAST_INTERVAL }); } - if (this.isEdit || !this.timewindow.realtime.hideQuickInterval || this.quickIntervalOnly) { + if (this.isEdit || !this.timewindow.realtime?.hideQuickInterval || this.quickIntervalOnly) { this.realtimeTimewindowOptions.push({ name: this.translate.instant(realtimeWindowTypeTranslations.get(RealtimeWindowType.INTERVAL)), value: this.realtimeTypes.INTERVAL @@ -188,21 +190,21 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O }); } - if (this.isEdit || !this.timewindow.history.hideLastInterval) { + if (this.isEdit || !this.timewindow.history?.hideLastInterval) { this.historyTimewindowOptions.push({ name: this.translate.instant(historyWindowTypeTranslations.get(HistoryWindowType.LAST_INTERVAL)), value: this.historyTypes.LAST_INTERVAL }); } - if (this.isEdit || !this.timewindow.history.hideFixedInterval) { + if (this.isEdit || !this.timewindow.history?.hideFixedInterval) { this.historyTimewindowOptions.push({ name: this.translate.instant(historyWindowTypeTranslations.get(HistoryWindowType.FIXED)), value: this.historyTypes.FIXED }); } - if (this.isEdit || !this.timewindow.history.hideQuickInterval) { + if (this.isEdit || !this.timewindow.history?.hideQuickInterval) { this.historyTimewindowOptions.push({ name: this.translate.instant(historyWindowTypeTranslations.get(HistoryWindowType.INTERVAL)), value: this.historyTypes.INTERVAL @@ -211,10 +213,10 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O this.realtimeTypeSelectionAvailable = this.realtimeTimewindowOptions.length > 1; this.historyTypeSelectionAvailable = this.historyTimewindowOptions.length > 1; - this.realtimeIntervalSelectionAvailable = this.isEdit || !(this.timewindow.realtime.hideInterval || - (this.timewindow.realtime.hideLastInterval && this.timewindow.realtime.hideQuickInterval)); - this.historyIntervalSelectionAvailable = this.isEdit || !(this.timewindow.history.hideInterval || - (this.timewindow.history.hideLastInterval && this.timewindow.history.hideQuickInterval && this.timewindow.history.hideFixedInterval)); + this.realtimeIntervalSelectionAvailable = this.isEdit || !(this.timewindow.realtime?.hideInterval || + (this.timewindow.realtime?.hideLastInterval && this.timewindow.realtime?.hideQuickInterval)); + this.historyIntervalSelectionAvailable = this.isEdit || !(this.timewindow.history?.hideInterval || + (this.timewindow.history?.hideLastInterval && this.timewindow.history?.hideQuickInterval && this.timewindow.history?.hideFixedInterval)); this.aggregationOptionsAvailable = this.aggregation && (this.isEdit || !(this.timewindow.hideAggregation && this.timewindow.hideAggInterval)); @@ -230,28 +232,28 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O const aggregation = this.timewindow.aggregation; if (!this.isEdit) { - if (realtime.hideLastInterval && !realtime.hideQuickInterval) { + if (realtime?.hideLastInterval && !realtime?.hideQuickInterval) { realtime.realtimeType = RealtimeWindowType.INTERVAL; } - if (realtime.hideQuickInterval && !realtime.hideLastInterval) { + if (realtime?.hideQuickInterval && !realtime?.hideLastInterval) { realtime.realtimeType = RealtimeWindowType.LAST_INTERVAL; } - if (history.hideLastInterval) { + if (history?.hideLastInterval) { if (!history.hideFixedInterval) { history.historyType = HistoryWindowType.FIXED; } else if (!history.hideQuickInterval) { history.historyType = HistoryWindowType.INTERVAL; } } - if (history.hideFixedInterval) { + if (history?.hideFixedInterval) { if (!history.hideLastInterval) { history.historyType = HistoryWindowType.LAST_INTERVAL; } else if (!history.hideQuickInterval) { history.historyType = HistoryWindowType.INTERVAL; } } - if (history.hideQuickInterval) { + if (history?.hideQuickInterval) { if (!history.hideLastInterval) { history.historyType = HistoryWindowType.LAST_INTERVAL; } else if (!history.hideFixedInterval) { @@ -265,29 +267,29 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O realtime: this.fb.group({ realtimeType: [{ value: isDefined(realtime?.realtimeType) ? realtime.realtimeType : RealtimeWindowType.LAST_INTERVAL, - disabled: realtime.hideInterval + disabled: realtime?.hideInterval }], timewindowMs: [{ - value: isDefined(realtime?.timewindowMs) ? realtime.timewindowMs : null, - disabled: realtime.hideInterval || realtime.hideLastInterval + value: isDefined(realtime?.timewindowMs) ? realtime.timewindowMs : MINUTE, + disabled: realtime?.hideInterval || realtime?.hideLastInterval }], interval: [{ value:isDefined(realtime?.interval) ? realtime.interval : null, disabled: hideAggInterval }], quickInterval: [{ - value: isDefined(realtime?.quickInterval) ? realtime.quickInterval : null, - disabled: realtime.hideInterval || realtime.hideQuickInterval + value: isDefined(realtime?.quickInterval) ? realtime.quickInterval : QuickTimeInterval.CURRENT_DAY, + disabled: realtime?.hideInterval || realtime?.hideQuickInterval }] }), history: this.fb.group({ historyType: [{ value: isDefined(history?.historyType) ? history.historyType : HistoryWindowType.LAST_INTERVAL, - disabled: history.hideInterval + disabled: history?.hideInterval }], timewindowMs: [{ - value: isDefined(history?.timewindowMs) ? history.timewindowMs : null, - disabled: history.hideInterval || history.hideLastInterval + value: isDefined(history?.timewindowMs) ? history.timewindowMs : MINUTE, + disabled: history?.hideInterval || history?.hideLastInterval }], interval: [{ value:isDefined(history?.interval) ? history.interval : null, @@ -296,11 +298,11 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O fixedTimewindow: [{ value: isDefined(history?.fixedTimewindow) && this.timewindow.selectedTab === TimewindowType.HISTORY && history.historyType === HistoryWindowType.FIXED ? history.fixedTimewindow : null, - disabled: history.hideInterval || history.hideFixedInterval + disabled: history?.hideInterval || history?.hideFixedInterval }], quickInterval: [{ - value: isDefined(history?.quickInterval) ? history.quickInterval : null, - disabled: history.hideInterval || history.hideQuickInterval + value: isDefined(history?.quickInterval) ? history.quickInterval : QuickTimeInterval.CURRENT_DAY, + disabled: history?.hideInterval || history?.hideQuickInterval }] }), aggregation: this.fb.group({ @@ -380,6 +382,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O takeUntil(this.destroy$) ).subscribe(() => { this.prepareTimewindowConfig(); + this.clearTimewindowConfig(); this.changeTimewindow.emit(this.timewindow); }); } @@ -407,6 +410,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O update() { this.prepareTimewindowConfig(); + this.clearTimewindowConfig(); this.result = this.timewindow; this.overlayRef?.dispose(); } @@ -414,21 +418,25 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O private prepareTimewindowConfig() { const timewindowFormValue = this.timewindowForm.getRawValue(); this.timewindow.selectedTab = timewindowFormValue.selectedTab; - this.timewindow.realtime = {...this.timewindow.realtime, ...{ - realtimeType: timewindowFormValue.realtime.realtimeType, - timewindowMs: timewindowFormValue.realtime.timewindowMs, - quickInterval: timewindowFormValue.realtime.quickInterval, - interval: timewindowFormValue.realtime.interval - }}; - this.timewindow.history = {...this.timewindow.history, ...{ - historyType: timewindowFormValue.history.historyType, - timewindowMs: timewindowFormValue.history.timewindowMs, - interval: timewindowFormValue.history.interval, - fixedTimewindow: timewindowFormValue.history.fixedTimewindow, - quickInterval: timewindowFormValue.history.quickInterval, - }}; + if (this.timewindow.selectedTab === TimewindowType.REALTIME) { + this.timewindow.realtime = {...this.timewindow.realtime, ...{ + realtimeType: timewindowFormValue.realtime.realtimeType, + timewindowMs: timewindowFormValue.realtime.timewindowMs, + quickInterval: timewindowFormValue.realtime.quickInterval, + }}; + } else { + this.timewindow.history = {...this.timewindow.history, ...{ + historyType: timewindowFormValue.history.historyType, + timewindowMs: timewindowFormValue.history.timewindowMs, + fixedTimewindow: timewindowFormValue.history.fixedTimewindow, + quickInterval: timewindowFormValue.history.quickInterval, + }}; + } if (this.aggregation) { + this.timewindow.realtime.interval = timewindowFormValue.realtime.interval; + this.timewindow.history.interval = timewindowFormValue.history.interval; + this.timewindow.aggregation = { type: timewindowFormValue.aggregation.type, limit: timewindowFormValue.aggregation.limit @@ -439,6 +447,10 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O } } + private clearTimewindowConfig() { + clearTimewindowConfig(this.timewindow, this.quickIntervalOnly, this.historyOnly, this.aggregation, this.timezone); + } + private updateTimewindowForm() { this.timewindowForm.patchValue(this.timewindow, {emitEvent: false}); this.updateValidators(this.timewindowForm.get('aggregation.type').value); @@ -568,12 +580,12 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O } private updateTimewindowAdvancedParams() { - this.realtimeDisableCustomInterval = this.timewindow.realtime.disableCustomInterval; - this.realtimeDisableCustomGroupInterval = this.timewindow.realtime.disableCustomGroupInterval; - this.historyDisableCustomInterval = this.timewindow.history.disableCustomInterval; - this.historyDisableCustomGroupInterval = this.timewindow.history.disableCustomGroupInterval; + this.realtimeDisableCustomInterval = this.timewindow.realtime?.disableCustomInterval; + this.realtimeDisableCustomGroupInterval = this.timewindow.realtime?.disableCustomGroupInterval; + this.historyDisableCustomInterval = this.timewindow.history?.disableCustomInterval; + this.historyDisableCustomGroupInterval = this.timewindow.history?.disableCustomGroupInterval; - if (this.timewindow.realtime.advancedParams) { + if (this.timewindow.realtime?.advancedParams) { this.realtimeAdvancedParams = this.timewindow.realtime.advancedParams; this.realtimeAllowedLastIntervals = this.timewindow.realtime.advancedParams.allowedLastIntervals; this.realtimeAllowedQuickIntervals = this.timewindow.realtime.advancedParams.allowedQuickIntervals; @@ -582,7 +594,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O this.realtimeAllowedLastIntervals = null; this.realtimeAllowedQuickIntervals = null; } - if (this.timewindow.history.advancedParams) { + if (this.timewindow.history?.advancedParams) { this.historyAdvancedParams = this.timewindow.history.advancedParams; this.historyAllowedLastIntervals = this.timewindow.history.advancedParams.allowedLastIntervals; this.historyAllowedQuickIntervals = this.timewindow.history.advancedParams.allowedQuickIntervals; diff --git a/ui-ngx/src/app/shared/components/time/timewindow.component.ts b/ui-ngx/src/app/shared/components/time/timewindow.component.ts index a52e3eb091..07f90db66e 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow.component.ts @@ -319,7 +319,8 @@ export class TimewindowComponent implements ControlValueAccessor, OnInit, OnChan } writeValue(obj: Timewindow): void { - this.innerValue = initModelFromDefaultTimewindow(obj, this.quickIntervalOnly, this.historyOnly, this.timeService); + this.innerValue = initModelFromDefaultTimewindow(obj, this.quickIntervalOnly, this.historyOnly, this.timeService, + this.aggregation); this.timewindowDisabled = this.isTimewindowDisabled(); if (this.onHistoryOnlyChanged()) { setTimeout(() => { diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index 5442cb95ca..7c53d93edf 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -15,11 +15,12 @@ /// import { TimeService } from '@core/services/time.service'; -import { deepClone, isDefined, isDefinedAndNotNull, isNumeric, isUndefined } from '@app/core/utils'; +import { deepClone, isDefined, isDefinedAndNotNull, isNumeric, isUndefined, isUndefinedOrNull } from '@app/core/utils'; import moment_ from 'moment'; import * as momentTz from 'moment-timezone'; import { IntervalType } from '@shared/models/telemetry/telemetry.models'; import { FormGroup } from '@angular/forms'; +import { isEmpty } from 'lodash'; const moment = moment_; @@ -314,7 +315,7 @@ const getTimewindowType = (timewindow: Timewindow): TimewindowType => { }; export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalOnly: boolean, - historyOnly: boolean, timeService: TimeService): Timewindow => { + historyOnly: boolean, timeService: TimeService, hasAggregation: boolean): Timewindow => { const model = defaultTimewindow(timeService); if (value) { if (value.allowedAggTypes?.length) { @@ -446,7 +447,9 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO } model.aggregation.limit = value.aggregation.limit || Math.floor(timeService.getMaxDatapointsLimit() / 2); } - model.timezone = value.timezone; + if (value.timezone) { + model.timezone = value.timezone; + } } if (quickIntervalOnly) { model.realtime.realtimeType = RealtimeWindowType.INTERVAL; @@ -454,6 +457,7 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO if (historyOnly) { model.selectedTab = TimewindowType.HISTORY; } + clearTimewindowConfig(model, quickIntervalOnly, historyOnly, hasAggregation); return model; }; @@ -1106,13 +1110,82 @@ export const cloneSelectedTimewindow = (timewindow: Timewindow): Timewindow => { if (isDefined(timewindow.selectedTab)) { cloned.selectedTab = timewindow.selectedTab; } - cloned.realtime = deepClone(timewindow.realtime); - cloned.history = deepClone(timewindow.history); - cloned.aggregation = deepClone(timewindow.aggregation); - cloned.timezone = timewindow.timezone; + if (isDefined(timewindow.realtime)) { + cloned.realtime = deepClone(timewindow.realtime); + } + if (isDefined(timewindow.history)) { + cloned.history = deepClone(timewindow.history); + } + if (isDefined(timewindow.aggregation)) { + cloned.aggregation = deepClone(timewindow.aggregation); + } + if (timewindow.timezone) { + cloned.timezone = timewindow.timezone; + } return cloned; }; +export const clearTimewindowConfig = (timewindow: Timewindow, quickIntervalOnly: boolean, + historyOnly: boolean, hasAggregation: boolean, hasTimezone = true): Timewindow => { + if (timewindow.selectedTab === TimewindowType.REALTIME) { + if (quickIntervalOnly || timewindow.realtime.realtimeType === RealtimeWindowType.INTERVAL) { + delete timewindow.realtime.timewindowMs; + } else { + delete timewindow.realtime.quickInterval; + } + + delete timewindow.history.historyType; + delete timewindow.history.timewindowMs; + delete timewindow.history.fixedTimewindow; + delete timewindow.history.quickInterval; + + delete timewindow.history.interval; + if (!hasAggregation) { + delete timewindow.realtime.interval; + } + } else { + if (timewindow.history.historyType === HistoryWindowType.LAST_INTERVAL) { + delete timewindow.history.fixedTimewindow; + delete timewindow.history.quickInterval; + } else if (timewindow.history.historyType === HistoryWindowType.FIXED) { + delete timewindow.history.timewindowMs; + delete timewindow.history.quickInterval; + } else if (timewindow.history.historyType === HistoryWindowType.INTERVAL) { + delete timewindow.history.timewindowMs; + delete timewindow.history.fixedTimewindow; + } else { + delete timewindow.history.timewindowMs; + delete timewindow.history.fixedTimewindow; + delete timewindow.history.quickInterval; + } + + delete timewindow.realtime.realtimeType; + delete timewindow.realtime.timewindowMs; + delete timewindow.realtime.quickInterval; + + delete timewindow.realtime.interval; + if (!hasAggregation) { + delete timewindow.history.interval; + } + } + + if (!hasAggregation) { + delete timewindow.aggregation; + } + + if (isEmpty(timewindow.history)) { + delete timewindow.history; + } + if (historyOnly || isEmpty(timewindow.realtime)) { + delete timewindow.realtime; + } + + if (!hasTimezone || isUndefinedOrNull(timewindow.timezone)) { + delete timewindow.timezone; + } + return timewindow; +}; + export interface TimeInterval { name: string; translateParams: {[key: string]: any}; From 6d509ca5d9312416cae4e6ebed154ef799a991e1 Mon Sep 17 00:00:00 2001 From: Ekaterina Chantsova Date: Mon, 21 Jul 2025 16:19:06 +0300 Subject: [PATCH 007/216] Timewindow: optimize advanced configuration parameters update, add deepClean --- ui-ngx/src/app/core/utils.ts | 30 ++++++++++++- .../timewindow-config-dialog.component.ts | 45 +++++-------------- .../time/timewindow-panel.component.ts | 10 ++--- .../src/app/shared/models/time/time.models.ts | 15 +++---- 4 files changed, 50 insertions(+), 50 deletions(-) diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 353727e006..d0bd9ae484 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -27,7 +27,8 @@ import { serverErrorCodesTranslations } from '@shared/models/constants'; import { SubscriptionEntityInfo } from '@core/api/widget-api.models'; import { CompiledTbFunction, - compileTbFunction, GenericFunction, + compileTbFunction, + GenericFunction, isNotEmptyTbFunction, TbFunction } from '@shared/models/js-function.models'; @@ -773,6 +774,33 @@ export function deepTrim(obj: T): T { }, (Array.isArray(obj) ? [] : {}) as T); } +export function deepClean | any[]>(obj: T, { + cleanKeys = [] +} = {}): T { + return _.transform(obj, (result, value, key) => { + if (cleanKeys.includes(key)) { + return; + } + if (Array.isArray(value) || isLiteralObject(value)) { + value = deepClean(value, {cleanKeys}); + } + if(isLiteralObject(value) && isEmpty(value)) { + return; + } + if (Array.isArray(value) && !value.length) { + return; + } + if (value === undefined || value === null || value === '' || Number.isNaN(value)) { + return; + } + + if (Array.isArray(result)) { + return result.push(value); + } + result[key] = value; + }); +} + export function generateSecret(length?: number): string { if (isUndefined(length) || length == null) { length = 1; diff --git a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts index 07e532cac0..33a9d44cf3 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts @@ -36,7 +36,7 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { TimeService } from '@core/services/time.service'; -import { deepClone, isDefined, isDefinedAndNotNull, isObject, mergeDeep } from '@core/utils'; +import { deepClean, deepClone, isDefined, isDefinedAndNotNull, isEmpty, mergeDeepIgnoreArray } from '@core/utils'; import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; import { TranslateService } from '@ngx-translate/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; @@ -423,7 +423,7 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On update() { const timewindowFormValue = this.timewindowForm.getRawValue(); - this.timewindow = mergeDeep(this.timewindow, timewindowFormValue); + this.timewindow = mergeDeepIgnoreArray(this.timewindow, timewindowFormValue); const realtimeConfigurableLastIntervalsAvailable = !(timewindowFormValue.hideAggInterval && (timewindowFormValue.realtime.hideInterval || timewindowFormValue.realtime.hideLastInterval)); @@ -434,62 +434,41 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On const historyConfigurableQuickIntervalsAvailable = !(timewindowFormValue.hideAggInterval && (timewindowFormValue.history.hideInterval || timewindowFormValue.history.hideQuickInterval)); - if (realtimeConfigurableLastIntervalsAvailable && timewindowFormValue.realtime.advancedParams.allowedLastIntervals?.length) { - this.timewindow.realtime.advancedParams.allowedLastIntervals = timewindowFormValue.realtime.advancedParams.allowedLastIntervals; - } else { + if (!realtimeConfigurableLastIntervalsAvailable) { delete this.timewindow.realtime.advancedParams.allowedLastIntervals; } - if (realtimeConfigurableQuickIntervalsAvailable && timewindowFormValue.realtime.advancedParams.allowedQuickIntervals?.length) { - this.timewindow.realtime.advancedParams.allowedQuickIntervals = timewindowFormValue.realtime.advancedParams.allowedQuickIntervals; - } else { + if (!realtimeConfigurableQuickIntervalsAvailable) { delete this.timewindow.realtime.advancedParams.allowedQuickIntervals; } - if (realtimeConfigurableLastIntervalsAvailable && isObject(timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig) && - Object.keys(timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig).length) { + if (realtimeConfigurableLastIntervalsAvailable && !isEmpty(timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig)) { this.timewindow.realtime.advancedParams.lastAggIntervalsConfig = timewindowFormValue.realtime.advancedParams.lastAggIntervalsConfig; } else { delete this.timewindow.realtime.advancedParams.lastAggIntervalsConfig; } - if (realtimeConfigurableQuickIntervalsAvailable && isObject(timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig) && - Object.keys(timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig).length) { + if (realtimeConfigurableQuickIntervalsAvailable && !isEmpty(timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig)) { this.timewindow.realtime.advancedParams.quickAggIntervalsConfig = timewindowFormValue.realtime.advancedParams.quickAggIntervalsConfig; } else { delete this.timewindow.realtime.advancedParams.quickAggIntervalsConfig; } - if (historyConfigurableLastIntervalsAvailable && timewindowFormValue.history.advancedParams.allowedLastIntervals?.length) { - this.timewindow.history.advancedParams.allowedLastIntervals = timewindowFormValue.history.advancedParams.allowedLastIntervals; - } else { + if (!historyConfigurableLastIntervalsAvailable) { delete this.timewindow.history.advancedParams.allowedLastIntervals; } - if (historyConfigurableQuickIntervalsAvailable && timewindowFormValue.history.advancedParams.allowedQuickIntervals?.length) { - this.timewindow.history.advancedParams.allowedQuickIntervals = timewindowFormValue.history.advancedParams.allowedQuickIntervals; - } else { + if (!historyConfigurableQuickIntervalsAvailable) { delete this.timewindow.history.advancedParams.allowedQuickIntervals; } - if (historyConfigurableLastIntervalsAvailable && isObject(timewindowFormValue.history.advancedParams.lastAggIntervalsConfig) && - Object.keys(timewindowFormValue.history.advancedParams.lastAggIntervalsConfig).length) { + if (historyConfigurableLastIntervalsAvailable && !isEmpty(timewindowFormValue.history.advancedParams.lastAggIntervalsConfig)) { this.timewindow.history.advancedParams.lastAggIntervalsConfig = timewindowFormValue.history.advancedParams.lastAggIntervalsConfig; } else { delete this.timewindow.history.advancedParams.lastAggIntervalsConfig; } - if (historyConfigurableQuickIntervalsAvailable && isObject(timewindowFormValue.history.advancedParams.quickAggIntervalsConfig) && - Object.keys(timewindowFormValue.history.advancedParams.quickAggIntervalsConfig).length) { + if (historyConfigurableQuickIntervalsAvailable && !isEmpty(timewindowFormValue.history.advancedParams.quickAggIntervalsConfig)) { this.timewindow.history.advancedParams.quickAggIntervalsConfig = timewindowFormValue.history.advancedParams.quickAggIntervalsConfig; } else { delete this.timewindow.history.advancedParams.quickAggIntervalsConfig; } - if (!Object.keys(this.timewindow.realtime.advancedParams).length) { - delete this.timewindow.realtime.advancedParams; - } - if (!Object.keys(this.timewindow.history.advancedParams).length) { - delete this.timewindow.history.advancedParams; - } - - if (timewindowFormValue.allowedAggTypes?.length && !timewindowFormValue.hideAggregation) { - this.timewindow.allowedAggTypes = timewindowFormValue.allowedAggTypes; - } else { + if (timewindowFormValue.hideAggregation) { delete this.timewindow.allowedAggTypes; } @@ -541,7 +520,7 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On if (!this.aggregation) { delete this.timewindow.aggregation; } - this.dialogRef.close(this.timewindow); + this.dialogRef.close(deepClean(this.timewindow)); } cancel() { diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts index 9831ec0599..7c6445c6b2 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts @@ -382,8 +382,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O takeUntil(this.destroy$) ).subscribe(() => { this.prepareTimewindowConfig(); - this.clearTimewindowConfig(); - this.changeTimewindow.emit(this.timewindow); + this.changeTimewindow.emit(this.clearTimewindowConfig()); }); } } @@ -410,8 +409,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O update() { this.prepareTimewindowConfig(); - this.clearTimewindowConfig(); - this.result = this.timewindow; + this.result = this.clearTimewindowConfig(); this.overlayRef?.dispose(); } @@ -447,8 +445,8 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O } } - private clearTimewindowConfig() { - clearTimewindowConfig(this.timewindow, this.quickIntervalOnly, this.historyOnly, this.aggregation, this.timezone); + private clearTimewindowConfig(): Timewindow { + return clearTimewindowConfig(this.timewindow, this.quickIntervalOnly, this.historyOnly, this.aggregation, this.timezone); } private updateTimewindowForm() { diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index 7c53d93edf..cace546b19 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -15,12 +15,11 @@ /// import { TimeService } from '@core/services/time.service'; -import { deepClone, isDefined, isDefinedAndNotNull, isNumeric, isUndefined, isUndefinedOrNull } from '@app/core/utils'; +import { deepClean, deepClone, isDefined, isDefinedAndNotNull, isNumeric, isUndefined } from '@app/core/utils'; import moment_ from 'moment'; import * as momentTz from 'moment-timezone'; import { IntervalType } from '@shared/models/telemetry/telemetry.models'; import { FormGroup } from '@angular/forms'; -import { isEmpty } from 'lodash'; const moment = moment_; @@ -457,8 +456,7 @@ export const initModelFromDefaultTimewindow = (value: Timewindow, quickIntervalO if (historyOnly) { model.selectedTab = TimewindowType.HISTORY; } - clearTimewindowConfig(model, quickIntervalOnly, historyOnly, hasAggregation); - return model; + return clearTimewindowConfig(model, quickIntervalOnly, historyOnly, hasAggregation); }; export const toHistoryTimewindow = (timewindow: Timewindow, startTimeMs: number, endTimeMs: number, @@ -1173,17 +1171,14 @@ export const clearTimewindowConfig = (timewindow: Timewindow, quickIntervalOnly: delete timewindow.aggregation; } - if (isEmpty(timewindow.history)) { - delete timewindow.history; - } - if (historyOnly || isEmpty(timewindow.realtime)) { + if (historyOnly) { delete timewindow.realtime; } - if (!hasTimezone || isUndefinedOrNull(timewindow.timezone)) { + if (!hasTimezone) { delete timewindow.timezone; } - return timewindow; + return deepClean(timewindow); }; export interface TimeInterval { From d95a00e4af4fb5b369d32f4d6f19cd659dc4577d Mon Sep 17 00:00:00 2001 From: Ekaterina Chantsova Date: Mon, 21 Jul 2025 17:22:34 +0300 Subject: [PATCH 008/216] Timewindow: optimize false properties deletion --- ui-ngx/src/app/core/utils.ts | 17 ++++++ .../timewindow-config-dialog.component.ts | 57 ++++--------------- 2 files changed, 28 insertions(+), 46 deletions(-) diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index d0bd9ae484..9937689ba8 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -197,6 +197,23 @@ export function deleteNullProperties(obj: any) { }); } +export function deleteFalseProperties(obj: any) { + if (isUndefinedOrNull(obj)) { + return; + } + Object.keys(obj).forEach((propName) => { + if (obj[propName] === false || isUndefinedOrNull(obj[propName])) { + delete obj[propName]; + } else if (isObject(obj[propName])) { + deleteFalseProperties(obj[propName]); + } else if (Array.isArray(obj[propName])) { + (obj[propName] as any[]).forEach((elem) => { + deleteFalseProperties(elem); + }); + } + }); +} + export function objToBase64(obj: any): string { const json = JSON.stringify(obj); return btoa(encodeURIComponent(json).replace(/%([0-9A-F]{2})/g, diff --git a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts index 33a9d44cf3..42e04cfc46 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts @@ -36,7 +36,15 @@ import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { TimeService } from '@core/services/time.service'; -import { deepClean, deepClone, isDefined, isDefinedAndNotNull, isEmpty, mergeDeepIgnoreArray } from '@core/utils'; +import { + deepClean, + deepClone, + deleteFalseProperties, + isDefined, + isDefinedAndNotNull, + isEmpty, + mergeDeepIgnoreArray +} from '@core/utils'; import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; import { TranslateService } from '@ngx-translate/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; @@ -472,54 +480,11 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On delete this.timewindow.allowedAggTypes; } - if (!this.timewindow.realtime.disableCustomInterval) { - delete this.timewindow.realtime.disableCustomInterval; - } - if (!this.timewindow.realtime.disableCustomGroupInterval) { - delete this.timewindow.realtime.disableCustomGroupInterval; - } - if (!this.timewindow.history.disableCustomInterval) { - delete this.timewindow.history.disableCustomInterval; - } - if (!this.timewindow.history.disableCustomGroupInterval) { - delete this.timewindow.history.disableCustomGroupInterval; - } - - if (!this.timewindow.hideAggregation) { - delete this.timewindow.hideAggregation; - } - if (!this.timewindow.hideAggInterval) { - delete this.timewindow.hideAggInterval; - } - if (!this.timewindow.hideTimezone) { - delete this.timewindow.hideTimezone; - } - - if (!this.timewindow.realtime.hideInterval) { - delete this.timewindow.realtime.hideInterval; - } - if (!this.timewindow.realtime.hideLastInterval) { - delete this.timewindow.realtime.hideLastInterval; - } - if (!this.timewindow.realtime.hideQuickInterval) { - delete this.timewindow.realtime.hideQuickInterval; - } - if (!this.timewindow.history.hideInterval) { - delete this.timewindow.history.hideInterval; - } - if (!this.timewindow.history.hideLastInterval) { - delete this.timewindow.history.hideLastInterval; - } - if (!this.timewindow.history.hideFixedInterval) { - delete this.timewindow.history.hideFixedInterval; - } - if (!this.timewindow.history.hideQuickInterval) { - delete this.timewindow.history.hideQuickInterval; - } - if (!this.aggregation) { delete this.timewindow.aggregation; } + + deleteFalseProperties(this.timewindow); this.dialogRef.close(deepClean(this.timewindow)); } From 81a5a3c3eaf8c7ab860ba4060edc9dba049bf8f4 Mon Sep 17 00:00:00 2001 From: Ekaterina Chantsova Date: Mon, 21 Jul 2025 16:19:06 +0300 Subject: [PATCH 009/216] Timewindow: fixed updating aggregation interval --- .../app/shared/components/time/timewindow-panel.component.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts index 7c6445c6b2..6b5d434102 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts @@ -421,6 +421,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O realtimeType: timewindowFormValue.realtime.realtimeType, timewindowMs: timewindowFormValue.realtime.timewindowMs, quickInterval: timewindowFormValue.realtime.quickInterval, + interval: timewindowFormValue.realtime.interval, }}; } else { this.timewindow.history = {...this.timewindow.history, ...{ @@ -428,13 +429,11 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O timewindowMs: timewindowFormValue.history.timewindowMs, fixedTimewindow: timewindowFormValue.history.fixedTimewindow, quickInterval: timewindowFormValue.history.quickInterval, + interval: timewindowFormValue.history.interval, }}; } if (this.aggregation) { - this.timewindow.realtime.interval = timewindowFormValue.realtime.interval; - this.timewindow.history.interval = timewindowFormValue.history.interval; - this.timewindow.aggregation = { type: timewindowFormValue.aggregation.type, limit: timewindowFormValue.aggregation.limit From 35b349bd6e155f9a31baf4a9787b884fe49d9e75 Mon Sep 17 00:00:00 2001 From: Ekaterina Chantsova Date: Mon, 21 Jul 2025 16:19:06 +0300 Subject: [PATCH 010/216] Timewindow: additional conditioning for clear config function; refactoring --- .../time/timewindow-panel.component.ts | 19 +++++++++---------- .../src/app/shared/models/time/time.models.ts | 18 +++++++++--------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts index 6b5d434102..2f0a1aa603 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts @@ -381,8 +381,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O this.timewindowForm.valueChanges.pipe( takeUntil(this.destroy$) ).subscribe(() => { - this.prepareTimewindowConfig(); - this.changeTimewindow.emit(this.clearTimewindowConfig()); + this.changeTimewindow.emit(this.prepareTimewindowConfig()); }); } } @@ -408,12 +407,11 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O } update() { - this.prepareTimewindowConfig(); - this.result = this.clearTimewindowConfig(); + this.result = this.prepareTimewindowConfig(); this.overlayRef?.dispose(); } - private prepareTimewindowConfig() { + private prepareTimewindowConfig(clearConfig = true): Timewindow { const timewindowFormValue = this.timewindowForm.getRawValue(); this.timewindow.selectedTab = timewindowFormValue.selectedTab; if (this.timewindow.selectedTab === TimewindowType.REALTIME) { @@ -442,10 +440,12 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O if (this.timezone) { this.timewindow.timezone = timewindowFormValue.timezone; } - } - private clearTimewindowConfig(): Timewindow { - return clearTimewindowConfig(this.timewindow, this.quickIntervalOnly, this.historyOnly, this.aggregation, this.timezone); + if (clearConfig) { + return clearTimewindowConfig(this.timewindow, this.quickIntervalOnly, this.historyOnly, this.aggregation, this.timezone); + } else { + return deepClone(this.timewindow); + } } private updateTimewindowForm() { @@ -555,7 +555,6 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O } openTimewindowConfig() { - this.prepareTimewindowConfig(); this.dialog.open( TimewindowConfigDialogComponent, { autoFocus: false, @@ -564,7 +563,7 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O data: { quickIntervalOnly: this.quickIntervalOnly, aggregation: this.aggregation, - timewindow: deepClone(this.timewindow) + timewindow: this.prepareTimewindowConfig(false) } }).afterClosed() .subscribe((res) => { diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index cace546b19..35ff3a0572 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -1132,12 +1132,12 @@ export const clearTimewindowConfig = (timewindow: Timewindow, quickIntervalOnly: delete timewindow.realtime.quickInterval; } - delete timewindow.history.historyType; - delete timewindow.history.timewindowMs; - delete timewindow.history.fixedTimewindow; - delete timewindow.history.quickInterval; + delete timewindow.history?.historyType; + delete timewindow.history?.timewindowMs; + delete timewindow.history?.fixedTimewindow; + delete timewindow.history?.quickInterval; - delete timewindow.history.interval; + delete timewindow.history?.interval; if (!hasAggregation) { delete timewindow.realtime.interval; } @@ -1157,11 +1157,11 @@ export const clearTimewindowConfig = (timewindow: Timewindow, quickIntervalOnly: delete timewindow.history.quickInterval; } - delete timewindow.realtime.realtimeType; - delete timewindow.realtime.timewindowMs; - delete timewindow.realtime.quickInterval; + delete timewindow.realtime?.realtimeType; + delete timewindow.realtime?.timewindowMs; + delete timewindow.realtime?.quickInterval; - delete timewindow.realtime.interval; + delete timewindow.realtime?.interval; if (!hasAggregation) { delete timewindow.history.interval; } From 20bccee9728f6590f5e9791598024b66a2f9b32f Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 24 Jul 2025 12:55:19 +0300 Subject: [PATCH 011/216] Version set 4.3.0-SNAPSHOT --- application/pom.xml | 2 +- application/src/main/resources/thingsboard.yml | 2 +- common/actor/pom.xml | 2 +- common/cache/pom.xml | 2 +- common/cluster-api/pom.xml | 2 +- common/coap-server/pom.xml | 2 +- common/dao-api/pom.xml | 2 +- common/data/pom.xml | 2 +- common/discovery-api/pom.xml | 2 +- common/edge-api/pom.xml | 2 +- common/edqs/pom.xml | 2 +- common/message/pom.xml | 2 +- common/pom.xml | 2 +- common/proto/pom.xml | 2 +- common/queue/pom.xml | 2 +- common/script/pom.xml | 2 +- common/script/remote-js-client/pom.xml | 2 +- common/script/script-api/pom.xml | 2 +- common/stats/pom.xml | 2 +- common/transport/coap/pom.xml | 2 +- common/transport/http/pom.xml | 2 +- common/transport/lwm2m/pom.xml | 2 +- common/transport/mqtt/pom.xml | 2 +- common/transport/pom.xml | 2 +- common/transport/snmp/pom.xml | 2 +- common/transport/transport-api/pom.xml | 2 +- common/util/pom.xml | 2 +- common/version-control/pom.xml | 2 +- dao/pom.xml | 2 +- edqs/pom.xml | 2 +- monitoring/pom.xml | 2 +- msa/black-box-tests/pom.xml | 2 +- msa/edqs/pom.xml | 2 +- msa/js-executor/package.json | 2 +- msa/js-executor/pom.xml | 2 +- msa/monitoring/pom.xml | 2 +- msa/pom.xml | 2 +- msa/tb-node/pom.xml | 2 +- msa/tb/pom.xml | 2 +- msa/transport/coap/pom.xml | 2 +- msa/transport/http/pom.xml | 2 +- msa/transport/lwm2m/pom.xml | 2 +- msa/transport/mqtt/pom.xml | 2 +- msa/transport/pom.xml | 2 +- msa/transport/snmp/pom.xml | 2 +- msa/vc-executor-docker/pom.xml | 2 +- msa/vc-executor/pom.xml | 2 +- msa/web-ui/package.json | 2 +- msa/web-ui/pom.xml | 2 +- netty-mqtt/pom.xml | 4 ++-- pom.xml | 2 +- rest-client/pom.xml | 2 +- rule-engine/pom.xml | 2 +- rule-engine/rule-engine-api/pom.xml | 2 +- rule-engine/rule-engine-components/pom.xml | 2 +- tools/pom.xml | 2 +- transport/coap/pom.xml | 2 +- transport/http/pom.xml | 2 +- transport/lwm2m/pom.xml | 2 +- transport/mqtt/pom.xml | 2 +- transport/pom.xml | 2 +- transport/snmp/pom.xml | 2 +- ui-ngx/package.json | 2 +- ui-ngx/pom.xml | 2 +- 64 files changed, 65 insertions(+), 65 deletions(-) diff --git a/application/pom.xml b/application/pom.xml index f4d1235a64..dba7d27c1b 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard application diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index a6ffc333b6..87163e85b3 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -208,7 +208,7 @@ ui: # Help parameters help: # Base URL for UI help assets - base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-4.1}" + base-url: "${UI_HELP_BASE_URL:https://raw.githubusercontent.com/thingsboard/thingsboard-ui-help/release-4.3}" # Database telemetry parameters database: diff --git a/common/actor/pom.xml b/common/actor/pom.xml index 5a7c3fd665..92b174ea61 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/cache/pom.xml b/common/cache/pom.xml index 3094e1601f..fcf52e01b7 100644 --- a/common/cache/pom.xml +++ b/common/cache/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index a8a419aba3..1f4b5ff41b 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml index 64867e9020..27ff645a0f 100644 --- a/common/coap-server/pom.xml +++ b/common/coap-server/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index 7f95023057..160351d55d 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/data/pom.xml b/common/data/pom.xml index f582c32023..779565ed8e 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/discovery-api/pom.xml b/common/discovery-api/pom.xml index e66d86ff41..3ae12c1bdb 100644 --- a/common/discovery-api/pom.xml +++ b/common/discovery-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/edge-api/pom.xml b/common/edge-api/pom.xml index a153ee7a9f..7c2967cf3d 100644 --- a/common/edge-api/pom.xml +++ b/common/edge-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/edqs/pom.xml b/common/edqs/pom.xml index fa5012a180..f7616d5b37 100644 --- a/common/edqs/pom.xml +++ b/common/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/message/pom.xml b/common/message/pom.xml index e34250fc23..757c5de251 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/pom.xml b/common/pom.xml index 1bfb9904ed..a9195e49d4 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard common diff --git a/common/proto/pom.xml b/common/proto/pom.xml index ad2243776b..09f9596d0c 100644 --- a/common/proto/pom.xml +++ b/common/proto/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/queue/pom.xml b/common/queue/pom.xml index e934be308e..ffee41f8a9 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/script/pom.xml b/common/script/pom.xml index 999d895513..d0a3b47218 100644 --- a/common/script/pom.xml +++ b/common/script/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/script/remote-js-client/pom.xml b/common/script/remote-js-client/pom.xml index d92e8e78cd..b1547d2e4b 100644 --- a/common/script/remote-js-client/pom.xml +++ b/common/script/remote-js-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT script org.thingsboard.common.script diff --git a/common/script/script-api/pom.xml b/common/script/script-api/pom.xml index bda91efa78..b0fcf1c4ae 100644 --- a/common/script/script-api/pom.xml +++ b/common/script/script-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT script org.thingsboard.common.script diff --git a/common/stats/pom.xml b/common/stats/pom.xml index f2d18688fa..e44e098a39 100644 --- a/common/stats/pom.xml +++ b/common/stats/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index c4f6f85e64..37f33470b2 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index 6cdc499a41..e49af5b0a2 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index 72a5731f8f..1787c851cd 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index 71a1de3228..9df8cf918e 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/pom.xml b/common/transport/pom.xml index dcb627b1b0..3bd21c96de 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/snmp/pom.xml b/common/transport/snmp/pom.xml index ecf85b1dac..1286073a5f 100644 --- a/common/transport/snmp/pom.xml +++ b/common/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.common - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index 366d2b3d9b..19b075cb61 100644 --- a/common/transport/transport-api/pom.xml +++ b/common/transport/transport-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/util/pom.xml b/common/util/pom.xml index bad961ac71..804ed0de38 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/version-control/pom.xml b/common/version-control/pom.xml index 5d6079b313..58c059915c 100644 --- a/common/version-control/pom.xml +++ b/common/version-control/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/dao/pom.xml b/dao/pom.xml index 187c7e6ea5..fd6af62330 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard dao diff --git a/edqs/pom.xml b/edqs/pom.xml index 4cca1bd89e..4a4c05be71 100644 --- a/edqs/pom.xml +++ b/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard edqs diff --git a/monitoring/pom.xml b/monitoring/pom.xml index 30a2386713..6dbcf85f07 100644 --- a/monitoring/pom.xml +++ b/monitoring/pom.xml @@ -21,7 +21,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index d3b47daa47..358c524e06 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/edqs/pom.xml b/msa/edqs/pom.xml index 882ba588e1..2dd4a9a584 100644 --- a/msa/edqs/pom.xml +++ b/msa/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json index 4d417081d8..3ee1614e87 100644 --- a/msa/js-executor/package.json +++ b/msa/js-executor/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-js-executor", "private": true, - "version": "4.2.0", + "version": "4.3.0", "description": "ThingsBoard JavaScript Executor Microservice", "main": "server.ts", "bin": "server.js", diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index 95e4516044..d7bac12742 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/monitoring/pom.xml b/msa/monitoring/pom.xml index a7a8ff1b89..cb5b609e43 100644 --- a/msa/monitoring/pom.xml +++ b/msa/monitoring/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT msa diff --git a/msa/pom.xml b/msa/pom.xml index b980bd703e..7fbc5d3c02 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard msa diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index 9f5c7cf358..8df294260b 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index 421cf1290f..1cbbf9d1ec 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml index f0a46069db..256220078d 100644 --- a/msa/transport/coap/pom.xml +++ b/msa/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml index 45f9323ab7..7e5871729d 100644 --- a/msa/transport/http/pom.xml +++ b/msa/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/lwm2m/pom.xml b/msa/transport/lwm2m/pom.xml index b95402ba14..ca6977eac6 100644 --- a/msa/transport/lwm2m/pom.xml +++ b/msa/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml index 2fbfc5b9f2..172c4facb0 100644 --- a/msa/transport/mqtt/pom.xml +++ b/msa/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml index 5b57136d88..a6cff4fd0b 100644 --- a/msa/transport/pom.xml +++ b/msa/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/snmp/pom.xml b/msa/transport/snmp/pom.xml index ee79717ca4..79734822aa 100644 --- a/msa/transport/snmp/pom.xml +++ b/msa/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.msa transport - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT org.thingsboard.msa.transport diff --git a/msa/vc-executor-docker/pom.xml b/msa/vc-executor-docker/pom.xml index b81504aa06..85634d8688 100644 --- a/msa/vc-executor-docker/pom.xml +++ b/msa/vc-executor-docker/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/vc-executor/pom.xml b/msa/vc-executor/pom.xml index e15fc6699c..0acd863612 100644 --- a/msa/vc-executor/pom.xml +++ b/msa/vc-executor/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json index de0c04fc9d..3cdea8fb72 100644 --- a/msa/web-ui/package.json +++ b/msa/web-ui/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-web-ui", "private": true, - "version": "4.2.0", + "version": "4.3.0", "description": "ThingsBoard Web UI Microservice", "main": "server.ts", "bin": "server.js", diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml index 4aeb5227c8..ce3475b113 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index b30f1e2da3..a09d1e2409 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -19,11 +19,11 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard netty-mqtt - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT jar Netty MQTT Client diff --git a/pom.xml b/pom.xml index 8dfa6ef9ec..21a5c4fb70 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT pom Thingsboard diff --git a/rest-client/pom.xml b/rest-client/pom.xml index 392c3b360f..c3cf2460dd 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard rest-client diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml index aa561832b3..7abb7be3a3 100644 --- a/rule-engine/pom.xml +++ b/rule-engine/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard rule-engine diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index 519ca0d54a..59397d4a92 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index a914b2c8f0..60615c21e2 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/tools/pom.xml b/tools/pom.xml index d748e649ab..0b63d84b0a 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard tools diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index 1d829679ae..67f16917d3 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/http/pom.xml b/transport/http/pom.xml index 10ffdfc07e..371ecb84ca 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/lwm2m/pom.xml b/transport/lwm2m/pom.xml index 1c83f8b443..66a3066a88 100644 --- a/transport/lwm2m/pom.xml +++ b/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index ea8c750b6d..57a73b1bbe 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/pom.xml b/transport/pom.xml index eeb4873e48..a8658bbf40 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard transport diff --git a/transport/snmp/pom.xml b/transport/snmp/pom.xml index 071c4423b6..d5c3d1fe02 100644 --- a/transport/snmp/pom.xml +++ b/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT transport diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 4ba512a024..580b63a588 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -1,6 +1,6 @@ { "name": "thingsboard", - "version": "4.2.0", + "version": "4.3.0", "scripts": { "ng": "ng", "start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --configuration development --host 0.0.0.0 --open", diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml index 772921ee7a..f2aadd3887 100644 --- a/ui-ngx/pom.xml +++ b/ui-ngx/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-SNAPSHOT + 4.3.0-SNAPSHOT thingsboard org.thingsboard From 4cdf1baf9a8e3871a4df94176dcff51187e1fc65 Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 24 Jul 2025 12:56:40 +0300 Subject: [PATCH 012/216] Temp version set to 4.2.0-RC --- application/pom.xml | 2 +- common/actor/pom.xml | 2 +- common/cache/pom.xml | 2 +- common/cluster-api/pom.xml | 2 +- common/coap-server/pom.xml | 2 +- common/dao-api/pom.xml | 2 +- common/data/pom.xml | 2 +- common/discovery-api/pom.xml | 2 +- common/edge-api/pom.xml | 2 +- common/edqs/pom.xml | 2 +- common/message/pom.xml | 2 +- common/pom.xml | 2 +- common/proto/pom.xml | 2 +- common/queue/pom.xml | 2 +- common/script/pom.xml | 2 +- common/script/remote-js-client/pom.xml | 2 +- common/script/script-api/pom.xml | 2 +- common/stats/pom.xml | 2 +- common/transport/coap/pom.xml | 2 +- common/transport/http/pom.xml | 2 +- common/transport/lwm2m/pom.xml | 2 +- common/transport/mqtt/pom.xml | 2 +- common/transport/pom.xml | 2 +- common/transport/snmp/pom.xml | 2 +- common/transport/transport-api/pom.xml | 2 +- common/util/pom.xml | 2 +- common/version-control/pom.xml | 2 +- dao/pom.xml | 2 +- edqs/pom.xml | 2 +- monitoring/pom.xml | 2 +- msa/black-box-tests/pom.xml | 2 +- msa/edqs/pom.xml | 2 +- msa/js-executor/pom.xml | 2 +- msa/monitoring/pom.xml | 2 +- msa/pom.xml | 2 +- msa/tb-node/pom.xml | 2 +- msa/tb/pom.xml | 2 +- msa/transport/coap/pom.xml | 2 +- msa/transport/http/pom.xml | 2 +- msa/transport/lwm2m/pom.xml | 2 +- msa/transport/mqtt/pom.xml | 2 +- msa/transport/pom.xml | 2 +- msa/transport/snmp/pom.xml | 2 +- msa/vc-executor-docker/pom.xml | 2 +- msa/vc-executor/pom.xml | 2 +- msa/web-ui/pom.xml | 2 +- netty-mqtt/pom.xml | 4 ++-- pom.xml | 2 +- rest-client/pom.xml | 2 +- rule-engine/pom.xml | 2 +- rule-engine/rule-engine-api/pom.xml | 2 +- rule-engine/rule-engine-components/pom.xml | 2 +- tools/pom.xml | 2 +- transport/coap/pom.xml | 2 +- transport/http/pom.xml | 2 +- transport/lwm2m/pom.xml | 2 +- transport/mqtt/pom.xml | 2 +- transport/pom.xml | 2 +- transport/snmp/pom.xml | 2 +- ui-ngx/pom.xml | 2 +- 60 files changed, 61 insertions(+), 61 deletions(-) diff --git a/application/pom.xml b/application/pom.xml index dba7d27c1b..33bc0972d4 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC thingsboard application diff --git a/common/actor/pom.xml b/common/actor/pom.xml index 92b174ea61..a6efdda77d 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC common org.thingsboard.common diff --git a/common/cache/pom.xml b/common/cache/pom.xml index fcf52e01b7..3088dad098 100644 --- a/common/cache/pom.xml +++ b/common/cache/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC common org.thingsboard.common diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index 1f4b5ff41b..939dfb1e16 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC common org.thingsboard.common diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml index 27ff645a0f..fbfbe78b55 100644 --- a/common/coap-server/pom.xml +++ b/common/coap-server/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC common org.thingsboard.common diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index 160351d55d..636c7b9ef4 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC common org.thingsboard.common diff --git a/common/data/pom.xml b/common/data/pom.xml index 779565ed8e..da2a0970b1 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC common org.thingsboard.common diff --git a/common/discovery-api/pom.xml b/common/discovery-api/pom.xml index 3ae12c1bdb..4f9ef70c5b 100644 --- a/common/discovery-api/pom.xml +++ b/common/discovery-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC common org.thingsboard.common diff --git a/common/edge-api/pom.xml b/common/edge-api/pom.xml index 7c2967cf3d..69a01be812 100644 --- a/common/edge-api/pom.xml +++ b/common/edge-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC common org.thingsboard.common diff --git a/common/edqs/pom.xml b/common/edqs/pom.xml index f7616d5b37..140eabd270 100644 --- a/common/edqs/pom.xml +++ b/common/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC common org.thingsboard.common diff --git a/common/message/pom.xml b/common/message/pom.xml index 757c5de251..834a041127 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC common org.thingsboard.common diff --git a/common/pom.xml b/common/pom.xml index a9195e49d4..563f1ec3ab 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC thingsboard common diff --git a/common/proto/pom.xml b/common/proto/pom.xml index 09f9596d0c..6357cae711 100644 --- a/common/proto/pom.xml +++ b/common/proto/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC common org.thingsboard.common diff --git a/common/queue/pom.xml b/common/queue/pom.xml index ffee41f8a9..ccc0eadf1a 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC common org.thingsboard.common diff --git a/common/script/pom.xml b/common/script/pom.xml index d0a3b47218..c203bfdbf2 100644 --- a/common/script/pom.xml +++ b/common/script/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC common org.thingsboard.common diff --git a/common/script/remote-js-client/pom.xml b/common/script/remote-js-client/pom.xml index b1547d2e4b..c2f5ee2da2 100644 --- a/common/script/remote-js-client/pom.xml +++ b/common/script/remote-js-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.3.0-SNAPSHOT + 4.2.0-RC script org.thingsboard.common.script diff --git a/common/script/script-api/pom.xml b/common/script/script-api/pom.xml index b0fcf1c4ae..e35dec1b1b 100644 --- a/common/script/script-api/pom.xml +++ b/common/script/script-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.3.0-SNAPSHOT + 4.2.0-RC script org.thingsboard.common.script diff --git a/common/stats/pom.xml b/common/stats/pom.xml index e44e098a39..090732d175 100644 --- a/common/stats/pom.xml +++ b/common/stats/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC common org.thingsboard.common diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index 37f33470b2..aaa9713e02 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.3.0-SNAPSHOT + 4.2.0-RC transport org.thingsboard.common.transport diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index e49af5b0a2..29d633d83e 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.3.0-SNAPSHOT + 4.2.0-RC transport org.thingsboard.common.transport diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index 1787c851cd..4d7013a2bc 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.3.0-SNAPSHOT + 4.2.0-RC transport org.thingsboard.common.transport diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index 9df8cf918e..2baee9bda2 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.3.0-SNAPSHOT + 4.2.0-RC transport org.thingsboard.common.transport diff --git a/common/transport/pom.xml b/common/transport/pom.xml index 3bd21c96de..2289b53aed 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC common org.thingsboard.common diff --git a/common/transport/snmp/pom.xml b/common/transport/snmp/pom.xml index 1286073a5f..71bd0c17fe 100644 --- a/common/transport/snmp/pom.xml +++ b/common/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.common - 4.3.0-SNAPSHOT + 4.2.0-RC transport diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index 19b075cb61..29ace644a9 100644 --- a/common/transport/transport-api/pom.xml +++ b/common/transport/transport-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.3.0-SNAPSHOT + 4.2.0-RC transport org.thingsboard.common.transport diff --git a/common/util/pom.xml b/common/util/pom.xml index 804ed0de38..3f07b2c05b 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC common org.thingsboard.common diff --git a/common/version-control/pom.xml b/common/version-control/pom.xml index 58c059915c..19278b8d13 100644 --- a/common/version-control/pom.xml +++ b/common/version-control/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC common org.thingsboard.common diff --git a/dao/pom.xml b/dao/pom.xml index fd6af62330..a32d0c4f6d 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC thingsboard dao diff --git a/edqs/pom.xml b/edqs/pom.xml index 4a4c05be71..c29710490b 100644 --- a/edqs/pom.xml +++ b/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC thingsboard edqs diff --git a/monitoring/pom.xml b/monitoring/pom.xml index 6dbcf85f07..cee8fc0a6b 100644 --- a/monitoring/pom.xml +++ b/monitoring/pom.xml @@ -21,7 +21,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC thingsboard diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index 358c524e06..84509ba48f 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC msa org.thingsboard.msa diff --git a/msa/edqs/pom.xml b/msa/edqs/pom.xml index 2dd4a9a584..d1f8291e2e 100644 --- a/msa/edqs/pom.xml +++ b/msa/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC msa org.thingsboard.msa diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index d7bac12742..6730da74e6 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC msa org.thingsboard.msa diff --git a/msa/monitoring/pom.xml b/msa/monitoring/pom.xml index cb5b609e43..61f492dca8 100644 --- a/msa/monitoring/pom.xml +++ b/msa/monitoring/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC msa diff --git a/msa/pom.xml b/msa/pom.xml index 7fbc5d3c02..58482a32cf 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC thingsboard msa diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index 8df294260b..5f53d868c9 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC msa org.thingsboard.msa diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index 1cbbf9d1ec..646b03bf14 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC msa org.thingsboard.msa diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml index 256220078d..4f510f937b 100644 --- a/msa/transport/coap/pom.xml +++ b/msa/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.3.0-SNAPSHOT + 4.2.0-RC transport org.thingsboard.msa.transport diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml index 7e5871729d..d9159cd0db 100644 --- a/msa/transport/http/pom.xml +++ b/msa/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.3.0-SNAPSHOT + 4.2.0-RC transport org.thingsboard.msa.transport diff --git a/msa/transport/lwm2m/pom.xml b/msa/transport/lwm2m/pom.xml index ca6977eac6..9972d66a18 100644 --- a/msa/transport/lwm2m/pom.xml +++ b/msa/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.3.0-SNAPSHOT + 4.2.0-RC transport org.thingsboard.msa.transport diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml index 172c4facb0..185addeb3b 100644 --- a/msa/transport/mqtt/pom.xml +++ b/msa/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.3.0-SNAPSHOT + 4.2.0-RC transport org.thingsboard.msa.transport diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml index a6cff4fd0b..193d01ef06 100644 --- a/msa/transport/pom.xml +++ b/msa/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC msa org.thingsboard.msa diff --git a/msa/transport/snmp/pom.xml b/msa/transport/snmp/pom.xml index 79734822aa..ea3d6847c0 100644 --- a/msa/transport/snmp/pom.xml +++ b/msa/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.msa transport - 4.3.0-SNAPSHOT + 4.2.0-RC org.thingsboard.msa.transport diff --git a/msa/vc-executor-docker/pom.xml b/msa/vc-executor-docker/pom.xml index 85634d8688..a77eb1d206 100644 --- a/msa/vc-executor-docker/pom.xml +++ b/msa/vc-executor-docker/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC msa org.thingsboard.msa diff --git a/msa/vc-executor/pom.xml b/msa/vc-executor/pom.xml index 0acd863612..0e91749cbe 100644 --- a/msa/vc-executor/pom.xml +++ b/msa/vc-executor/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC msa org.thingsboard.msa diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml index ce3475b113..da7215c5c5 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC msa org.thingsboard.msa diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index a09d1e2409..bbcfcfea18 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -19,11 +19,11 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC thingsboard netty-mqtt - 4.3.0-SNAPSHOT + 4.2.0-RC jar Netty MQTT Client diff --git a/pom.xml b/pom.xml index 21a5c4fb70..5d656214b8 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC pom Thingsboard diff --git a/rest-client/pom.xml b/rest-client/pom.xml index c3cf2460dd..07499f1129 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC thingsboard rest-client diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml index 7abb7be3a3..120d27ac89 100644 --- a/rule-engine/pom.xml +++ b/rule-engine/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC thingsboard rule-engine diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index 59397d4a92..a6ee3492e5 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 60615c21e2..530cbfe7d3 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC rule-engine org.thingsboard.rule-engine diff --git a/tools/pom.xml b/tools/pom.xml index 0b63d84b0a..1224e73777 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC thingsboard tools diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index 67f16917d3..c96f4fddf7 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC transport org.thingsboard.transport diff --git a/transport/http/pom.xml b/transport/http/pom.xml index 371ecb84ca..e32915c51e 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC transport org.thingsboard.transport diff --git a/transport/lwm2m/pom.xml b/transport/lwm2m/pom.xml index 66a3066a88..af17bf703b 100644 --- a/transport/lwm2m/pom.xml +++ b/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC transport org.thingsboard.transport diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index 57a73b1bbe..87c98f9d78 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC transport org.thingsboard.transport diff --git a/transport/pom.xml b/transport/pom.xml index a8658bbf40..8ddcf992d8 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC thingsboard transport diff --git a/transport/snmp/pom.xml b/transport/snmp/pom.xml index d5c3d1fe02..09901661a6 100644 --- a/transport/snmp/pom.xml +++ b/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC transport diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml index f2aadd3887..ca8094430a 100644 --- a/ui-ngx/pom.xml +++ b/ui-ngx/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.0-RC thingsboard org.thingsboard From 418cecf016be65c902adcccc0a32408cbc80c1fd Mon Sep 17 00:00:00 2001 From: Igor Kulikov Date: Thu, 24 Jul 2025 12:58:19 +0300 Subject: [PATCH 013/216] Version 4.3.0-SNAPSHOT --- application/pom.xml | 2 +- common/actor/pom.xml | 2 +- common/cache/pom.xml | 2 +- common/cluster-api/pom.xml | 2 +- common/coap-server/pom.xml | 2 +- common/dao-api/pom.xml | 2 +- common/data/pom.xml | 2 +- common/discovery-api/pom.xml | 2 +- common/edge-api/pom.xml | 2 +- common/edqs/pom.xml | 2 +- common/message/pom.xml | 2 +- common/pom.xml | 2 +- common/proto/pom.xml | 2 +- common/queue/pom.xml | 2 +- common/script/pom.xml | 2 +- common/script/remote-js-client/pom.xml | 2 +- common/script/script-api/pom.xml | 2 +- common/stats/pom.xml | 2 +- common/transport/coap/pom.xml | 2 +- common/transport/http/pom.xml | 2 +- common/transport/lwm2m/pom.xml | 2 +- common/transport/mqtt/pom.xml | 2 +- common/transport/pom.xml | 2 +- common/transport/snmp/pom.xml | 2 +- common/transport/transport-api/pom.xml | 2 +- common/util/pom.xml | 2 +- common/version-control/pom.xml | 2 +- dao/pom.xml | 2 +- edqs/pom.xml | 2 +- monitoring/pom.xml | 2 +- msa/black-box-tests/pom.xml | 2 +- msa/edqs/pom.xml | 2 +- msa/js-executor/pom.xml | 2 +- msa/monitoring/pom.xml | 2 +- msa/pom.xml | 2 +- msa/tb-node/pom.xml | 2 +- msa/tb/pom.xml | 2 +- msa/transport/coap/pom.xml | 2 +- msa/transport/http/pom.xml | 2 +- msa/transport/lwm2m/pom.xml | 2 +- msa/transport/mqtt/pom.xml | 2 +- msa/transport/pom.xml | 2 +- msa/transport/snmp/pom.xml | 2 +- msa/vc-executor-docker/pom.xml | 2 +- msa/vc-executor/pom.xml | 2 +- msa/web-ui/pom.xml | 2 +- netty-mqtt/pom.xml | 4 ++-- pom.xml | 2 +- rest-client/pom.xml | 2 +- rule-engine/pom.xml | 2 +- rule-engine/rule-engine-api/pom.xml | 2 +- rule-engine/rule-engine-components/pom.xml | 2 +- tools/pom.xml | 2 +- transport/coap/pom.xml | 2 +- transport/http/pom.xml | 2 +- transport/lwm2m/pom.xml | 2 +- transport/mqtt/pom.xml | 2 +- transport/pom.xml | 2 +- transport/snmp/pom.xml | 2 +- ui-ngx/pom.xml | 2 +- 60 files changed, 61 insertions(+), 61 deletions(-) diff --git a/application/pom.xml b/application/pom.xml index 33bc0972d4..dba7d27c1b 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT thingsboard application diff --git a/common/actor/pom.xml b/common/actor/pom.xml index a6efdda77d..92b174ea61 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/cache/pom.xml b/common/cache/pom.xml index 3088dad098..fcf52e01b7 100644 --- a/common/cache/pom.xml +++ b/common/cache/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index 939dfb1e16..1f4b5ff41b 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml index fbfbe78b55..27ff645a0f 100644 --- a/common/coap-server/pom.xml +++ b/common/coap-server/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index 636c7b9ef4..160351d55d 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/data/pom.xml b/common/data/pom.xml index da2a0970b1..779565ed8e 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/discovery-api/pom.xml b/common/discovery-api/pom.xml index 4f9ef70c5b..3ae12c1bdb 100644 --- a/common/discovery-api/pom.xml +++ b/common/discovery-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/edge-api/pom.xml b/common/edge-api/pom.xml index 69a01be812..7c2967cf3d 100644 --- a/common/edge-api/pom.xml +++ b/common/edge-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/edqs/pom.xml b/common/edqs/pom.xml index 140eabd270..f7616d5b37 100644 --- a/common/edqs/pom.xml +++ b/common/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/message/pom.xml b/common/message/pom.xml index 834a041127..757c5de251 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/pom.xml b/common/pom.xml index 563f1ec3ab..a9195e49d4 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT thingsboard common diff --git a/common/proto/pom.xml b/common/proto/pom.xml index 6357cae711..09f9596d0c 100644 --- a/common/proto/pom.xml +++ b/common/proto/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/queue/pom.xml b/common/queue/pom.xml index ccc0eadf1a..ffee41f8a9 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/script/pom.xml b/common/script/pom.xml index c203bfdbf2..d0a3b47218 100644 --- a/common/script/pom.xml +++ b/common/script/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/script/remote-js-client/pom.xml b/common/script/remote-js-client/pom.xml index c2f5ee2da2..b1547d2e4b 100644 --- a/common/script/remote-js-client/pom.xml +++ b/common/script/remote-js-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-RC + 4.3.0-SNAPSHOT script org.thingsboard.common.script diff --git a/common/script/script-api/pom.xml b/common/script/script-api/pom.xml index e35dec1b1b..b0fcf1c4ae 100644 --- a/common/script/script-api/pom.xml +++ b/common/script/script-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-RC + 4.3.0-SNAPSHOT script org.thingsboard.common.script diff --git a/common/stats/pom.xml b/common/stats/pom.xml index 090732d175..e44e098a39 100644 --- a/common/stats/pom.xml +++ b/common/stats/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index aaa9713e02..37f33470b2 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-RC + 4.3.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index 29d633d83e..e49af5b0a2 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-RC + 4.3.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index 4d7013a2bc..1787c851cd 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-RC + 4.3.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index 2baee9bda2..9df8cf918e 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-RC + 4.3.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/pom.xml b/common/transport/pom.xml index 2289b53aed..3bd21c96de 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/snmp/pom.xml b/common/transport/snmp/pom.xml index 71bd0c17fe..1286073a5f 100644 --- a/common/transport/snmp/pom.xml +++ b/common/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.common - 4.2.0-RC + 4.3.0-SNAPSHOT transport diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index 29ace644a9..19b075cb61 100644 --- a/common/transport/transport-api/pom.xml +++ b/common/transport/transport-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-RC + 4.3.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/util/pom.xml b/common/util/pom.xml index 3f07b2c05b..804ed0de38 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/version-control/pom.xml b/common/version-control/pom.xml index 19278b8d13..58c059915c 100644 --- a/common/version-control/pom.xml +++ b/common/version-control/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/dao/pom.xml b/dao/pom.xml index a32d0c4f6d..fd6af62330 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT thingsboard dao diff --git a/edqs/pom.xml b/edqs/pom.xml index c29710490b..4a4c05be71 100644 --- a/edqs/pom.xml +++ b/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT thingsboard edqs diff --git a/monitoring/pom.xml b/monitoring/pom.xml index cee8fc0a6b..6dbcf85f07 100644 --- a/monitoring/pom.xml +++ b/monitoring/pom.xml @@ -21,7 +21,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT thingsboard diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index 84509ba48f..358c524e06 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/edqs/pom.xml b/msa/edqs/pom.xml index d1f8291e2e..2dd4a9a584 100644 --- a/msa/edqs/pom.xml +++ b/msa/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index 6730da74e6..d7bac12742 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/monitoring/pom.xml b/msa/monitoring/pom.xml index 61f492dca8..cb5b609e43 100644 --- a/msa/monitoring/pom.xml +++ b/msa/monitoring/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT msa diff --git a/msa/pom.xml b/msa/pom.xml index 58482a32cf..7fbc5d3c02 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT thingsboard msa diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index 5f53d868c9..8df294260b 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index 646b03bf14..1cbbf9d1ec 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml index 4f510f937b..256220078d 100644 --- a/msa/transport/coap/pom.xml +++ b/msa/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.0-RC + 4.3.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml index d9159cd0db..7e5871729d 100644 --- a/msa/transport/http/pom.xml +++ b/msa/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.0-RC + 4.3.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/lwm2m/pom.xml b/msa/transport/lwm2m/pom.xml index 9972d66a18..ca6977eac6 100644 --- a/msa/transport/lwm2m/pom.xml +++ b/msa/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.0-RC + 4.3.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml index 185addeb3b..172c4facb0 100644 --- a/msa/transport/mqtt/pom.xml +++ b/msa/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.0-RC + 4.3.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml index 193d01ef06..a6cff4fd0b 100644 --- a/msa/transport/pom.xml +++ b/msa/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/snmp/pom.xml b/msa/transport/snmp/pom.xml index ea3d6847c0..79734822aa 100644 --- a/msa/transport/snmp/pom.xml +++ b/msa/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.msa transport - 4.2.0-RC + 4.3.0-SNAPSHOT org.thingsboard.msa.transport diff --git a/msa/vc-executor-docker/pom.xml b/msa/vc-executor-docker/pom.xml index a77eb1d206..85634d8688 100644 --- a/msa/vc-executor-docker/pom.xml +++ b/msa/vc-executor-docker/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/vc-executor/pom.xml b/msa/vc-executor/pom.xml index 0e91749cbe..0acd863612 100644 --- a/msa/vc-executor/pom.xml +++ b/msa/vc-executor/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml index da7215c5c5..ce3475b113 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index bbcfcfea18..a09d1e2409 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -19,11 +19,11 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT thingsboard netty-mqtt - 4.2.0-RC + 4.3.0-SNAPSHOT jar Netty MQTT Client diff --git a/pom.xml b/pom.xml index 5d656214b8..21a5c4fb70 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT pom Thingsboard diff --git a/rest-client/pom.xml b/rest-client/pom.xml index 07499f1129..c3cf2460dd 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT thingsboard rest-client diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml index 120d27ac89..7abb7be3a3 100644 --- a/rule-engine/pom.xml +++ b/rule-engine/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT thingsboard rule-engine diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index a6ee3492e5..59397d4a92 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 530cbfe7d3..60615c21e2 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/tools/pom.xml b/tools/pom.xml index 1224e73777..0b63d84b0a 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT thingsboard tools diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index c96f4fddf7..67f16917d3 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/http/pom.xml b/transport/http/pom.xml index e32915c51e..371ecb84ca 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/lwm2m/pom.xml b/transport/lwm2m/pom.xml index af17bf703b..66a3066a88 100644 --- a/transport/lwm2m/pom.xml +++ b/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index 87c98f9d78..57a73b1bbe 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/pom.xml b/transport/pom.xml index 8ddcf992d8..a8658bbf40 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT thingsboard transport diff --git a/transport/snmp/pom.xml b/transport/snmp/pom.xml index 09901661a6..d5c3d1fe02 100644 --- a/transport/snmp/pom.xml +++ b/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT transport diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml index ca8094430a..f2aadd3887 100644 --- a/ui-ngx/pom.xml +++ b/ui-ngx/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.3.0-SNAPSHOT thingsboard org.thingsboard From b191c2a2ec5932d39b62370da2889c116e59833e Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Tue, 29 Jul 2025 17:07:58 +0300 Subject: [PATCH 014/216] Edge Misordering Compensation Config - Moved misordering compensation to configuration for better control of edge event handling and data consistency --- .../server/service/edge/rpc/EdgeEventStorageSettings.java | 2 ++ .../server/service/edge/rpc/EdgeGrpcSession.java | 2 +- .../service/edge/rpc/fetch/GeneralEdgeEventFetcher.java | 8 +++----- application/src/main/resources/thingsboard.yml | 3 +++ 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java index 89d19438bd..d9e3f1a920 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java @@ -29,4 +29,6 @@ public class EdgeEventStorageSettings { private long noRecordsSleepInterval; @Value("${edges.storage.sleep_between_batches}") private long sleepIntervalBetweenBatches; + @Value("${edges.storage.misordering_compensation_millis:3600000}") + private long misorderingCompensationMillis; } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 521730741f..03732bed64 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -589,7 +589,7 @@ public abstract class EdgeGrpcSession implements Closeable { previousStartSeqId, false, Integer.toUnsignedLong(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()), - ctx.getEdgeEventService()); + ctx.getEdgeEventService(), ctx.getEdgeEventStorageSettings().getMisorderingCompensationMillis()); log.trace("[{}][{}] starting processing edge events, previousStartTs = {}, previousStartSeqId = {}", tenantId, edge.getId(), previousStartTs, previousStartSeqId); Futures.addCallback(startProcessingEdgeEvents(fetcher), new FutureCallback<>() { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java index 5d7df601b5..e3fa970284 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java @@ -26,13 +26,9 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.dao.edge.EdgeEventService; -import java.util.concurrent.TimeUnit; - @AllArgsConstructor @Slf4j public class GeneralEdgeEventFetcher implements EdgeEventFetcher { - // Subtract from queueStartTs to ensure no data is lost due to potential misordering of edge events by created_time. - private static final long MISORDERING_COMPENSATION_MILLIS = TimeUnit.SECONDS.toMillis(60); private final Long queueStartTs; private Long seqIdStart; @@ -40,6 +36,8 @@ public class GeneralEdgeEventFetcher implements EdgeEventFetcher { private boolean seqIdNewCycleStarted; private Long maxReadRecordsCount; private final EdgeEventService edgeEventService; + // Subtract from queueStartTs to ensure no data is lost due to potential misordering of edge events by created_time. + private final long misorderingCompensationMillis; @Override public PageLink getPageLink(int pageSize) { @@ -48,7 +46,7 @@ public class GeneralEdgeEventFetcher implements EdgeEventFetcher { 0, null, null, - queueStartTs > 0 ? queueStartTs - MISORDERING_COMPENSATION_MILLIS : 0, + queueStartTs > 0 ? queueStartTs - misorderingCompensationMillis : 0, System.currentTimeMillis()); } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 0c1d419da2..61ed93b03c 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1480,6 +1480,9 @@ edges: no_read_records_sleep: "${EDGES_NO_READ_RECORDS_SLEEP:1000}" # Number of milliseconds to wait before resending failed batch of edge events to edge sleep_between_batches: "${EDGES_SLEEP_BETWEEN_BATCHES:60000}" + # Number of milliseconds to subtract from queue start timestamp to compensate for potential + # misordering of edge events by created_time. Prevents skipping early events. + misordering_compensation_millis: "${EDGES_MISORDERING_COMPENSATION_MILLIS:3600000}" # Max number of high priority edge events per edge session. No persistence - stored in memory max_high_priority_queue_size_per_session: "${EDGES_MAX_HIGH_PRIORITY_QUEUE_SIZE_PER_SESSION:10000}" # Number of threads that are used to check DB for edge events From a101c9403ffb8f496e77ba5d616655153f3ca0d1 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Tue, 29 Jul 2025 17:44:15 +0300 Subject: [PATCH 015/216] Refactoring --- .../thingsboard/server/service/edge/rpc/EdgeGrpcSession.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index 03732bed64..a86409c6af 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -589,7 +589,8 @@ public abstract class EdgeGrpcSession implements Closeable { previousStartSeqId, false, Integer.toUnsignedLong(ctx.getEdgeEventStorageSettings().getMaxReadRecordsCount()), - ctx.getEdgeEventService(), ctx.getEdgeEventStorageSettings().getMisorderingCompensationMillis()); + ctx.getEdgeEventService(), + ctx.getEdgeEventStorageSettings().getMisorderingCompensationMillis()); log.trace("[{}][{}] starting processing edge events, previousStartTs = {}, previousStartSeqId = {}", tenantId, edge.getId(), previousStartTs, previousStartSeqId); Futures.addCallback(startProcessingEdgeEvents(fetcher), new FutureCallback<>() { From 2c7c63c537e5f4c0685fad5e699042ee511f7e68 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Tue, 29 Jul 2025 18:36:07 +0300 Subject: [PATCH 016/216] Refactoring --- .../service/edge/rpc/fetch/GeneralEdgeEventFetcher.java | 4 +++- application/src/main/resources/thingsboard.yml | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java index e3fa970284..b4f894c422 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/GeneralEdgeEventFetcher.java @@ -36,7 +36,9 @@ public class GeneralEdgeEventFetcher implements EdgeEventFetcher { private boolean seqIdNewCycleStarted; private Long maxReadRecordsCount; private final EdgeEventService edgeEventService; - // Subtract from queueStartTs to ensure no data is lost due to potential misordering of edge events by created_time. + // Subtract from queueStartTs to compensate for possible misalignment between `created_time` and `seqId`. + // This ensures early events with lower seqId are not skipped due to partitioning by `created_time`. + // See: edge_event is partitioned by created_time but sorted by seqId during retrieval. private final long misorderingCompensationMillis; @Override diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 61ed93b03c..d36a890861 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1480,8 +1480,10 @@ edges: no_read_records_sleep: "${EDGES_NO_READ_RECORDS_SLEEP:1000}" # Number of milliseconds to wait before resending failed batch of edge events to edge sleep_between_batches: "${EDGES_SLEEP_BETWEEN_BATCHES:60000}" - # Number of milliseconds to subtract from queue start timestamp to compensate for potential - # misordering of edge events by created_time. Prevents skipping early events. + # Time (in milliseconds) to subtract from the start timestamp when fetching edge events. + # This compensates for possible misordering between `created_time` (used for partitioning) + # and `seqId` (used for sorting). Without this, events with smaller seqId but larger created_time + # might be skipped, especially across partition boundaries. misordering_compensation_millis: "${EDGES_MISORDERING_COMPENSATION_MILLIS:3600000}" # Max number of high priority edge events per edge session. No persistence - stored in memory max_high_priority_queue_size_per_session: "${EDGES_MAX_HIGH_PRIORITY_QUEUE_SIZE_PER_SESSION:10000}" From 2e4b917e09096337c0af3af5368606a9364bd81f Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Thu, 31 Jul 2025 16:00:13 +0300 Subject: [PATCH 017/216] Refactoring --- application/src/main/resources/thingsboard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index d36a890861..b515884842 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1484,7 +1484,7 @@ edges: # This compensates for possible misordering between `created_time` (used for partitioning) # and `seqId` (used for sorting). Without this, events with smaller seqId but larger created_time # might be skipped, especially across partition boundaries. - misordering_compensation_millis: "${EDGES_MISORDERING_COMPENSATION_MILLIS:3600000}" + misordering_compensation_millis: "${EDGES_MISORDERING_COMPENSATION_MILLIS:60000}" # Max number of high priority edge events per edge session. No persistence - stored in memory max_high_priority_queue_size_per_session: "${EDGES_MAX_HIGH_PRIORITY_QUEUE_SIZE_PER_SESSION:10000}" # Number of threads that are used to check DB for edge events From c8490080a1007f3548da8e7ab80965d56783211a Mon Sep 17 00:00:00 2001 From: dshvaika Date: Fri, 1 Aug 2025 13:12:44 +0300 Subject: [PATCH 018/216] added basic logic to update state periodically --- .../CalculatedFieldEntityActor.java | 5 +- ...CalculatedFieldEntityMessageProcessor.java | 55 ++++++--- .../CalculatedFieldManagerActor.java | 3 + ...alculatedFieldManagerMessageProcessor.java | 76 ++++++++++++ ...latedFieldScheduledCheckForUpdatesMsg.java | 35 ++++++ ...tityCalculatedFieldCheckForUpdatesMsg.java | 37 ++++++ ...faultCalculatedFieldProcessingService.java | 5 +- .../ctx/state/BaseCalculatedFieldState.java | 14 --- .../cf/ctx/state/CalculatedFieldCtx.java | 48 +++++--- .../cf/ctx/state/CalculatedFieldState.java | 14 ++- .../cf/ctx/state/GeofencingArgumentEntry.java | 62 ++++++---- .../state/GeofencingCalculatedFieldState.java | 108 +++--------------- .../cf/ctx/state/GeofencingZoneState.java | 94 +++++++++++++++ .../server/utils/CalculatedFieldUtils.java | 67 ++++++++++- application/src/main/resources/logback.xml | 1 + .../server/cluster/TbClusterService.java | 3 +- .../BaseCalculatedFieldConfiguration.java | 5 + .../CalculatedFieldConfiguration.java | 12 ++ ...eofencingCalculatedFieldConfiguration.java | 6 + .../server/common/msg/MsgType.java | 5 +- common/proto/src/main/proto/queue.proto | 22 ++++ .../script/api/tbel/TbelCfArg.java | 3 +- .../api/tbel/TbelCfTsGeofencingArg.java | 21 +++- .../common/util/geo/PerimeterDefinition.java | 1 + 24 files changed, 531 insertions(+), 171 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldScheduledCheckForUpdatesMsg.java create mode 100644 application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldCheckForUpdatesMsg.java create mode 100644 application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java rename application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldInitService.java => common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsGeofencingArg.java (62%) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java index 2959bfc8eb..4812ed6652 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java @@ -51,7 +51,7 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor { @Override public void destroy(TbActorStopReason stopReason, Throwable cause) throws TbActorException { log.debug("[{}] Stopping CF entity actor.", processor.tenantId); - processor.stop(); + processor.stop(false); } @Override @@ -75,6 +75,9 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor { case CF_LINKED_TELEMETRY_MSG: processor.process((EntityCalculatedFieldLinkedTelemetryMsg) msg); break; + case CF_ENTITY_CHECK_FOR_UPDATES_MSG: + processor.process((EntityCalculatedFieldCheckForUpdatesMsg) msg); + break; default: return false; } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 75582ef4ba..68c0471cb7 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -59,7 +59,9 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; @@ -91,16 +93,18 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM this.ctx = ctx; } - public void stop() { - log.info("[{}][{}] Stopping entity actor.", tenantId, entityId); + public void stop(boolean partitionChanged) { + log.info(partitionChanged ? + "[{}][{}] Stopping entity actor due to change partition event." : + "[{}][{}] Stopping entity actor.", + tenantId, entityId); states.clear(); ctx.stop(ctx.getSelf()); } public void process(CalculatedFieldPartitionChangeMsg msg) { if (!systemContext.getPartitionService().resolve(ServiceType.TB_RULE_ENGINE, DataConstants.CF_QUEUE_NAME, tenantId, entityId).isMyPartition()) { - log.info("[{}] Stopping entity actor due to change partition event.", entityId); - ctx.stop(ctx.getSelf()); + stop(true); } } @@ -224,6 +228,25 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } + public void process(EntityCalculatedFieldCheckForUpdatesMsg msg) throws CalculatedFieldException { + CalculatedFieldCtx cfCtx = msg.getCfCtx(); + CalculatedFieldId cfId = cfCtx.getCfId(); + log.debug("[{}] [{}] Processing CF dynamic sources refresh msg.", entityId, cfId); + try { + var state = updateStateFromDb(cfCtx); + if (state.isSizeOk()) { + processStateIfReady(cfCtx, Collections.singletonList(cfId), state, null, null, msg.getCallback()); + } else { + throw new RuntimeException(cfCtx.getSizeExceedsLimitMessage()); + } + } catch (Exception e) { + if (e instanceof CalculatedFieldException cfe) { + throw cfe; + } + throw CalculatedFieldException.builder().ctx(cfCtx).eventEntity(entityId).cause(e).build(); + } + } + private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto)); } @@ -270,16 +293,19 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM CalculatedFieldState state = states.get(ctx.getCfId()); if (state != null) { return state; - } else { - ListenableFuture stateFuture = systemContext.getCalculatedFieldProcessingService().fetchStateFromDb(ctx, entityId); - // Ugly but necessary. We do not expect to often fetch data from DB. Only once per pair lifetime. - // This call happens while processing the CF pack from the queue consumer. So the timeout should be relatively low. - // Alternatively, we can fetch the state outside the actor system and push separate command to create this actor, - // but this will significantly complicate the code. - state = stateFuture.get(1, TimeUnit.MINUTES); - state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); - states.put(ctx.getCfId(), state); } + return updateStateFromDb(ctx); + } + + private CalculatedFieldState updateStateFromDb(CalculatedFieldCtx ctx) throws InterruptedException, ExecutionException, TimeoutException { + ListenableFuture stateFuture = cfService.fetchStateFromDb(ctx, entityId); + // Ugly but necessary. We do not expect to often fetch data from DB. Only once per pair lifetime. + // This call happens while processing the CF pack from the queue consumer. So the timeout should be relatively low. + // Alternatively, we can fetch the state outside the actor system and push separate command to create this actor, + // but this will significantly complicate the code. + CalculatedFieldState state = stateFuture.get(1, TimeUnit.MINUTES); + state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); + states.put(ctx.getCfId(), state); return state; } @@ -297,12 +323,11 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } else { TbCallback effectiveCallback = calculationResults.size() > 1 ? new MultipleTbCallback(calculationResults.size(), callback) : callback; - for (CalculatedFieldResult calculationResult : calculationResults) { if (calculationResult.isEmpty()) { effectiveCallback.onSuccess(); } else { - cfService.pushMsgToRuleEngine(tenantId, entityId, calculationResult, cfIdList, callback); + cfService.pushMsgToRuleEngine(tenantId, entityId, calculationResult, cfIdList, effectiveCallback); } } } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java index 9f59a80e67..5adca78fa9 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java @@ -91,6 +91,9 @@ public class CalculatedFieldManagerActor extends AbstractCalculatedFieldActor { case CF_LINKED_TELEMETRY_MSG: processor.onLinkedTelemetryMsg((CalculatedFieldLinkedTelemetryMsg) msg); break; + case CF_SCHEDULED_CHECK_FOR_UPDATES_MSG: + processor.onScheduledCheckForUpdatesMsg((CalculatedFieldScheduledCheckForUpdatesMsg) msg); + break; default: return false; } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index ba1ca71bda..03de43d08b 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; +import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.DeviceId; @@ -59,7 +60,10 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto; @@ -72,6 +76,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private final Map calculatedFields = new HashMap<>(); private final Map> entityIdCalculatedFields = new HashMap<>(); private final Map> entityIdCalculatedFieldLinks = new HashMap<>(); + private final Map> checkForCalculatedFieldUpdateTasks = new ConcurrentHashMap<>(); private final CalculatedFieldProcessingService cfExecService; private final CalculatedFieldStateService cfStateService; @@ -110,6 +115,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware calculatedFields.clear(); entityIdCalculatedFields.clear(); entityIdCalculatedFieldLinks.clear(); + checkForCalculatedFieldUpdateTasks.values().forEach(future -> future.cancel(true)); + checkForCalculatedFieldUpdateTasks.clear(); ctx.stop(ctx.getSelf()); } @@ -117,6 +124,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware log.debug("[{}] Processing CF actor init message.", msg.getTenantId().getId()); initEntityProfileCache(); initCalculatedFields(); + // TODO: implement cache for 1:1 relations to use in the CFs that based on a relation queries? msg.getCallback().onSuccess(); } @@ -139,6 +147,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware // We use copy on write lists to safely pass the reference to another actor for the iteration. // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx); + scheduleCalculatedFieldUpdateMsgIfNeeded(cfCtx); msg.getCallback().onSuccess(); } @@ -324,6 +333,11 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } calculatedFields.put(newCf.getId(), newCfCtx); List oldCfList = entityIdCalculatedFields.get(newCf.getEntityId()); + + if (newCfCtx.hasSchedulingConfigChanges(oldCfCtx)) { + cancelCfUpdateTaskIfExists(cfId, false); + } + List newCfList = new CopyOnWriteArrayList<>(); boolean found = false; for (CalculatedFieldCtx oldCtx : oldCfList) { @@ -364,6 +378,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx); deleteLinks(cfCtx); + cancelCfUpdateTaskIfExists(cfId, true); + EntityId entityId = cfCtx.getEntityId(); EntityType entityType = cfCtx.getEntityId().getEntityType(); if (isProfileEntity(entityType)) { @@ -387,6 +403,15 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } } + private void cancelCfUpdateTaskIfExists(CalculatedFieldId cfId, boolean cfDeleted) { + var existingTask = checkForCalculatedFieldUpdateTasks.remove(cfId); + if (existingTask != null) { + existingTask.cancel(false); + String reason = cfDeleted ? "removal" : "update"; + log.debug("[{}][{}] Cancelled check for update task for CF due to: " + reason + "!", tenantId, cfId); + } + } + public void onTelemetryMsg(CalculatedFieldTelemetryMsg msg) { EntityId entityId = msg.getEntityId(); log.debug("Received telemetry msg from entity [{}]", entityId); @@ -498,16 +523,66 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware initCfForEntity(id, cfCtx, forceStateReinit, multiCallback); } }); + scheduleCalculatedFieldUpdateMsgIfNeeded(cfCtx); } else { callback.onSuccess(); } } else { if (isMyPartition(entityId, callback)) { initCfForEntity(entityId, cfCtx, forceStateReinit, callback); + scheduleCalculatedFieldUpdateMsgIfNeeded(cfCtx); + } + } + } + + private void scheduleCalculatedFieldUpdateMsgIfNeeded(CalculatedFieldCtx cfCtx) { + CalculatedField cf = cfCtx.getCalculatedField(); + CalculatedFieldConfiguration cfConfig = cf.getConfiguration(); + if (!cfConfig.isDynamicRefreshEnabled()) { + return; + } + if (checkForCalculatedFieldUpdateTasks.containsKey(cf.getId())) { + log.debug("[{}][{}] Check for update msg for CF is already scheduled!", tenantId, cf.getId()); + return; + } + long refreshDynamicSourceInterval = TimeUnit.SECONDS.toMillis(cfConfig.getRefreshIntervalSec()); + var scheduledMsg = new CalculatedFieldScheduledCheckForUpdatesMsg(tenantId, cfCtx); + + ScheduledFuture scheduledFuture = systemContext + .schedulePeriodicMsgWithDelay(ctx, scheduledMsg, refreshDynamicSourceInterval, refreshDynamicSourceInterval); + checkForCalculatedFieldUpdateTasks.put(cf.getId(), scheduledFuture); + log.debug("[{}][{}] Scheduled check for update msg for CF!", tenantId, cf.getId()); + } + + public void onScheduledCheckForUpdatesMsg(CalculatedFieldScheduledCheckForUpdatesMsg msg) { + CalculatedFieldCtx cfCtx = msg.getCfCtx(); + EntityId entityId = cfCtx.getEntityId(); + log.debug("[{}] [{}] Processing CF scheduled update msg.", cfCtx.getCfId(), entityId); + EntityType entityType = entityId.getEntityType(); + if (isProfileEntity(entityType)) { + var entityIds = entityProfileCache.getEntityIdsByProfileId(entityId); + if (!entityIds.isEmpty()) { + var multiCallback = new MultipleTbCallback(entityIds.size(), msg.getCallback()); + entityIds.forEach(id -> { + if (isMyPartition(id, multiCallback)) { + updateCfWithDynamicSourceForEntity(id, cfCtx, multiCallback); + } + }); + } else { + msg.getCallback().onSuccess(); + } + } else { + if (isMyPartition(entityId, msg.getCallback())) { + updateCfWithDynamicSourceForEntity(entityId, cfCtx, msg.getCallback()); } } } + private void updateCfWithDynamicSourceForEntity(EntityId entityId, CalculatedFieldCtx cfCtx, TbCallback callback) { + log.debug("Pushing entity dynamic source refresh CF msg to specific actor [{}]", entityId); + getOrCreateActor(entityId).tell(new EntityCalculatedFieldCheckForUpdatesMsg(tenantId, cfCtx, callback)); + } + private void deleteCfForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) { log.debug("Pushing delete CF msg to specific actor [{}]", entityId); getOrCreateActor(entityId).tell(new CalculatedFieldEntityDeleteMsg(tenantId, cfId, callback)); @@ -571,6 +646,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware log.error("Failed to process calculated field record: {}", cf, e); } }); + // TODO: why we need to do this loop if we do this inside the onFieldInitMsg? calculatedFields.values().forEach(cf -> { entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cf); }); diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldScheduledCheckForUpdatesMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldScheduledCheckForUpdatesMsg.java new file mode 100644 index 0000000000..f53d2e7572 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldScheduledCheckForUpdatesMsg.java @@ -0,0 +1,35 @@ +/** + * 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.actors.calculatedField; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.MsgType; +import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; + +@Data +public class CalculatedFieldScheduledCheckForUpdatesMsg implements ToCalculatedFieldSystemMsg { + + private final TenantId tenantId; + private final CalculatedFieldCtx cfCtx; + + @Override + public MsgType getMsgType() { + return MsgType.CF_SCHEDULED_CHECK_FOR_UPDATES_MSG; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldCheckForUpdatesMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldCheckForUpdatesMsg.java new file mode 100644 index 0000000000..908680c068 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldCheckForUpdatesMsg.java @@ -0,0 +1,37 @@ +/** + * 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.actors.calculatedField; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.msg.MsgType; +import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; +import org.thingsboard.server.common.msg.queue.TbCallback; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; + +@Data +public class EntityCalculatedFieldCheckForUpdatesMsg implements ToCalculatedFieldSystemMsg { + + private final TenantId tenantId; + private final CalculatedFieldCtx cfCtx; + private final TbCallback callback; + + @Override + public MsgType getMsgType() { + return MsgType.CF_ENTITY_CHECK_FOR_UPDATES_MSG; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index 9d75692718..424df50009 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -140,7 +140,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP case SAVE_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY -> { var resolvedEntityIdsFuture = resolveGeofencingEntityIds(ctx.getTenantId(), entityId, entry); argFutures.put(entry.getKey(), Futures.transformAsync(resolvedEntityIdsFuture, resolvedEntityIds -> - fetchGeofencingKvEntry(ctx.getTenantId(), resolvedEntityIds, entry.getValue()), MoreExecutors.directExecutor())); + fetchGeofencingKvEntry(ctx.getTenantId(), resolvedEntityIds, entry.getValue()), calculatedFieldCallbackExecutor)); } } } @@ -288,7 +288,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP case RELATION_QUERY -> { var relationQueryDynamicSourceConfiguration = (RelationQueryDynamicSourceConfiguration) value.getRefDynamicSourceConfiguration(); yield Futures.transform(relationService.findByQuery(tenantId, relationQueryDynamicSourceConfiguration.toEntityRelationsQuery(entityId)), - relationQueryDynamicSourceConfiguration::resolveEntityIds, MoreExecutors.directExecutor()); + relationQueryDynamicSourceConfiguration::resolveEntityIds, calculatedFieldCallbackExecutor); } }; } @@ -298,7 +298,6 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP if (argument.getRefEntityKey().getType() != ArgumentType.ATTRIBUTE) { throw new IllegalStateException("Unsupported argument key type: " + argument.getRefEntityKey().getType()); } - List>> kvFutures = geofencingEntities.stream() .map(entityId -> { var attributesFuture = attributesService.find( diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index e21d56b6d2..eb87d375c5 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -25,8 +25,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import static org.thingsboard.server.utils.CalculatedFieldUtils.toSingleValueArgumentProto; - @Data @AllArgsConstructor public abstract class BaseCalculatedFieldState implements CalculatedFieldState { @@ -95,18 +93,6 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { } } - @Override - public void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx) { - if (entry instanceof TsRollingArgumentEntry) { - return; - } - if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { - if (ctx.getMaxSingleValueArgumentSize() > 0 && toSingleValueArgumentProto(name, singleValueArgumentEntry).getSerializedSize() > ctx.getMaxSingleValueArgumentSize()) { - throw new IllegalArgumentException("Single value size exceeds the maximum allowed limit. The argument will not be used for calculation."); - } - } - } - protected abstract void validateNewEntry(ArgumentEntry newEntry); private void updateLastUpdateTimestamp(ArgumentEntry entry) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 89f76bd482..8124d78d3a 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -109,25 +109,29 @@ public class CalculatedFieldCtx { } public void init() { - if (CalculatedFieldType.SCRIPT.equals(cfType)) { - try { - this.calculatedFieldScriptEngine = initEngine(tenantId, expression, tbelInvokeService); - initialized = true; - } catch (Exception e) { - throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax.", e); + switch (cfType) { + case SCRIPT -> { + try { + this.calculatedFieldScriptEngine = initEngine(tenantId, expression, tbelInvokeService); + initialized = true; + } catch (Exception e) { + throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax.", e); + } } - } else { - if (isValidExpression(expression)) { - this.customExpression = ThreadLocal.withInitial(() -> - new ExpressionBuilder(expression) - .functions(userDefinedFunctions) - .implicitMultiplication(true) - .variables(this.arguments.keySet()) - .build() - ); - initialized = true; - } else { - throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax."); + case GEOFENCING -> initialized = true; + default -> { + if (isValidExpression(expression)) { + this.customExpression = ThreadLocal.withInitial(() -> + new ExpressionBuilder(expression) + .functions(userDefinedFunctions) + .implicitMultiplication(true) + .variables(this.arguments.keySet()) + .build() + ); + initialized = true; + } else { + throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax."); + } } } } @@ -308,6 +312,14 @@ public class CalculatedFieldCtx { return typeChanged || argumentsChanged; } + public boolean hasSchedulingConfigChanges(CalculatedFieldCtx other) { + CalculatedFieldConfiguration thisConfig = calculatedField.getConfiguration(); + CalculatedFieldConfiguration otherConfig = other.calculatedField.getConfiguration(); + boolean refreshTriggerChanged = thisConfig.isDynamicRefreshEnabled() != otherConfig.isDynamicRefreshEnabled(); + boolean refreshIntervalChanged = thisConfig.getRefreshIntervalSec() != otherConfig.getRefreshIntervalSec(); + return refreshTriggerChanged || refreshIntervalChanged; + } + public String getSizeExceedsLimitMessage() { return "Failed to init CF state. State size exceeds limit of " + (maxStateSize / 1024) + "Kb!"; } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java index dc98ed836c..77e630baaa 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java @@ -26,6 +26,8 @@ import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import java.util.List; import java.util.Map; +import static org.thingsboard.server.utils.CalculatedFieldUtils.toSingleValueArgumentProto; + @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, @@ -63,6 +65,16 @@ public interface CalculatedFieldState { void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize); - void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx); + default void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx) { + // TODO: Do we need to restrict the size of Geofencing arguments? Number of zones? + if (entry instanceof TsRollingArgumentEntry || entry instanceof GeofencingArgumentEntry) { + return; + } + if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { + if (ctx.getMaxSingleValueArgumentSize() > 0 && toSingleValueArgumentProto(name, singleValueArgumentEntry).getSerializedSize() > ctx.getMaxSingleValueArgumentSize()) { + throw new IllegalArgumentException("Single value size exceeds the maximum allowed limit. The argument will not be used for calculation."); + } + } + } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java index 51f5d4fd4f..cf77d5da7d 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java @@ -16,9 +16,9 @@ package org.thingsboard.server.service.cf.ctx.state; import lombok.Data; -import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.common.util.geo.PerimeterDefinition; +import lombok.extern.slf4j.Slf4j; import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.script.api.tbel.TbelCfTsGeofencingArg; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.KvEntry; @@ -26,15 +26,18 @@ import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; -// TODO: implement @Data +@Slf4j public class GeofencingArgumentEntry implements ArgumentEntry { - private Map geofencingIdToPerimeter; + private Map zoneStates; private boolean forceResetPrevious; + public GeofencingArgumentEntry() { + } + public GeofencingArgumentEntry(Map entityIdKvEntryMap) { - this.geofencingIdToPerimeter = toPerimetersMap(entityIdKvEntryMap); + this.zoneStates = toZones(entityIdKvEntryMap); } @Override @@ -44,7 +47,7 @@ public class GeofencingArgumentEntry implements ArgumentEntry { @Override public Object getValue() { - return geofencingIdToPerimeter; + return zoneStates; } @Override @@ -52,34 +55,49 @@ public class GeofencingArgumentEntry implements ArgumentEntry { if (!(entry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) { throw new IllegalArgumentException("Unsupported argument entry type for geofencing argument entry: " + entry.getType()); } - if (Objects.equals(this.geofencingIdToPerimeter, geofencingArgumentEntry.getGeofencingIdToPerimeter())) { - return false; // No change + boolean updated = false; + for (var zoneEntry : geofencingArgumentEntry.getZoneStates().entrySet()) { + if (updateZone(zoneEntry)) { + updated = true; + } } - this.geofencingIdToPerimeter = geofencingArgumentEntry.getGeofencingIdToPerimeter(); - return true; + return updated; } @Override public boolean isEmpty() { - return geofencingIdToPerimeter == null || geofencingIdToPerimeter.isEmpty(); + return zoneStates == null || zoneStates.isEmpty(); } @Override public TbelCfArg toTbelCfArg() { - return null; + return new TbelCfTsGeofencingArg(); } - private Map toPerimetersMap(Map entityIdKvEntryMap) { + private Map toZones(Map entityIdKvEntryMap) { return entityIdKvEntryMap.entrySet().stream().map(entry -> { - if (entry.getValue().getJsonValue().isEmpty()) { - return null; - } - String rawPerimeterValue = entry.getValue().getJsonValue().get(); - PerimeterDefinition perimeter = JacksonUtil.fromString(rawPerimeterValue, PerimeterDefinition.class); - return Map.entry(entry.getKey(), perimeter); - }) - .filter(Objects::nonNull) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + try { + if (entry.getValue().getJsonValue().isEmpty()) { + return null; + } + return Map.entry(entry.getKey(), new GeofencingZoneState(entry.getKey(), entry.getValue())); + } catch (Exception e) { + log.error("Failed to parse geofencing zone perimeter for entity id: {}", entry.getKey(), e); + return null; + } + }).filter(Objects::nonNull).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private boolean updateZone(Map.Entry zoneEntry) { + EntityId zoneId = zoneEntry.getKey(); + GeofencingZoneState newZoneState = zoneEntry.getValue(); + + GeofencingZoneState existingZoneState = zoneStates.get(zoneId); + if (existingZoneState == null) { + zoneStates.put(zoneId, newZoneState); + return true; + } + return existingZoneState.update(newZoneState); } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java index 11853e7823..787f47d82c 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java @@ -21,19 +21,15 @@ import com.google.common.util.concurrent.ListenableFuture; import lombok.Data; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.geo.Coordinates; -import org.thingsboard.common.util.geo.PerimeterDefinition; -import org.thingsboard.rule.engine.geo.EntityGeofencingState; -import org.thingsboard.rule.engine.util.GpsGeofencingEvents; import org.thingsboard.server.common.data.cf.CalculatedFieldType; -import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import org.thingsboard.server.utils.CalculatedFieldUtils; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; @Data public class GeofencingCalculatedFieldState implements CalculatedFieldState { @@ -45,10 +41,11 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { private List requiredArguments; private Map arguments; + + protected boolean sizeExceedsLimit; + private long latestTimestamp = -1; - private Map saveZoneStates; - private Map restrictedZoneStates; public GeofencingCalculatedFieldState() { this(List.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, SAVE_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY)); @@ -57,8 +54,6 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { public GeofencingCalculatedFieldState(List argNames) { this.requiredArguments = argNames; this.arguments = new HashMap<>(); - this.saveZoneStates = new HashMap<>(); - this.restrictedZoneStates = new HashMap<>(); } @Override @@ -68,7 +63,6 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { @Override public boolean updateState(CalculatedFieldCtx ctx, Map argumentValues) { - // TODO: Do I need to check argument for null? if (arguments == null) { arguments = new HashMap<>(); } @@ -79,17 +73,12 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { String key = entry.getKey(); ArgumentEntry newEntry = entry.getValue(); - // TODO: Do I need to check argument size? - // checkArgumentSize(key, newEntry, ctx); + checkArgumentSize(key, newEntry, ctx); ArgumentEntry existingEntry = arguments.get(key); boolean entryUpdated; - // TODO: What is force reset previos? - // if (existingEntry == null || newEntry.isForceResetPrevious()) { - - // fresh start of state. No entry exists yet. - if (existingEntry == null) { + if (existingEntry == null || newEntry.isForceResetPrevious()) { switch (key) { case ENTITY_ID_LATITUDE_ARGUMENT_KEY: case ENTITY_ID_LONGITUDE_ARGUMENT_KEY: @@ -111,25 +100,8 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { throw new IllegalArgumentException("Unsupported argument: " + key); } } else { - entryUpdated = switch (key) { - case ENTITY_ID_LATITUDE_ARGUMENT_KEY, - ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> existingEntry.updateEntry(newEntry); - case SAVE_ZONES_ARGUMENT_KEY, - RESTRICTED_ZONES_ARGUMENT_KEY -> { - // TODO: ensure zone cleanup working correctly. - boolean updated = existingEntry.updateEntry(newEntry); - if (updated) { - Map currentStates = - key.equals(SAVE_ZONES_ARGUMENT_KEY) ? saveZoneStates : restrictedZoneStates; - Set newZoneIds = ((GeofencingArgumentEntry) newEntry).getGeofencingIdToPerimeter().keySet(); - currentStates.keySet().removeIf(existingZoneId -> !newZoneIds.contains(existingZoneId)); - } - yield updated; - } - default -> throw new IllegalStateException("Unsupported argument: " + key); - }; + entryUpdated = existingEntry.updateEntry(newEntry); } - if (entryUpdated) { stateUpdated = true; updateLastUpdateTimestamp(newEntry); @@ -141,8 +113,8 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { @Override public ListenableFuture> performCalculation(CalculatedFieldCtx ctx) { - List savedZonesStatesResults = updateSavedGeofencingZonesState(ctx); - List restrictedZonesStatesResults = updateRestrictedGeofencingZonesState(ctx); + List savedZonesStatesResults = updateGeofencingZonesState(ctx, false); + List restrictedZonesStatesResults = updateGeofencingZonesState(ctx, true); List allZoneStatesResults = new ArrayList<>(savedZonesStatesResults.size() + restrictedZonesStatesResults.size()); @@ -158,22 +130,12 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { arguments.values().stream().noneMatch(ArgumentEntry::isEmpty); } - // TODO: implement - @Override - public boolean isSizeExceedsLimit() { - return false; - } - - // TODO: implement @Override public void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize) { - - } - - // TODO: implement - @Override - public void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx) { - + if (!sizeExceedsLimit && maxStateSize > 0 && CalculatedFieldUtils.toProto(ctxId, this).getSerializedSize() > maxStateSize) { + arguments.clear(); + sizeExceedsLimit = true; + } } private void updateLastUpdateTimestamp(ArgumentEntry entry) { @@ -184,59 +146,27 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { this.latestTimestamp = Math.max(this.latestTimestamp, newTs); } - private List updateSavedGeofencingZonesState(CalculatedFieldCtx ctx) { - return updateGeofencingZonesState(ctx, saveZoneStates, false); - } - - private List updateRestrictedGeofencingZonesState(CalculatedFieldCtx ctx) { - return updateGeofencingZonesState(ctx, restrictedZoneStates, true); - } - // TODO: Ensure all cases are covered based on rule node logic. - private List updateGeofencingZonesState(CalculatedFieldCtx ctx, Map zoneStates, boolean restricted) { + private List updateGeofencingZonesState(CalculatedFieldCtx ctx, boolean restricted) { var results = new ArrayList(); - long stateSwitchTime = System.currentTimeMillis(); double latitude = (double) arguments.get(ENTITY_ID_LATITUDE_ARGUMENT_KEY).getValue(); double longitude = (double) arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY).getValue(); - Coordinates entityCoordinates = new Coordinates(latitude, longitude); + Coordinates entityCoordinates = new Coordinates(latitude, longitude); String zoneKey = restricted ? RESTRICTED_ZONES_ARGUMENT_KEY : SAVE_ZONES_ARGUMENT_KEY; GeofencingArgumentEntry zonesEntry = (GeofencingArgumentEntry) arguments.get(zoneKey); - for (Map.Entry entry : zonesEntry.getGeofencingIdToPerimeter().entrySet()) { - EntityId zoneId = entry.getKey(); - PerimeterDefinition perimeter = entry.getValue(); - - boolean inside = perimeter.checkMatches(entityCoordinates); - - // Always present or created - EntityGeofencingState state = zoneStates.computeIfAbsent( - zoneId, id -> new EntityGeofencingState(false, 0L, false) - ); - - String event; - if (state.getStateSwitchTime() == 0L || state.isInside() != inside) { - // First state or transition (entered/left) - state.setInside(inside); - state.setStateSwitchTime(stateSwitchTime); - state.setStayed(false); - - event = inside ? GpsGeofencingEvents.ENTERED : GpsGeofencingEvents.LEFT; - } else { - // No transition - event = inside ? GpsGeofencingEvents.INSIDE : GpsGeofencingEvents.OUTSIDE; - } - + for (var zoneEntry : zonesEntry.getZoneStates().entrySet()) { + GeofencingZoneState state = zoneEntry.getValue(); + String event = state.evaluate(entityCoordinates, stateSwitchTime); ObjectNode stateNode = JacksonUtil.newObjectNode(); stateNode.put("entityId", ctx.getEntityId().toString()); - stateNode.put("zoneId", zoneId.getId().toString()); + stateNode.put("zoneId", state.getZoneId().toString()); stateNode.put("restricted", restricted); stateNode.put("event", event); - results.add(new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), stateNode)); } - return results; } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java new file mode 100644 index 0000000000..abee7cabb6 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java @@ -0,0 +1,94 @@ +/** + * 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.service.cf.ctx.state; + +import lombok.Data; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.common.util.geo.Coordinates; +import org.thingsboard.common.util.geo.PerimeterDefinition; +import org.thingsboard.rule.engine.geo.EntityGeofencingState; +import org.thingsboard.rule.engine.util.GpsGeofencingEvents; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneProto; + +import java.util.UUID; + +@Data +public class GeofencingZoneState { + + private final EntityId zoneId; + + private long ts; + private Long version; + private PerimeterDefinition perimeterDefinition; + + private EntityGeofencingState state; + + public GeofencingZoneState(EntityId zoneId, KvEntry entry) { + this.zoneId = zoneId; + if (entry instanceof TsKvEntry tsKvEntry) { + this.ts = tsKvEntry.getTs(); + this.version = tsKvEntry.getVersion(); + } else if (entry instanceof AttributeKvEntry attributeKvEntry) { + this.ts = attributeKvEntry.getLastUpdateTs(); + this.version = attributeKvEntry.getVersion(); + } + this.perimeterDefinition = JacksonUtil.fromString(entry.getJsonValue().orElseThrow(), PerimeterDefinition.class); + } + + public GeofencingZoneState(GeofencingZoneProto proto) { + this.zoneId = EntityIdFactory.getByTypeAndUuid(proto.getZoneId().getType(), + new UUID(proto.getZoneId().getZoneIdMSB(), proto.getZoneId().getZoneIdLSB())); + this.ts = proto.getTs(); + this.version = proto.getVersion(); + this.perimeterDefinition = JacksonUtil.fromString(proto.getPerimeterDefinition(), PerimeterDefinition.class); + this.state = new EntityGeofencingState(proto.getInside(), proto.getStateSwitchTime(), proto.getStayed()); + } + + public boolean update(GeofencingZoneState newZoneState) { + if (newZoneState.getTs() <= this.ts) { + return false; + } + Long newVersion = newZoneState.getVersion(); + if (newVersion == null || this.version == null || newVersion > this.version) { + this.ts = newZoneState.getTs(); + this.version = newVersion; + this.perimeterDefinition = newZoneState.getPerimeterDefinition(); + // TODO: should we reinitialize state if zone changed? + return true; + } + return false; + } + + public String evaluate(Coordinates entityCoordinates, long currentTs) { + boolean inside = perimeterDefinition.checkMatches(entityCoordinates); + if (state == null) { + state = new EntityGeofencingState(inside, ts, false); + } + if (state.getStateSwitchTime() == 0L || state.isInside() != inside) { + state.setInside(inside); + state.setStateSwitchTime(currentTs); + state.setStayed(false); + return inside ? GpsGeofencingEvents.ENTERED : GpsGeofencingEvents.LEFT; + } + return inside ? GpsGeofencingEvents.INSIDE : GpsGeofencingEvents.OUTSIDE; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java index d9a6248b96..302ced0df1 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.utils; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.geo.EntityGeofencingState; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.CalculatedFieldId; @@ -26,21 +28,30 @@ import org.thingsboard.server.common.util.KvProtoUtil; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldEntityCtxIdProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldIdProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; +import org.thingsboard.server.gen.transport.TransportProtos.GeofencingArgumentProto; +import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneIdProto; +import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneProto; import org.thingsboard.server.gen.transport.TransportProtos.SingleValueArgumentProto; import org.thingsboard.server.gen.transport.TransportProtos.TsDoubleValProto; import org.thingsboard.server.gen.transport.TransportProtos.TsRollingArgumentProto; import org.thingsboard.server.gen.transport.TransportProtos.TsValueProto; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.GeofencingArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.GeofencingZoneState; import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry; +import java.util.Map; import java.util.Optional; import java.util.TreeMap; import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; public class CalculatedFieldUtils { @@ -80,6 +91,8 @@ public class CalculatedFieldUtils { builder.addSingleValueArguments(toSingleValueArgumentProto(argName, singleValueArgumentEntry)); } else if (argEntry instanceof TsRollingArgumentEntry rollingArgumentEntry) { builder.addRollingValueArguments(toRollingArgumentProto(argName, rollingArgumentEntry)); + } else if (argEntry instanceof GeofencingArgumentEntry geofencingArgumentEntry) { + builder.addGeofencingArguments(toGeofencingArgumentProto(argName, geofencingArgumentEntry)); } }); return builder.build(); @@ -109,6 +122,42 @@ public class CalculatedFieldUtils { return builder.build(); } + + private static GeofencingArgumentProto toGeofencingArgumentProto(String argName, GeofencingArgumentEntry geofencingArgumentEntry) { + GeofencingArgumentProto.Builder builder = GeofencingArgumentProto.newBuilder() + .setArgName(argName); + Map zoneStates = geofencingArgumentEntry.getZoneStates(); + zoneStates.forEach((entityId, zoneState) -> { + builder.addZones(toGeofencingZoneProto(entityId, zoneState)); + }); + return builder.build(); + } + + private static GeofencingZoneProto toGeofencingZoneProto(EntityId entityId, GeofencingZoneState zoneState) { + GeofencingZoneProto.Builder builder = GeofencingZoneProto.newBuilder() + .setZoneId(toGeofencingZoneIdProto(entityId)) + .setTs(zoneState.getTs()) + .setVersion(zoneState.getVersion()) + .setPerimeterDefinition(JacksonUtil.toString(zoneState.getPerimeterDefinition())); + if (zoneState.getState() != null) { + EntityGeofencingState state = zoneState.getState(); + builder.setInside(state.isInside()) + .setStayed(state.isStayed()) + .setStateSwitchTime(state.getStateSwitchTime()); + + } + return builder.build(); + } + + private static GeofencingZoneIdProto toGeofencingZoneIdProto(EntityId zoneId) { + return GeofencingZoneIdProto.newBuilder() + .setType(zoneId.getEntityType().name()) + .setZoneIdLSB(zoneId.getId().getLeastSignificantBits()) + .setZoneIdMSB(zoneId.getId().getMostSignificantBits()) + .build(); + } + + public static CalculatedFieldState fromProto(CalculatedFieldStateProto proto) { if (StringUtils.isEmpty(proto.getType())) { return null; @@ -122,8 +171,6 @@ public class CalculatedFieldUtils { case GEOFENCING -> new GeofencingCalculatedFieldState(); }; - // TODO: add logic to restore geofencing state from proto - proto.getSingleValueArgumentsList().forEach(argProto -> state.getArguments().put(argProto.getArgName(), fromSingleValueArgumentProto(argProto))); @@ -132,6 +179,11 @@ public class CalculatedFieldUtils { state.getArguments().put(argProto.getKey(), fromRollingArgumentProto(argProto))); } + if (CalculatedFieldType.GEOFENCING.equals(type)) { + proto.getGeofencingArgumentsList().forEach(argProto -> + state.getArguments().put(argProto.getArgName(), fromGeofencingArgumentProto(argProto))); + } + return state; } @@ -153,4 +205,15 @@ public class CalculatedFieldUtils { return new TsRollingArgumentEntry(tsRecords, proto.getLimit(), proto.getTimeWindow()); } + + private static ArgumentEntry fromGeofencingArgumentProto(GeofencingArgumentProto proto) { + Map zoneStates = proto.getZonesList() + .stream() + .map(GeofencingZoneState::new) + .collect(Collectors.toMap(GeofencingZoneState::getZoneId, Function.identity())); + GeofencingArgumentEntry geofencingArgumentEntry = new GeofencingArgumentEntry(); + geofencingArgumentEntry.setZoneStates(zoneStates); + return geofencingArgumentEntry; + } + } diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml index c37be2e620..5478d65d93 100644 --- a/application/src/main/resources/logback.xml +++ b/application/src/main/resources/logback.xml @@ -56,6 +56,7 @@ + diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java index 6da6fcf8a8..1805788007 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java @@ -38,7 +38,6 @@ import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; -import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.RestApiCallResponseMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldMsg; import org.thingsboard.server.gen.transport.TransportProtos.ToCalculatedFieldNotificationMsg; @@ -81,7 +80,7 @@ public interface TbClusterService extends TbQueueClusterService { void pushNotificationToTransport(String targetServiceId, ToTransportMsg response, TbQueueCallback callback); - void pushMsgToCalculatedFields(TenantId tenantId, EntityId entityId, TransportProtos.ToCalculatedFieldMsg msg, TbQueueCallback callback); + void pushMsgToCalculatedFields(TenantId tenantId, EntityId entityId, ToCalculatedFieldMsg msg, TbQueueCallback callback); void pushMsgToCalculatedFields(TopicPartitionInfo tpi, UUID msgId, ToCalculatedFieldMsg msg, TbQueueCallback callback); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java index 8227ff4603..d3cfe3a59a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java @@ -58,4 +58,9 @@ public abstract class BaseCalculatedFieldConfiguration implements CalculatedFiel return link; } + @Override + public boolean hasDynamicSourceArguments() { + return arguments.values().stream().anyMatch(arg -> arg.getRefDynamicSource() != null); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java index b713f9030d..d090feeca7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java @@ -59,4 +59,16 @@ public interface CalculatedFieldConfiguration { CalculatedFieldLink buildCalculatedFieldLink(TenantId tenantId, EntityId referencedEntityId, CalculatedFieldId calculatedFieldId); + @JsonIgnore + boolean hasDynamicSourceArguments(); + + @JsonIgnore + default boolean isDynamicRefreshEnabled() { + return hasDynamicSourceArguments() && getRefreshIntervalSec() > 0; + } + + default int getRefreshIntervalSec() { + return 0; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java index 6c1450d713..db6a5fd460 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java @@ -23,9 +23,15 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; @EqualsAndHashCode(callSuper = true) public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements CalculatedFieldConfiguration { + private int refreshIntervalSec; + @Override public CalculatedFieldType getType() { return CalculatedFieldType.GEOFENCING; } + public boolean isDynamicRefreshEnabled() { + return refreshIntervalSec > 0; + } + } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java index f1c404ce16..bfcd3f3071 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java @@ -150,7 +150,10 @@ public enum MsgType { /* CF Manager Actor -> CF Entity actor */ CF_ENTITY_TELEMETRY_MSG, CF_ENTITY_INIT_CF_MSG, - CF_ENTITY_DELETE_MSG; + CF_ENTITY_DELETE_MSG, + + CF_SCHEDULED_CHECK_FOR_UPDATES_MSG, + CF_ENTITY_CHECK_FOR_UPDATES_MSG; @Getter private final boolean ignoreOnStart; diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 2667838b60..885bad2cca 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -894,11 +894,33 @@ message TsRollingArgumentProto { repeated TsDoubleValProto tsValue = 4; } +message GeofencingZoneIdProto { + string type = 1; + int64 zoneIdMSB = 2; + int64 zoneIdLSB = 3; +} + +message GeofencingZoneProto { + GeofencingZoneIdProto zoneId = 1; + int64 ts = 2; + string perimeterDefinition = 3; + int64 version = 4; + bool inside = 5; + int64 stateSwitchTime = 6; + bool stayed = 7; +} + +message GeofencingArgumentProto { + string argName = 1; // e.g., "restrictedZones" or "saveZones" + repeated GeofencingZoneProto zones = 2; +} + message CalculatedFieldStateProto { CalculatedFieldEntityCtxIdProto id = 1; string type = 2; repeated SingleValueArgumentProto singleValueArguments = 3; repeated TsRollingArgumentProto rollingValueArguments = 4; + repeated GeofencingArgumentProto geofencingArguments = 5; } //Used to report session state to tb-Service and persist this state in the cache on the tb-Service level. diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfArg.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfArg.java index f95b08195e..73a2183564 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfArg.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfArg.java @@ -26,7 +26,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; ) @JsonSubTypes({ @JsonSubTypes.Type(value = TbelCfSingleValueArg.class, name = "SINGLE_VALUE"), - @JsonSubTypes.Type(value = TbelCfTsRollingArg.class, name = "TS_ROLLING") + @JsonSubTypes.Type(value = TbelCfTsRollingArg.class, name = "TS_ROLLING"), + @JsonSubTypes.Type(value = TbelCfTsGeofencingArg.class, name = "GEOFENCING_CF_ARGUMENT_VALUE"), }) public interface TbelCfArg extends TbelCfObject { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldInitService.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsGeofencingArg.java similarity index 62% rename from application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldInitService.java rename to common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsGeofencingArg.java index 6505dae581..46ac553a76 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldInitService.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsGeofencingArg.java @@ -13,7 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.cf; +package org.thingsboard.script.api.tbel; + +// TODO: should I add any specific logic for this? +public class TbelCfTsGeofencingArg implements TbelCfArg { + + public TbelCfTsGeofencingArg() { + + } + + @Override + public String getType() { + return "GEOFENCING_CF_ARGUMENT_VALUE"; + } + + + @Override + public long memorySize() { + return OBJ_SIZE; + } -public interface CalculatedFieldInitService { } diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinition.java b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinition.java index 68191f1a17..4cba6f9c8a 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinition.java +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinition.java @@ -35,5 +35,6 @@ public interface PerimeterDefinition extends Serializable { @JsonIgnore PerimeterType getType(); + @JsonIgnore boolean checkMatches(Coordinates entityCoordinates); } From e22462521faab76c1c5e115cb8f6c8cbebd7d236 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Fri, 1 Aug 2025 15:31:43 +0300 Subject: [PATCH 019/216] Scheduling exclusively during CF init and update & added simple relation check for fetch from DB --- .../CalculatedFieldEntityMessageProcessor.java | 2 +- ...CalculatedFieldManagerMessageProcessor.java | 18 ++++++++---------- ...efaultCalculatedFieldProcessingService.java | 17 ++++++++++++++--- .../cf/ctx/state/CalculatedFieldCtx.java | 4 ++-- .../CalculatedFieldConfiguration.java | 6 +++--- .../CfArgumentDynamicSourceConfiguration.java | 8 ++++++++ ...GeofencingCalculatedFieldConfiguration.java | 2 +- ...elationQueryDynamicSourceConfiguration.java | 12 +++++++++++- 8 files changed, 48 insertions(+), 21 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 68c0471cb7..d41feac542 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -231,7 +231,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM public void process(EntityCalculatedFieldCheckForUpdatesMsg msg) throws CalculatedFieldException { CalculatedFieldCtx cfCtx = msg.getCfCtx(); CalculatedFieldId cfId = cfCtx.getCfId(); - log.debug("[{}] [{}] Processing CF dynamic sources refresh msg.", entityId, cfId); + log.debug("[{}][{}] Processing CF check for updates msg.", entityId, cfId); try { var state = updateStateFromDb(cfCtx); if (state.isSizeOk()) { diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index 03de43d08b..9f102c893f 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -334,7 +334,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware calculatedFields.put(newCf.getId(), newCfCtx); List oldCfList = entityIdCalculatedFields.get(newCf.getEntityId()); - if (newCfCtx.hasSchedulingConfigChanges(oldCfCtx)) { + boolean hasSchedulingConfigChanges = newCfCtx.hasSchedulingConfigChanges(oldCfCtx); + if (hasSchedulingConfigChanges) { cancelCfUpdateTaskIfExists(cfId, false); } @@ -359,7 +360,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware // We use copy on write lists to safely pass the reference to another actor for the iteration. // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) var stateChanges = newCfCtx.hasStateChanges(oldCfCtx); - if (stateChanges || newCfCtx.hasOtherSignificantChanges(oldCfCtx)) { + if (stateChanges || newCfCtx.hasOtherSignificantChanges(oldCfCtx) || hasSchedulingConfigChanges) { initCf(newCfCtx, callback, stateChanges); } else { callback.onSuccess(); @@ -514,6 +515,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private void initCf(CalculatedFieldCtx cfCtx, TbCallback callback, boolean forceStateReinit) { EntityId entityId = cfCtx.getEntityId(); EntityType entityType = cfCtx.getEntityId().getEntityType(); + scheduleCalculatedFieldUpdateMsgIfNeeded(cfCtx); if (isProfileEntity(entityType)) { var entityIds = entityProfileCache.getEntityIdsByProfileId(entityId); if (!entityIds.isEmpty()) { @@ -523,29 +525,25 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware initCfForEntity(id, cfCtx, forceStateReinit, multiCallback); } }); - scheduleCalculatedFieldUpdateMsgIfNeeded(cfCtx); } else { callback.onSuccess(); } - } else { - if (isMyPartition(entityId, callback)) { - initCfForEntity(entityId, cfCtx, forceStateReinit, callback); - scheduleCalculatedFieldUpdateMsgIfNeeded(cfCtx); - } + } else if (isMyPartition(entityId, callback)) { + initCfForEntity(entityId, cfCtx, forceStateReinit, callback); } } private void scheduleCalculatedFieldUpdateMsgIfNeeded(CalculatedFieldCtx cfCtx) { CalculatedField cf = cfCtx.getCalculatedField(); CalculatedFieldConfiguration cfConfig = cf.getConfiguration(); - if (!cfConfig.isDynamicRefreshEnabled()) { + if (!cfConfig.isScheduledUpdateEnabled()) { return; } if (checkForCalculatedFieldUpdateTasks.containsKey(cf.getId())) { log.debug("[{}][{}] Check for update msg for CF is already scheduled!", tenantId, cf.getId()); return; } - long refreshDynamicSourceInterval = TimeUnit.SECONDS.toMillis(cfConfig.getRefreshIntervalSec()); + long refreshDynamicSourceInterval = TimeUnit.SECONDS.toMillis(cfConfig.getScheduledUpdateIntervalSec()); var scheduledMsg = new CalculatedFieldScheduledCheckForUpdatesMsg(tenantId, cfCtx); ScheduledFuture scheduledFuture = systemContext diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index 424df50009..44544fbbdd 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -52,6 +52,7 @@ import org.thingsboard.server.common.data.kv.ReadTsKvQuery; import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.msg.TbMsgType; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; @@ -286,9 +287,19 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP } return switch (value.getRefDynamicSource()) { case RELATION_QUERY -> { - var relationQueryDynamicSourceConfiguration = (RelationQueryDynamicSourceConfiguration) value.getRefDynamicSourceConfiguration(); - yield Futures.transform(relationService.findByQuery(tenantId, relationQueryDynamicSourceConfiguration.toEntityRelationsQuery(entityId)), - relationQueryDynamicSourceConfiguration::resolveEntityIds, calculatedFieldCallbackExecutor); + var configuration = (RelationQueryDynamicSourceConfiguration) value.getRefDynamicSourceConfiguration(); + if (configuration.isSimpleRelation()) { + yield switch (configuration.getDirection()) { + case FROM -> + Futures.transform(relationService.findByFromAndTypeAsync(tenantId, entityId, configuration.getRelationType(), RelationTypeGroup.COMMON), + configuration::resolveEntityIds, calculatedFieldCallbackExecutor); + case TO -> + Futures.transform(relationService.findByToAndTypeAsync(tenantId, entityId, configuration.getRelationType(), RelationTypeGroup.COMMON), + configuration::resolveEntityIds, calculatedFieldCallbackExecutor); + }; + } + yield Futures.transform(relationService.findByQuery(tenantId, configuration.toEntityRelationsQuery(entityId)), + configuration::resolveEntityIds, calculatedFieldCallbackExecutor); } }; } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 8124d78d3a..0fa653cf67 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -315,8 +315,8 @@ public class CalculatedFieldCtx { public boolean hasSchedulingConfigChanges(CalculatedFieldCtx other) { CalculatedFieldConfiguration thisConfig = calculatedField.getConfiguration(); CalculatedFieldConfiguration otherConfig = other.calculatedField.getConfiguration(); - boolean refreshTriggerChanged = thisConfig.isDynamicRefreshEnabled() != otherConfig.isDynamicRefreshEnabled(); - boolean refreshIntervalChanged = thisConfig.getRefreshIntervalSec() != otherConfig.getRefreshIntervalSec(); + boolean refreshTriggerChanged = thisConfig.isScheduledUpdateEnabled() != otherConfig.isScheduledUpdateEnabled(); + boolean refreshIntervalChanged = thisConfig.getScheduledUpdateIntervalSec() != otherConfig.getScheduledUpdateIntervalSec(); return refreshTriggerChanged || refreshIntervalChanged; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java index d090feeca7..3ee55f5d66 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java @@ -63,11 +63,11 @@ public interface CalculatedFieldConfiguration { boolean hasDynamicSourceArguments(); @JsonIgnore - default boolean isDynamicRefreshEnabled() { - return hasDynamicSourceArguments() && getRefreshIntervalSec() > 0; + default boolean isScheduledUpdateEnabled() { + return hasDynamicSourceArguments() && getScheduledUpdateIntervalSec() > 0; } - default int getRefreshIntervalSec() { + default int getScheduledUpdateIntervalSec() { return 0; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java index 3fe432917b..7af6283536 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelationsQuery; @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -36,4 +38,10 @@ public interface CfArgumentDynamicSourceConfiguration { default void validate() {} + @JsonIgnore + boolean isSimpleRelation(); + + @JsonIgnore + EntityRelationsQuery toEntityRelationsQuery(EntityId rootEntityId); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java index db6a5fd460..77369e6daa 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java @@ -30,7 +30,7 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC return CalculatedFieldType.GEOFENCING; } - public boolean isDynamicRefreshEnabled() { + public boolean isScheduledUpdateEnabled() { return refreshIntervalSec > 0; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java index 219fd068cd..d3500606f6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java @@ -31,6 +31,7 @@ import java.util.List; public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration { private int maxLevel; + private boolean fetchLastLevelOnly; private EntitySearchDirection direction; private String relationType; private List profiles; @@ -40,9 +41,18 @@ public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynami return CFArgumentDynamicSourceType.RELATION_QUERY; } + @Override + public boolean isSimpleRelation() { + return maxLevel == 1 && (profiles == null || profiles.isEmpty()); + } + + @Override public EntityRelationsQuery toEntityRelationsQuery(EntityId rootEntityId) { + if (isSimpleRelation()) { + throw new IllegalArgumentException("Entity relations query can't be created for a simple relation!"); + } var entityRelationsQuery = new EntityRelationsQuery(); - entityRelationsQuery.setParameters(new RelationsSearchParameters(rootEntityId, direction, maxLevel, false)); + entityRelationsQuery.setParameters(new RelationsSearchParameters(rootEntityId, direction, maxLevel, fetchLastLevelOnly)); entityRelationsQuery.setFilters(Collections.singletonList(new RelationEntityTypeFilter(relationType, profiles))); return entityRelationsQuery; } From 4cd0ec9e27c6e658f909177ad1db98da8bf659c8 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Fri, 1 Aug 2025 19:18:10 +0300 Subject: [PATCH 020/216] Basic data validation & fixed calculated field update in scheduled update msg --- ...alculatedFieldManagerMessageProcessor.java | 11 +++- ...latedFieldScheduledCheckForUpdatesMsg.java | 4 +- ...faultCalculatedFieldProcessingService.java | 8 +-- .../state/GeofencingCalculatedFieldState.java | 12 ++-- .../BaseCalculatedFieldConfiguration.java | 16 ++++- .../CalculatedFieldConfiguration.java | 11 ++-- ...eofencingCalculatedFieldConfiguration.java | 64 ++++++++++++++++++- ...lationQueryDynamicSourceConfiguration.java | 13 ++++ .../DefaultTenantProfileConfiguration.java | 4 ++ .../dao/cf/BaseCalculatedFieldService.java | 12 ++++ .../dao/entity/AbstractEntityService.java | 2 +- .../CalculatedFieldDataValidator.java | 21 ++++-- 12 files changed, 147 insertions(+), 31 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index 9f102c893f..f557a632b4 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -544,7 +544,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware return; } long refreshDynamicSourceInterval = TimeUnit.SECONDS.toMillis(cfConfig.getScheduledUpdateIntervalSec()); - var scheduledMsg = new CalculatedFieldScheduledCheckForUpdatesMsg(tenantId, cfCtx); + var scheduledMsg = new CalculatedFieldScheduledCheckForUpdatesMsg(tenantId, cfCtx.getCfId()); ScheduledFuture scheduledFuture = systemContext .schedulePeriodicMsgWithDelay(ctx, scheduledMsg, refreshDynamicSourceInterval, refreshDynamicSourceInterval); @@ -553,9 +553,14 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } public void onScheduledCheckForUpdatesMsg(CalculatedFieldScheduledCheckForUpdatesMsg msg) { - CalculatedFieldCtx cfCtx = msg.getCfCtx(); + log.debug("[{}] [{}] Processing CF scheduled update msg.", tenantId, msg.getCfId()); + CalculatedFieldCtx cfCtx = calculatedFields.get(msg.getCfId()); + if (cfCtx == null) { + log.debug("[{}][{}] Failed to find CF context, going to stop scheduler updates.", tenantId, msg.getCfId()); + cancelCfUpdateTaskIfExists(msg.getCfId(), true); + return; + } EntityId entityId = cfCtx.getEntityId(); - log.debug("[{}] [{}] Processing CF scheduled update msg.", cfCtx.getCfId(), entityId); EntityType entityType = entityId.getEntityType(); if (isProfileEntity(entityType)) { var entityIds = entityProfileCache.getEntityIdsByProfileId(entityId); diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldScheduledCheckForUpdatesMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldScheduledCheckForUpdatesMsg.java index f53d2e7572..95d6b6759a 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldScheduledCheckForUpdatesMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldScheduledCheckForUpdatesMsg.java @@ -16,16 +16,16 @@ package org.thingsboard.server.actors.calculatedField; import lombok.Data; +import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; -import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; @Data public class CalculatedFieldScheduledCheckForUpdatesMsg implements ToCalculatedFieldSystemMsg { private final TenantId tenantId; - private final CalculatedFieldCtx cfCtx; + private final CalculatedFieldId cfId; @Override public MsgType getMsgType() { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index 44544fbbdd..0284428c46 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -93,10 +93,10 @@ import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import static org.thingsboard.server.common.data.DataConstants.SCOPE; -import static org.thingsboard.server.service.cf.ctx.state.GeofencingCalculatedFieldState.ENTITY_ID_LATITUDE_ARGUMENT_KEY; -import static org.thingsboard.server.service.cf.ctx.state.GeofencingCalculatedFieldState.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; -import static org.thingsboard.server.service.cf.ctx.state.GeofencingCalculatedFieldState.RESTRICTED_ZONES_ARGUMENT_KEY; -import static org.thingsboard.server.service.cf.ctx.state.GeofencingCalculatedFieldState.SAVE_ZONES_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.RESTRICTED_ZONES_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.SAVE_ZONES_ARGUMENT_KEY; import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto; @TbRuleEngineComponent diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java index 787f47d82c..bc7460784b 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java @@ -31,14 +31,14 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.RESTRICTED_ZONES_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.SAVE_ZONES_ARGUMENT_KEY; + @Data public class GeofencingCalculatedFieldState implements CalculatedFieldState { - public static final String ENTITY_ID_LATITUDE_ARGUMENT_KEY = "latitude"; - public static final String ENTITY_ID_LONGITUDE_ARGUMENT_KEY = "longitude"; - public static final String SAVE_ZONES_ARGUMENT_KEY = "saveZones"; - public static final String RESTRICTED_ZONES_ARGUMENT_KEY = "restrictedZones"; - private List requiredArguments; private Map arguments; @@ -73,7 +73,7 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { String key = entry.getKey(); ArgumentEntry newEntry = entry.getValue(); - checkArgumentSize(key, newEntry, ctx); + checkArgumentSize(key, newEntry, ctx); ArgumentEntry existingEntry = arguments.get(key); boolean entryUpdated; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java index d3cfe3a59a..71d8bba6cb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java @@ -33,6 +33,8 @@ public abstract class BaseCalculatedFieldConfiguration implements CalculatedFiel protected String expression; protected Output output; + protected int scheduledUpdateIntervalSec; + @Override public List getReferencedEntities() { return arguments.values().stream() @@ -58,9 +60,19 @@ public abstract class BaseCalculatedFieldConfiguration implements CalculatedFiel return link; } + // TODO: update validate method in PE version. @Override - public boolean hasDynamicSourceArguments() { - return arguments.values().stream().anyMatch(arg -> arg.getRefDynamicSource() != null); + public void validate() { + boolean hasDynamicSourceRelationQuery = arguments.values() + .stream() + .anyMatch(arg -> CFArgumentDynamicSourceType.RELATION_QUERY.equals(arg.getRefDynamicSource())); + if (hasDynamicSourceRelationQuery) { + throw new IllegalArgumentException("Calculated field with type: '" + getType() + "' doesn't support arguments with 'RELATION_QUERY' dynamic source type!"); + } + } + + public boolean isScheduledUpdateEnabled() { + return scheduledUpdateIntervalSec > 0 && arguments.values().stream().anyMatch(arg -> arg.getRefDynamicSource() != null); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java index 3ee55f5d66..9103391326 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java @@ -59,16 +59,15 @@ public interface CalculatedFieldConfiguration { CalculatedFieldLink buildCalculatedFieldLink(TenantId tenantId, EntityId referencedEntityId, CalculatedFieldId calculatedFieldId); - @JsonIgnore - boolean hasDynamicSourceArguments(); + void validate(); @JsonIgnore default boolean isScheduledUpdateEnabled() { - return hasDynamicSourceArguments() && getScheduledUpdateIntervalSec() > 0; + return false; } - default int getScheduledUpdateIntervalSec() { - return 0; - } + void setScheduledUpdateIntervalSec(int scheduledUpdateIntervalSec); + + int getScheduledUpdateIntervalSec(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java index 77369e6daa..3d85afa77c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java @@ -19,19 +19,77 @@ import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import java.util.Set; + +import static org.thingsboard.server.common.data.cf.configuration.CFArgumentDynamicSourceType.RELATION_QUERY; + @Data @EqualsAndHashCode(callSuper = true) public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements CalculatedFieldConfiguration { - private int refreshIntervalSec; + public static final String ENTITY_ID_LATITUDE_ARGUMENT_KEY = "latitude"; + public static final String ENTITY_ID_LONGITUDE_ARGUMENT_KEY = "longitude"; + public static final String SAVE_ZONES_ARGUMENT_KEY = "saveZones"; + public static final String RESTRICTED_ZONES_ARGUMENT_KEY = "restrictedZones"; + + private static final Set requiredKeys = Set.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, + SAVE_ZONES_ARGUMENT_KEY, + RESTRICTED_ZONES_ARGUMENT_KEY + ); @Override public CalculatedFieldType getType() { return CalculatedFieldType.GEOFENCING; } - public boolean isScheduledUpdateEnabled() { - return refreshIntervalSec > 0; + // TODO: update validate method in PE version. + @Override + public void validate() { + if (arguments == null || arguments.size() != 4) { + throw new IllegalArgumentException("Geofencing calculated field configuration must contain exactly 4 arguments: " + requiredKeys); + } + for (String requiredKey : requiredKeys) { + Argument argument = arguments.get(requiredKey); + if (argument == null) { + throw new IllegalArgumentException("Missing required argument: " + requiredKey); + } + ReferencedEntityKey refEntityKey = argument.getRefEntityKey(); + if (refEntityKey == null || refEntityKey.getType() == null) { + throw new IllegalArgumentException("Missing or invalid reference entity key for argument: " + requiredKey); + } + switch (requiredKey) { + case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> { + if (!ArgumentType.TS_LATEST.equals(refEntityKey.getType())) { + throw new IllegalArgumentException("Argument: '" + requiredKey + "' must be set to " + ArgumentType.TS_LATEST + " type!"); + } + var dynamicSource = argument.getRefDynamicSource(); + if (dynamicSource != null) { + String test = "test"; + throw new IllegalArgumentException("Dynamic source configuration is forbidden for '" + requiredKey + "' argument. " + + "Only '" + SAVE_ZONES_ARGUMENT_KEY + "' and '" + RESTRICTED_ZONES_ARGUMENT_KEY + "' " + + "may use dynamic source configuration."); + } + } + case SAVE_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY -> { + if (!ArgumentType.ATTRIBUTE.equals(refEntityKey.getType())) { + throw new IllegalArgumentException("Argument: '" + requiredKey + "' must be set to " + ArgumentType.ATTRIBUTE + " type!"); + } + var dynamicSource = argument.getRefDynamicSource(); + if (dynamicSource == null) { + continue; + } + if (!RELATION_QUERY.equals(dynamicSource)) { + throw new IllegalArgumentException("Only relation query dynamic source is supported for argument: " + requiredKey); + } + if (argument.getRefDynamicSourceConfiguration() == null) { + throw new IllegalArgumentException("Missing dynamic source configuration for: " + requiredKey); + } + argument.getRefDynamicSourceConfiguration().validate(); + } + } + } } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java index d3500606f6..85f1ee21bd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java @@ -41,6 +41,19 @@ public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynami return CFArgumentDynamicSourceType.RELATION_QUERY; } + @Override + public void validate() { + if (maxLevel > 2) { + throw new IllegalArgumentException("Relation query dynamic source configuration max relation level can't be greater than 2!"); + } + if (direction == null) { + throw new IllegalArgumentException("Relation query dynamic source configuration direction must be specified!"); + } + if (relationType == null) { + throw new IllegalArgumentException("Relation query dynamic source configuration relation type must be specified!"); + } + } + @Override public boolean isSimpleRelation() { return maxLevel == 1 && (profiles == null || profiles.isEmpty()); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index 246fe46791..9da0e27dc6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -172,6 +172,10 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long maxCalculatedFieldsPerEntity = 5; @Schema(example = "10") private long maxArgumentsPerCF = 10; + @Schema(example = "300") + private int minAllowedScheduledUpdateIntervalInSecForCF = 60; + @Schema(example = "300") + private int maxAllowedScheduledUpdateIntervalInSecForCF = 3600; @Builder.Default @Min(value = 1, message = "must be at least 1") @Schema(example = "1000") diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index c0cb886747..40694050be 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -78,6 +78,7 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements TenantId tenantId = calculatedField.getTenantId(); log.trace("Executing save calculated field, [{}]", calculatedField); updateDebugSettings(tenantId, calculatedField, System.currentTimeMillis()); + updatedSchedulingConfiguration(calculatedField); CalculatedField savedCalculatedField = calculatedFieldDao.save(tenantId, calculatedField); createOrUpdateCalculatedFieldLink(tenantId, savedCalculatedField); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedCalculatedField.getTenantId()).entityId(savedCalculatedField.getId()) @@ -91,6 +92,17 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements } } + private void updatedSchedulingConfiguration(CalculatedField calculatedField) { + CalculatedFieldConfiguration configuration = calculatedField.getConfiguration(); + if (!configuration.isScheduledUpdateEnabled()) { + return; + } + var defaultProfileConfiguration = tbTenantProfileCache.get(calculatedField.getTenantId()).getDefaultProfileConfiguration(); + int min = defaultProfileConfiguration.getMinAllowedScheduledUpdateIntervalInSecForCF(); + int max = defaultProfileConfiguration.getMaxAllowedScheduledUpdateIntervalInSecForCF(); + configuration.setScheduledUpdateIntervalSec(Math.max(min, Math.min(configuration.getScheduledUpdateIntervalSec(), max))); + } + @Override public CalculatedField findById(TenantId tenantId, CalculatedFieldId calculatedFieldId) { log.trace("Executing findById, tenantId [{}], calculatedFieldId [{}]", tenantId, calculatedFieldId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java index 7560c7fb76..a34e968acc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java @@ -81,7 +81,7 @@ public abstract class AbstractEntityService { @Autowired @Lazy - private TbTenantProfileCache tbTenantProfileCache; + protected TbTenantProfileCache tbTenantProfileCache; @Value("${debug.settings.default_duration:15}") private int defaultDebugDurationMinutes; diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java index c9c7af1a89..12d9764af2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java @@ -18,6 +18,8 @@ package org.thingsboard.server.dao.service.validator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; @@ -39,7 +41,7 @@ public class CalculatedFieldDataValidator extends DataValidator protected void validateCreate(TenantId tenantId, CalculatedField calculatedField) { validateNumberOfCFsPerEntity(tenantId, calculatedField.getEntityId()); validateNumberOfArgumentsPerCF(tenantId, calculatedField); - validateArgumentNames(calculatedField); + validateCalculatedFieldConfiguration(calculatedField); } @Override @@ -49,7 +51,7 @@ public class CalculatedFieldDataValidator extends DataValidator throw new DataValidationException("Can't update non existing calculated field!"); } validateNumberOfArgumentsPerCF(tenantId, calculatedField); - validateArgumentNames(calculatedField); + validateCalculatedFieldConfiguration(calculatedField); return old; } @@ -68,15 +70,26 @@ public class CalculatedFieldDataValidator extends DataValidator if (maxArgumentsPerCF <= 0) { return; } + if (CalculatedFieldType.GEOFENCING.equals(calculatedField.getType()) && maxArgumentsPerCF < 4) { + throw new DataValidationException("Geofencing calculated field requires 4 arguments, but the system limit is " + + maxArgumentsPerCF + ". Contact your administrator to increase the limit." + ); + } if (calculatedField.getConfiguration().getArguments().size() > maxArgumentsPerCF) { throw new DataValidationException("Calculated field arguments limit reached!"); } } - private void validateArgumentNames(CalculatedField calculatedField) { - if (calculatedField.getConfiguration().getArguments().containsKey("ctx")) { + private void validateCalculatedFieldConfiguration(CalculatedField calculatedField) { + CalculatedFieldConfiguration configuration = calculatedField.getConfiguration(); + if (configuration.getArguments().containsKey("ctx")) { throw new DataValidationException("Argument name 'ctx' is reserved and cannot be used."); } + try { + configuration.validate(); + } catch (IllegalArgumentException e) { + throw new DataValidationException(e.getMessage(), e); + } } } From 5e9921905f1928d416874bd47071b9490163bfd2 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Mon, 4 Aug 2025 14:41:46 +0300 Subject: [PATCH 021/216] Simplified impl of geofencing zone state & resolved TODOs --- ...CalculatedFieldEntityMessageProcessor.java | 19 +++++++++++++---- ...alculatedFieldManagerMessageProcessor.java | 2 +- ...faultCalculatedFieldProcessingService.java | 1 - .../state/GeofencingCalculatedFieldState.java | 13 +----------- .../cf/ctx/state/GeofencingZoneState.java | 21 +++++++++---------- .../server/utils/CalculatedFieldUtils.java | 9 ++------ common/proto/src/main/proto/queue.proto | 4 +--- 7 files changed, 30 insertions(+), 39 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index d41feac542..95b705adfc 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -232,10 +232,16 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM CalculatedFieldCtx cfCtx = msg.getCfCtx(); CalculatedFieldId cfId = cfCtx.getCfId(); log.debug("[{}][{}] Processing CF check for updates msg.", entityId, cfId); + CalculatedFieldState currentState = states.get(cfId); try { - var state = updateStateFromDb(cfCtx); - if (state.isSizeOk()) { - processStateIfReady(cfCtx, Collections.singletonList(cfId), state, null, null, msg.getCallback()); + var stateFromDb = getStateFromDb(cfCtx); + if (currentState.equals(stateFromDb)) { + log.debug("[{}][{}] CF state is up-to-date.", entityId, cfId); + return; + } + states.put(cfId, stateFromDb); + if (stateFromDb.isSizeOk()) { + processStateIfReady(cfCtx, Collections.singletonList(cfId), stateFromDb, null, null, msg.getCallback()); } else { throw new RuntimeException(cfCtx.getSizeExceedsLimitMessage()); } @@ -298,6 +304,12 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } private CalculatedFieldState updateStateFromDb(CalculatedFieldCtx ctx) throws InterruptedException, ExecutionException, TimeoutException { + CalculatedFieldState stateFromDb = getStateFromDb(ctx); + states.put(ctx.getCfId(), stateFromDb); + return stateFromDb; + } + + private CalculatedFieldState getStateFromDb(CalculatedFieldCtx ctx) throws InterruptedException, ExecutionException, TimeoutException { ListenableFuture stateFuture = cfService.fetchStateFromDb(ctx, entityId); // Ugly but necessary. We do not expect to often fetch data from DB. Only once per pair lifetime. // This call happens while processing the CF pack from the queue consumer. So the timeout should be relatively low. @@ -305,7 +317,6 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM // but this will significantly complicate the code. CalculatedFieldState state = stateFuture.get(1, TimeUnit.MINUTES); state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); - states.put(ctx.getCfId(), state); return state; } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index f557a632b4..de6c38b9b9 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -409,7 +409,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware if (existingTask != null) { existingTask.cancel(false); String reason = cfDeleted ? "removal" : "update"; - log.debug("[{}][{}] Cancelled check for update task for CF due to: " + reason + "!", tenantId, cfId); + log.debug("[{}][{}] Cancelled check for update task due to CF " + reason + "!", tenantId, cfId); } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index 0284428c46..75ee581274 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -305,7 +305,6 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP } private ListenableFuture fetchGeofencingKvEntry(TenantId tenantId, List geofencingEntities, Argument argument) { - // TODO: Should we handle any other case? if (argument.getRefEntityKey().getType() != ArgumentType.ATTRIBUTE) { throw new IllegalStateException("Unsupported argument key type: " + argument.getRefEntityKey().getType()); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java index bc7460784b..0d6772b676 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java @@ -104,7 +104,6 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { } if (entryUpdated) { stateUpdated = true; - updateLastUpdateTimestamp(newEntry); } } return stateUpdated; @@ -138,18 +137,8 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { } } - private void updateLastUpdateTimestamp(ArgumentEntry entry) { - long newTs = this.latestTimestamp; - if (entry instanceof SingleValueArgumentEntry singleValueArgumentEntry) { - newTs = singleValueArgumentEntry.getTs(); - } - this.latestTimestamp = Math.max(this.latestTimestamp, newTs); - } - - // TODO: Ensure all cases are covered based on rule node logic. private List updateGeofencingZonesState(CalculatedFieldCtx ctx, boolean restricted) { var results = new ArrayList(); - long stateSwitchTime = System.currentTimeMillis(); double latitude = (double) arguments.get(ENTITY_ID_LATITUDE_ARGUMENT_KEY).getValue(); double longitude = (double) arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY).getValue(); @@ -159,7 +148,7 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { for (var zoneEntry : zonesEntry.getZoneStates().entrySet()) { GeofencingZoneState state = zoneEntry.getValue(); - String event = state.evaluate(entityCoordinates, stateSwitchTime); + String event = state.evaluate(entityCoordinates); ObjectNode stateNode = JacksonUtil.newObjectNode(); stateNode.put("entityId", ctx.getEntityId().toString()); stateNode.put("zoneId", state.getZoneId().toString()); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java index abee7cabb6..9e27907b73 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java @@ -16,10 +16,10 @@ package org.thingsboard.server.service.cf.ctx.state; import lombok.Data; +import lombok.EqualsAndHashCode; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.geo.Coordinates; import org.thingsboard.common.util.geo.PerimeterDefinition; -import org.thingsboard.rule.engine.geo.EntityGeofencingState; import org.thingsboard.rule.engine.util.GpsGeofencingEvents; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -39,7 +39,8 @@ public class GeofencingZoneState { private Long version; private PerimeterDefinition perimeterDefinition; - private EntityGeofencingState state; + @EqualsAndHashCode.Exclude + private Boolean inside; public GeofencingZoneState(EntityId zoneId, KvEntry entry) { this.zoneId = zoneId; @@ -59,7 +60,9 @@ public class GeofencingZoneState { this.ts = proto.getTs(); this.version = proto.getVersion(); this.perimeterDefinition = JacksonUtil.fromString(proto.getPerimeterDefinition(), PerimeterDefinition.class); - this.state = new EntityGeofencingState(proto.getInside(), proto.getStateSwitchTime(), proto.getStayed()); + if (proto.hasInside()) { + this.inside = proto.getInside(); + } } public boolean update(GeofencingZoneState newZoneState) { @@ -72,20 +75,16 @@ public class GeofencingZoneState { this.version = newVersion; this.perimeterDefinition = newZoneState.getPerimeterDefinition(); // TODO: should we reinitialize state if zone changed? + // this.inside = null; return true; } return false; } - public String evaluate(Coordinates entityCoordinates, long currentTs) { + public String evaluate(Coordinates entityCoordinates) { boolean inside = perimeterDefinition.checkMatches(entityCoordinates); - if (state == null) { - state = new EntityGeofencingState(inside, ts, false); - } - if (state.getStateSwitchTime() == 0L || state.isInside() != inside) { - state.setInside(inside); - state.setStateSwitchTime(currentTs); - state.setStayed(false); + if (this.inside == null || this.inside != inside) { + this.inside = inside; return inside ? GpsGeofencingEvents.ENTERED : GpsGeofencingEvents.LEFT; } return inside ? GpsGeofencingEvents.INSIDE : GpsGeofencingEvents.OUTSIDE; diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java index 302ced0df1..370f7883f0 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java @@ -16,7 +16,6 @@ package org.thingsboard.server.utils; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.rule.engine.geo.EntityGeofencingState; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.CalculatedFieldId; @@ -139,12 +138,8 @@ public class CalculatedFieldUtils { .setTs(zoneState.getTs()) .setVersion(zoneState.getVersion()) .setPerimeterDefinition(JacksonUtil.toString(zoneState.getPerimeterDefinition())); - if (zoneState.getState() != null) { - EntityGeofencingState state = zoneState.getState(); - builder.setInside(state.isInside()) - .setStayed(state.isStayed()) - .setStateSwitchTime(state.getStateSwitchTime()); - + if (zoneState.getInside() != null) { + builder.setInside(zoneState.getInside()); } return builder.build(); } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 885bad2cca..de55cd549d 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -905,9 +905,7 @@ message GeofencingZoneProto { int64 ts = 2; string perimeterDefinition = 3; int64 version = 4; - bool inside = 5; - int64 stateSwitchTime = 6; - bool stayed = 7; + optional bool inside = 5; } message GeofencingArgumentProto { From 51b610f679da1f67ea0b758366cbac427be388bb Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Mon, 4 Aug 2025 15:25:22 +0300 Subject: [PATCH 022/216] Refactoring default value --- .../server/service/edge/rpc/EdgeEventStorageSettings.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java index d9e3f1a920..618aee5e00 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeEventStorageSettings.java @@ -29,6 +29,6 @@ public class EdgeEventStorageSettings { private long noRecordsSleepInterval; @Value("${edges.storage.sleep_between_batches}") private long sleepIntervalBetweenBatches; - @Value("${edges.storage.misordering_compensation_millis:3600000}") + @Value("${edges.storage.misordering_compensation_millis:60000}") private long misorderingCompensationMillis; } From 82cca8c665989acbcb3cdfe4738ad8df5912c896 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Mon, 4 Aug 2025 18:27:30 +0300 Subject: [PATCH 023/216] renamed saveZones to allowedZones --- .../cf/DefaultCalculatedFieldProcessingService.java | 4 ++-- .../cf/ctx/state/GeofencingCalculatedFieldState.java | 8 ++++---- .../GeofencingCalculatedFieldConfiguration.java | 8 ++++---- common/proto/src/main/proto/queue.proto | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index 75ee581274..d5e36cfc95 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -96,7 +96,7 @@ import static org.thingsboard.server.common.data.DataConstants.SCOPE; import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.RESTRICTED_ZONES_ARGUMENT_KEY; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.SAVE_ZONES_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ALLOWED_ZONES_ARGUMENT_KEY; import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto; @TbRuleEngineComponent @@ -138,7 +138,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP switch (entry.getKey()) { case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> argFutures.put(entry.getKey(), fetchKvEntry(ctx.getTenantId(), resolveEntityId(entityId, entry), entry.getValue())); - case SAVE_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY -> { + case ALLOWED_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY -> { var resolvedEntityIdsFuture = resolveGeofencingEntityIds(ctx.getTenantId(), entityId, entry); argFutures.put(entry.getKey(), Futures.transformAsync(resolvedEntityIdsFuture, resolvedEntityIds -> fetchGeofencingKvEntry(ctx.getTenantId(), resolvedEntityIds, entry.getValue()), calculatedFieldCallbackExecutor)); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java index 0d6772b676..a98db7f0f0 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java @@ -34,7 +34,7 @@ import java.util.Map; import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.RESTRICTED_ZONES_ARGUMENT_KEY; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.SAVE_ZONES_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ALLOWED_ZONES_ARGUMENT_KEY; @Data public class GeofencingCalculatedFieldState implements CalculatedFieldState { @@ -48,7 +48,7 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { public GeofencingCalculatedFieldState() { - this(List.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, SAVE_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY)); + this(List.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, ALLOWED_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY)); } public GeofencingCalculatedFieldState(List argNames) { @@ -88,7 +88,7 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { arguments.put(key, singleValueArgumentEntry); entryUpdated = true; break; - case SAVE_ZONES_ARGUMENT_KEY: + case ALLOWED_ZONES_ARGUMENT_KEY: case RESTRICTED_ZONES_ARGUMENT_KEY: if (!(newEntry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) { throw new IllegalArgumentException(key + " argument must be a geofencing argument entry."); @@ -143,7 +143,7 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { double longitude = (double) arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY).getValue(); Coordinates entityCoordinates = new Coordinates(latitude, longitude); - String zoneKey = restricted ? RESTRICTED_ZONES_ARGUMENT_KEY : SAVE_ZONES_ARGUMENT_KEY; + String zoneKey = restricted ? RESTRICTED_ZONES_ARGUMENT_KEY : ALLOWED_ZONES_ARGUMENT_KEY; GeofencingArgumentEntry zonesEntry = (GeofencingArgumentEntry) arguments.get(zoneKey); for (var zoneEntry : zonesEntry.getZoneStates().entrySet()) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java index 3d85afa77c..98850f0797 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java @@ -29,13 +29,13 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC public static final String ENTITY_ID_LATITUDE_ARGUMENT_KEY = "latitude"; public static final String ENTITY_ID_LONGITUDE_ARGUMENT_KEY = "longitude"; - public static final String SAVE_ZONES_ARGUMENT_KEY = "saveZones"; + public static final String ALLOWED_ZONES_ARGUMENT_KEY = "allowedZones"; public static final String RESTRICTED_ZONES_ARGUMENT_KEY = "restrictedZones"; private static final Set requiredKeys = Set.of( ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, - SAVE_ZONES_ARGUMENT_KEY, + ALLOWED_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY ); @@ -68,11 +68,11 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC if (dynamicSource != null) { String test = "test"; throw new IllegalArgumentException("Dynamic source configuration is forbidden for '" + requiredKey + "' argument. " + - "Only '" + SAVE_ZONES_ARGUMENT_KEY + "' and '" + RESTRICTED_ZONES_ARGUMENT_KEY + "' " + + "Only '" + ALLOWED_ZONES_ARGUMENT_KEY + "' and '" + RESTRICTED_ZONES_ARGUMENT_KEY + "' " + "may use dynamic source configuration."); } } - case SAVE_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY -> { + case ALLOWED_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY -> { if (!ArgumentType.ATTRIBUTE.equals(refEntityKey.getType())) { throw new IllegalArgumentException("Argument: '" + requiredKey + "' must be set to " + ArgumentType.ATTRIBUTE + " type!"); } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index de55cd549d..87040fcf02 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -909,7 +909,7 @@ message GeofencingZoneProto { } message GeofencingArgumentProto { - string argName = 1; // e.g., "restrictedZones" or "saveZones" + string argName = 1; // e.g., "restrictedZones" or "allowedZones" repeated GeofencingZoneProto zones = 2; } From f1c80e43795946cb7b3d5485930faaf3fa790228 Mon Sep 17 00:00:00 2001 From: Ekaterina Chantsova Date: Tue, 22 Jul 2025 20:44:43 +0300 Subject: [PATCH 024/216] Timewindow: remove timewindow config from widget if unused --- .../core/services/dashboard-utils.service.ts | 31 +++++++++++++++++-- .../dashboard-page.component.ts | 3 +- .../config/widget-config.component.models.ts | 17 ++++++++-- .../widget/widget-config.component.ts | 29 ++++++++++------- ui-ngx/src/app/shared/models/widget.models.ts | 8 +++++ 5 files changed, 71 insertions(+), 17 deletions(-) diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index ecb3e65c61..b743126fdc 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -38,6 +38,7 @@ import { import { deepClone, isDefined, isDefinedAndNotNull, isNotEmptyStr, isString, isUndefined } from '@core/utils'; import { Datasource, + datasourcesHasAggregation, datasourcesHasOnlyComparisonAggregation, DatasourceType, defaultLegendConfig, @@ -49,7 +50,8 @@ import { WidgetConfigMode, WidgetSize, widgetType, - WidgetTypeDescriptor + WidgetTypeDescriptor, + widgetTypeHasTimewindow } from '@app/shared/models/widget.models'; import { EntityType } from '@shared/models/entity-type.models'; import { AliasFilterType, EntityAlias, EntityAliasFilter } from '@app/shared/models/alias.models'; @@ -295,8 +297,11 @@ export class DashboardUtilsService { widgetConfig.datasources = this.validateAndUpdateDatasources(widgetConfig.datasources); if (type === widgetType.latest) { const onlyHistoryTimewindow = datasourcesHasOnlyComparisonAggregation(widgetConfig.datasources); - widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true, - onlyHistoryTimewindow, this.timeService, false); + const aggregationEnabledForKeys = datasourcesHasAggregation(widgetConfig.datasources); + if (aggregationEnabledForKeys) { + widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true, + onlyHistoryTimewindow, this.timeService, false); + } } else if (type === widgetType.rpc) { if (widgetConfig.targetDeviceAliasIds && widgetConfig.targetDeviceAliasIds.length) { widgetConfig.targetDevice = { @@ -346,9 +351,29 @@ export class DashboardUtilsService { } } } + + this.removeTimewindowConfigIfUnused(widgetConfig, type); return widgetConfig; } + public removeTimewindowConfigIfUnused(widgetConfig: WidgetConfig, type: widgetType) { + const widgetHasTimewindow = widgetTypeHasTimewindow(type) || (type === widgetType.latest && datasourcesHasAggregation(widgetConfig.datasources)); + if (!widgetHasTimewindow || widgetConfig.useDashboardTimewindow) { + delete widgetConfig.displayTimewindow; + delete widgetConfig.timewindow; + delete widgetConfig.timewindowStyle; + + if (!widgetHasTimewindow) { + delete widgetConfig.useDashboardTimewindow; + } + } + } + + public prepareWidgetForSaving(widget: Widget): Widget { + this.removeTimewindowConfigIfUnused(widget.config, widget.type); + return widget; + } + public prepareWidgetForScadaLayout(widget: Widget, isScada: boolean): Widget { const config = widget.config; config.showTitle = false; diff --git a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts index 60d05d4c86..4c528fd55a 100644 --- a/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts +++ b/ui-ngx/src/app/modules/home/components/dashboard-page/dashboard-page.component.ts @@ -1322,6 +1322,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC } private addWidgetToDashboard(widget: Widget) { + this.dashboardUtils.prepareWidgetForSaving(widget); if (this.addingLayoutCtx) { this.addWidgetToLayout(widget, this.addingLayoutCtx.id); this.addingLayoutCtx = null; @@ -1409,7 +1410,7 @@ export class DashboardPageComponent extends PageComponent implements IDashboardC saveWidget() { this.editWidgetComponent.widgetFormGroup.markAsPristine(); - const widget = deepClone(this.editingWidget); + const widget = this.dashboardUtils.prepareWidgetForSaving(deepClone(this.editingWidget)); const widgetLayout = deepClone(this.editingWidgetLayout); const id = this.editingWidgetOriginal.id; this.dashboardConfiguration.widgets[id] = widget; diff --git a/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts index ec6af2857c..b359a16f51 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/widget-config.component.models.ts @@ -23,11 +23,19 @@ import { PageComponent } from '@shared/components/page.component'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { AbstractControl, UntypedFormGroup } from '@angular/forms'; -import { DataKey, DatasourceType, Widget, WidgetConfigMode, widgetType } from '@shared/models/widget.models'; +import { + DataKey, + DatasourceType, + Widget, + widgetTypeCanHaveTimewindow, + WidgetConfigMode, + widgetType +} from '@shared/models/widget.models'; import { WidgetConfigComponent } from '@home/components/widget/widget-config.component'; -import { isDefinedAndNotNull } from '@core/utils'; +import { isDefinedAndNotNull, isUndefinedOrNull } from '@core/utils'; import { IAliasController } from '@core/api/widget-api.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models'; export type WidgetConfigCallbacks = DatasourceCallbacks & WidgetActionCallbacks; @@ -107,6 +115,11 @@ export abstract class BasicWidgetConfigComponent extends PageComponent implement if (this.isAdd) { this.setupDefaults(widgetConfig); } + if (widgetTypeCanHaveTimewindow(widgetConfig.widgetType) && isUndefinedOrNull(widgetConfig.config.timewindow)) { + widgetConfig.config.timewindow = initModelFromDefaultTimewindow(null, + widgetConfig.widgetType === widgetType.latest, false, this.widgetConfigComponent.timeService, + widgetConfig.widgetType === widgetType.timeseries); + } this.onConfigSet(widgetConfig); this.updateValidators(false); for (const trigger of this.validatorTriggers()) { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 4c2d3d1f43..3cc9d4c6d1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -38,6 +38,7 @@ import { TargetDevice, targetDeviceValid, Widget, + widgetTypeCanHaveTimewindow, WidgetConfigMode, widgetType } from '@shared/models/widget.models'; @@ -53,7 +54,7 @@ import { Validators } from '@angular/forms'; import { WidgetConfigComponentData } from '@home/models/widget-component.models'; -import { deepClone, genNextLabel, isDefined, isObject } from '@app/core/utils'; +import { deepClone, genNextLabel, isDefined, isDefinedAndNotNull, isObject } from '@app/core/utils'; import { alarmFields, AlarmSearchStatus } from '@shared/models/alarm.models'; import { IAliasController } from '@core/api/widget-api.models'; import { EntityAlias } from '@shared/models/alias.models'; @@ -84,6 +85,8 @@ import { DataKeySettingsFunction } from '@home/components/widget/lib/settings/co import { defaultFormProperties, FormProperty } from '@shared/models/dynamic-form.models'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { WidgetService } from '@core/http/widget.service'; +import { TimeService } from '@core/services/time.service'; +import { initModelFromDefaultTimewindow } from '@shared/models/time/time.models'; import Timeout = NodeJS.Timeout; @Component({ @@ -201,6 +204,7 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe constructor(protected store: Store, private utils: UtilsService, private entityService: EntityService, + public timeService: TimeService, private dialog: MatDialog, public translate: TranslateService, private fb: UntypedFormBuilder, @@ -366,16 +370,16 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe this.dataSettings = this.fb.group({}); this.targetDeviceSettings = this.fb.group({}); this.advancedSettings = this.fb.group({}); - if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) { + if (widgetTypeCanHaveTimewindow(this.widgetType)) { this.dataSettings.addControl('timewindowConfig', this.fb.control({ - useDashboardTimewindow: true, - displayTimewindow: true, + useDashboardTimewindow: this.widgetType !== widgetType.latest, + displayTimewindow: this.widgetType !== widgetType.latest, timewindow: null, timewindowStyle: null })); - if (this.widgetType === widgetType.alarm) { - this.dataSettings.addControl('alarmFilterConfig', this.fb.control(null)); - } + } + if (this.widgetType === widgetType.alarm) { + this.dataSettings.addControl('alarmFilterConfig', this.fb.control(null)); } if (this.modelValue.isDataEnabled) { if (this.widgetType !== widgetType.rpc && @@ -529,14 +533,17 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe }, {emitEvent: false} ); - if (this.widgetType === widgetType.timeseries || this.widgetType === widgetType.alarm || this.widgetType === widgetType.latest) { + if (widgetTypeCanHaveTimewindow(this.widgetType)) { const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ? - config.useDashboardTimewindow : true; + config.useDashboardTimewindow : this.widgetType !== widgetType.latest; this.dataSettings.get('timewindowConfig').patchValue({ useDashboardTimewindow, displayTimewindow: isDefined(config.displayTimewindow) ? - config.displayTimewindow : true, - timewindow: config.timewindow, + config.displayTimewindow : this.widgetType !== widgetType.latest, + timewindow: isDefinedAndNotNull(config.timewindow) + ? config.timewindow + : initModelFromDefaultTimewindow(null, this.widgetType === widgetType.latest, this.onlyHistoryTimewindow(), + this.timeService, this.widgetType === widgetType.timeseries), timewindowStyle: config.timewindowStyle }, {emitEvent: false}); } diff --git a/ui-ngx/src/app/shared/models/widget.models.ts b/ui-ngx/src/app/shared/models/widget.models.ts index 00b62cfb72..45ae5986d9 100644 --- a/ui-ngx/src/app/shared/models/widget.models.ts +++ b/ui-ngx/src/app/shared/models/widget.models.ts @@ -489,6 +489,14 @@ export const targetDeviceValid = (targetDevice?: TargetDevice): boolean => ((targetDevice.type === TargetDeviceType.device && !!targetDevice.deviceId) || (targetDevice.type === TargetDeviceType.entity && !!targetDevice.entityAliasId)); +export const widgetTypeHasTimewindow = (type: widgetType): boolean => { + return type === widgetType.timeseries || type === widgetType.alarm; +} + +export const widgetTypeCanHaveTimewindow = (type: widgetType): boolean => { + return widgetTypeHasTimewindow(type) || type === widgetType.latest; +} + export const datasourcesHasAggregation = (datasources?: Array): boolean => { if (datasources) { const foundDatasource = datasources.find(datasource => { From 442c45524e30a9094f1685e404befa8d865feb09 Mon Sep 17 00:00:00 2001 From: Ekaterina Chantsova Date: Tue, 5 Aug 2025 16:36:29 +0300 Subject: [PATCH 025/216] Refactoring: optimize condition for latest widget timewindow; fix types --- ui-ngx/src/app/core/services/dashboard-utils.service.ts | 7 +++---- ui-ngx/src/app/core/utils.ts | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/ui-ngx/src/app/core/services/dashboard-utils.service.ts b/ui-ngx/src/app/core/services/dashboard-utils.service.ts index b743126fdc..7101aa0a2e 100644 --- a/ui-ngx/src/app/core/services/dashboard-utils.service.ts +++ b/ui-ngx/src/app/core/services/dashboard-utils.service.ts @@ -296,9 +296,8 @@ export class DashboardUtilsService { } widgetConfig.datasources = this.validateAndUpdateDatasources(widgetConfig.datasources); if (type === widgetType.latest) { - const onlyHistoryTimewindow = datasourcesHasOnlyComparisonAggregation(widgetConfig.datasources); - const aggregationEnabledForKeys = datasourcesHasAggregation(widgetConfig.datasources); - if (aggregationEnabledForKeys) { + if (datasourcesHasAggregation(widgetConfig.datasources)) { + const onlyHistoryTimewindow = datasourcesHasOnlyComparisonAggregation(widgetConfig.datasources); widgetConfig.timewindow = initModelFromDefaultTimewindow(widgetConfig.timewindow, true, onlyHistoryTimewindow, this.timeService, false); } @@ -356,7 +355,7 @@ export class DashboardUtilsService { return widgetConfig; } - public removeTimewindowConfigIfUnused(widgetConfig: WidgetConfig, type: widgetType) { + private removeTimewindowConfigIfUnused(widgetConfig: WidgetConfig, type: widgetType) { const widgetHasTimewindow = widgetTypeHasTimewindow(type) || (type === widgetType.latest && datasourcesHasAggregation(widgetConfig.datasources)); if (!widgetHasTimewindow || widgetConfig.useDashboardTimewindow) { delete widgetConfig.displayTimewindow; diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 9937689ba8..0c1b066887 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -197,7 +197,7 @@ export function deleteNullProperties(obj: any) { }); } -export function deleteFalseProperties(obj: any) { +export function deleteFalseProperties(obj: Record): void { if (isUndefinedOrNull(obj)) { return; } From ede9fd5e05e91665e8da7d39407e1e7df1d27eac Mon Sep 17 00:00:00 2001 From: dshvaika Date: Wed, 6 Aug 2025 16:12:33 +0300 Subject: [PATCH 026/216] Added support to use only one zone type instead of two + minor validation fixes --- .../state/GeofencingCalculatedFieldState.java | 27 ++++--- .../cf/ctx/state/GeofencingZoneState.java | 3 + ...eofencingCalculatedFieldConfiguration.java | 72 ++++++++++++++----- ...lationQueryDynamicSourceConfiguration.java | 6 +- .../CalculatedFieldDataValidator.java | 4 +- 5 files changed, 77 insertions(+), 35 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java index a98db7f0f0..960fdf217f 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.cf.ctx.state; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import lombok.AllArgsConstructor; import lombok.Data; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.geo.Coordinates; @@ -31,12 +32,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ALLOWED_ZONES_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.RESTRICTED_ZONES_ARGUMENT_KEY; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ALLOWED_ZONES_ARGUMENT_KEY; @Data +@AllArgsConstructor public class GeofencingCalculatedFieldState implements CalculatedFieldState { private List requiredArguments; @@ -46,9 +48,8 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { private long latestTimestamp = -1; - public GeofencingCalculatedFieldState() { - this(List.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, ALLOWED_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY)); + this(new ArrayList<>(), new HashMap<>(), false, -1); } public GeofencingCalculatedFieldState(List argNames) { @@ -112,8 +113,12 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { @Override public ListenableFuture> performCalculation(CalculatedFieldCtx ctx) { - List savedZonesStatesResults = updateGeofencingZonesState(ctx, false); - List restrictedZonesStatesResults = updateGeofencingZonesState(ctx, true); + double latitude = (double) arguments.get(ENTITY_ID_LATITUDE_ARGUMENT_KEY).getValue(); + double longitude = (double) arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY).getValue(); + Coordinates entityCoordinates = new Coordinates(latitude, longitude); + + List savedZonesStatesResults = updateGeofencingZonesState(ctx, entityCoordinates, false); + List restrictedZonesStatesResults = updateGeofencingZonesState(ctx, entityCoordinates, true); List allZoneStatesResults = new ArrayList<>(savedZonesStatesResults.size() + restrictedZonesStatesResults.size()); @@ -137,15 +142,15 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { } } - private List updateGeofencingZonesState(CalculatedFieldCtx ctx, boolean restricted) { - var results = new ArrayList(); - double latitude = (double) arguments.get(ENTITY_ID_LATITUDE_ARGUMENT_KEY).getValue(); - double longitude = (double) arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY).getValue(); - - Coordinates entityCoordinates = new Coordinates(latitude, longitude); + private List updateGeofencingZonesState(CalculatedFieldCtx ctx, Coordinates entityCoordinates, boolean restricted) { String zoneKey = restricted ? RESTRICTED_ZONES_ARGUMENT_KEY : ALLOWED_ZONES_ARGUMENT_KEY; GeofencingArgumentEntry zonesEntry = (GeofencingArgumentEntry) arguments.get(zoneKey); + if (zonesEntry == null) { + return List.of(); + } + + var results = new ArrayList(); for (var zoneEntry : zonesEntry.getZoneStates().entrySet()) { GeofencingZoneState state = zoneEntry.getValue(); String event = state.evaluate(entityCoordinates); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java index 9e27907b73..d4a42d0645 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java @@ -83,6 +83,9 @@ public class GeofencingZoneState { public String evaluate(Coordinates entityCoordinates) { boolean inside = perimeterDefinition.checkMatches(entityCoordinates); + // TODO: maybe handle this.inside == null as ENTERED or OUTSIDE. + // Since if this.inside == null then we don't have a state for this zone yet + // and logically say that we are OUTSIDE instead of LEFT. if (this.inside == null || this.inside != inside) { this.inside = inside; return inside ? GpsGeofencingEvents.ENTERED : GpsGeofencingEvents.LEFT; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java index 98850f0797..b5482b3e96 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java @@ -19,6 +19,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import java.util.Map; import java.util.Set; import static org.thingsboard.server.common.data.cf.configuration.CFArgumentDynamicSourceType.RELATION_QUERY; @@ -32,13 +33,18 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC public static final String ALLOWED_ZONES_ARGUMENT_KEY = "allowedZones"; public static final String RESTRICTED_ZONES_ARGUMENT_KEY = "restrictedZones"; - private static final Set requiredKeys = Set.of( + private static final Set allowedKeys = Set.of( ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, ALLOWED_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY ); + private static final Set requiredKeys = Set.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY + ); + @Override public CalculatedFieldType getType() { return CalculatedFieldType.GEOFENCING; @@ -47,44 +53,72 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC // TODO: update validate method in PE version. @Override public void validate() { - if (arguments == null || arguments.size() != 4) { - throw new IllegalArgumentException("Geofencing calculated field configuration must contain exactly 4 arguments: " + requiredKeys); + if (arguments == null) { + throw new IllegalArgumentException("Geofencing calculated field arguments are empty!"); } + + // Check key count + if (arguments.size() < 3 || arguments.size() > 4) { + throw new IllegalArgumentException("Geofencing calculated field must contain 3 or 4 arguments: " + allowedKeys); + } + + // Check for unsupported argument keys + for (String key : arguments.keySet()) { + if (!allowedKeys.contains(key)) { + throw new IllegalArgumentException("Unsupported argument key: '" + key + "'. Allowed keys: " + allowedKeys); + } + } + + // Check required fields: latitude and longitude for (String requiredKey : requiredKeys) { - Argument argument = arguments.get(requiredKey); - if (argument == null) { + if (!arguments.containsKey(requiredKey)) { throw new IllegalArgumentException("Missing required argument: " + requiredKey); } + } + + // Ensure at least one of the zone types is configured + boolean hasAllowedZones = arguments.containsKey(ALLOWED_ZONES_ARGUMENT_KEY); + boolean hasRestrictedZones = arguments.containsKey(RESTRICTED_ZONES_ARGUMENT_KEY); + + if (!hasAllowedZones && !hasRestrictedZones) { + throw new IllegalArgumentException("Geofencing calculated field must contain at least one of the following arguments: 'allowedZones' or 'restrictedZones'"); + } + + for (Map.Entry entry : arguments.entrySet()) { + String argumentKey = entry.getKey(); + Argument argument = entry.getValue(); + if (argument == null) { + throw new IllegalArgumentException("Missing required argument: " + argumentKey); + } ReferencedEntityKey refEntityKey = argument.getRefEntityKey(); if (refEntityKey == null || refEntityKey.getType() == null) { - throw new IllegalArgumentException("Missing or invalid reference entity key for argument: " + requiredKey); + throw new IllegalArgumentException("Missing or invalid reference entity key for argument: " + argumentKey); } - switch (requiredKey) { - case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> { + + switch (argumentKey) { + case ENTITY_ID_LATITUDE_ARGUMENT_KEY, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> { if (!ArgumentType.TS_LATEST.equals(refEntityKey.getType())) { - throw new IllegalArgumentException("Argument: '" + requiredKey + "' must be set to " + ArgumentType.TS_LATEST + " type!"); + throw new IllegalArgumentException("Argument '" + argumentKey + "' must be of type TS_LATEST."); } - var dynamicSource = argument.getRefDynamicSource(); - if (dynamicSource != null) { - String test = "test"; - throw new IllegalArgumentException("Dynamic source configuration is forbidden for '" + requiredKey + "' argument. " + - "Only '" + ALLOWED_ZONES_ARGUMENT_KEY + "' and '" + RESTRICTED_ZONES_ARGUMENT_KEY + "' " + - "may use dynamic source configuration."); + if (argument.getRefDynamicSource() != null) { + throw new IllegalArgumentException("Dynamic source is not allowed for argument: '" + argumentKey + "'."); } } - case ALLOWED_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY -> { + case ALLOWED_ZONES_ARGUMENT_KEY, + RESTRICTED_ZONES_ARGUMENT_KEY -> { if (!ArgumentType.ATTRIBUTE.equals(refEntityKey.getType())) { - throw new IllegalArgumentException("Argument: '" + requiredKey + "' must be set to " + ArgumentType.ATTRIBUTE + " type!"); + throw new IllegalArgumentException("Argument '" + argumentKey + "' must be of type ATTRIBUTE."); } var dynamicSource = argument.getRefDynamicSource(); if (dynamicSource == null) { continue; } if (!RELATION_QUERY.equals(dynamicSource)) { - throw new IllegalArgumentException("Only relation query dynamic source is supported for argument: " + requiredKey); + throw new IllegalArgumentException("Only relation query dynamic source is supported for argument: '" + argumentKey + "'."); } if (argument.getRefDynamicSourceConfiguration() == null) { - throw new IllegalArgumentException("Missing dynamic source configuration for: " + requiredKey); + throw new IllegalArgumentException("Missing dynamic source configuration for argument: '" + argumentKey + "'."); } argument.getRefDynamicSourceConfiguration().validate(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java index 85f1ee21bd..b6e085395e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java @@ -34,7 +34,7 @@ public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynami private boolean fetchLastLevelOnly; private EntitySearchDirection direction; private String relationType; - private List profiles; + private List entityTypes; @Override public CFArgumentDynamicSourceType getType() { @@ -56,7 +56,7 @@ public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynami @Override public boolean isSimpleRelation() { - return maxLevel == 1 && (profiles == null || profiles.isEmpty()); + return maxLevel == 1 && (entityTypes == null || entityTypes.isEmpty()); } @Override @@ -66,7 +66,7 @@ public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynami } var entityRelationsQuery = new EntityRelationsQuery(); entityRelationsQuery.setParameters(new RelationsSearchParameters(rootEntityId, direction, maxLevel, fetchLastLevelOnly)); - entityRelationsQuery.setFilters(Collections.singletonList(new RelationEntityTypeFilter(relationType, profiles))); + entityRelationsQuery.setFilters(Collections.singletonList(new RelationEntityTypeFilter(relationType, entityTypes))); return entityRelationsQuery; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java index 12d9764af2..4fe663ff5d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java @@ -70,8 +70,8 @@ public class CalculatedFieldDataValidator extends DataValidator if (maxArgumentsPerCF <= 0) { return; } - if (CalculatedFieldType.GEOFENCING.equals(calculatedField.getType()) && maxArgumentsPerCF < 4) { - throw new DataValidationException("Geofencing calculated field requires 4 arguments, but the system limit is " + + if (CalculatedFieldType.GEOFENCING.equals(calculatedField.getType()) && maxArgumentsPerCF < 3) { + throw new DataValidationException("Geofencing calculated field requires at least 3 arguments, but the system limit is " + maxArgumentsPerCF + ". Contact your administrator to increase the limit." ); } From 88ec0f91d0c1817bdd17171403a8455e4be8bd2d Mon Sep 17 00:00:00 2001 From: Ekaterina Chantsova Date: Wed, 6 Aug 2025 18:40:00 +0300 Subject: [PATCH 027/216] Timewindow: revert default values for useDashboardTimewindow and displayTimewindow --- .../home/components/widget/widget-config.component.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts index 3cc9d4c6d1..e34783f5b7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.ts @@ -372,8 +372,8 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe this.advancedSettings = this.fb.group({}); if (widgetTypeCanHaveTimewindow(this.widgetType)) { this.dataSettings.addControl('timewindowConfig', this.fb.control({ - useDashboardTimewindow: this.widgetType !== widgetType.latest, - displayTimewindow: this.widgetType !== widgetType.latest, + useDashboardTimewindow: true, + displayTimewindow: true, timewindow: null, timewindowStyle: null })); @@ -535,11 +535,11 @@ export class WidgetConfigComponent extends PageComponent implements OnInit, OnDe ); if (widgetTypeCanHaveTimewindow(this.widgetType)) { const useDashboardTimewindow = isDefined(config.useDashboardTimewindow) ? - config.useDashboardTimewindow : this.widgetType !== widgetType.latest; + config.useDashboardTimewindow : true; this.dataSettings.get('timewindowConfig').patchValue({ useDashboardTimewindow, displayTimewindow: isDefined(config.displayTimewindow) ? - config.displayTimewindow : this.widgetType !== widgetType.latest, + config.displayTimewindow : true, timewindow: isDefinedAndNotNull(config.timewindow) ? config.timewindow : initModelFromDefaultTimewindow(null, this.widgetType === widgetType.latest, this.onlyHistoryTimewindow(), From c783176e71ce886e094bbb54d648ea0e480bbe20 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Thu, 7 Aug 2025 15:51:19 +0300 Subject: [PATCH 028/216] Updated to use zone groups --- ...faultCalculatedFieldProcessingService.java | 18 ++- .../service/cf/ctx/state/ArgumentEntry.java | 5 +- .../cf/ctx/state/GeofencingArgumentEntry.java | 9 +- .../state/GeofencingCalculatedFieldState.java | 108 ++++++++----- .../cf/ctx/state/GeofencingZoneState.java | 20 ++- .../server/utils/CalculatedFieldUtils.java | 23 ++- ...eofencingCalculatedFieldConfiguration.java | 153 ++++++++++-------- .../cf/configuration/GeofencingEvent.java | 49 ++++++ .../GeofencingZoneGroupConfiguration.java | 28 ++++ common/proto/src/main/proto/queue.proto | 13 +- 10 files changed, 292 insertions(+), 134 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingZoneGroupConfiguration.java diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index d5e36cfc95..29c4809a75 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -35,6 +35,8 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; @@ -95,8 +97,6 @@ import java.util.stream.Collectors; import static org.thingsboard.server.common.data.DataConstants.SCOPE; import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.RESTRICTED_ZONES_ARGUMENT_KEY; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ALLOWED_ZONES_ARGUMENT_KEY; import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto; @TbRuleEngineComponent @@ -132,16 +132,17 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP Map> argFutures = new HashMap<>(); if (ctx.getCalculatedField().getType().equals(CalculatedFieldType.GEOFENCING)) { - // Ignoring any other arguments except ENTITY_ID_LATITUDE_ARGUMENT_KEY, - // ENTITY_ID_LONGITUDE_ARGUMENT_KEY, SAVE_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY. + var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); + var zoneGroupConfigs = configuration.getGeofencingZoneGroupConfigurations(); for (var entry : ctx.getArguments().entrySet()) { switch (entry.getKey()) { case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> argFutures.put(entry.getKey(), fetchKvEntry(ctx.getTenantId(), resolveEntityId(entityId, entry), entry.getValue())); - case ALLOWED_ZONES_ARGUMENT_KEY, RESTRICTED_ZONES_ARGUMENT_KEY -> { + default -> { + var zoneGroupConfiguration = zoneGroupConfigs.get(entry.getKey()); var resolvedEntityIdsFuture = resolveGeofencingEntityIds(ctx.getTenantId(), entityId, entry); argFutures.put(entry.getKey(), Futures.transformAsync(resolvedEntityIdsFuture, resolvedEntityIds -> - fetchGeofencingKvEntry(ctx.getTenantId(), resolvedEntityIds, entry.getValue()), calculatedFieldCallbackExecutor)); + fetchGeofencingKvEntry(ctx.getTenantId(), resolvedEntityIds, entry.getValue(), zoneGroupConfiguration), calculatedFieldCallbackExecutor)); } } } @@ -304,7 +305,8 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP }; } - private ListenableFuture fetchGeofencingKvEntry(TenantId tenantId, List geofencingEntities, Argument argument) { + private ListenableFuture fetchGeofencingKvEntry(TenantId tenantId, List geofencingEntities, + Argument argument, GeofencingZoneGroupConfiguration zoneGroupConfiguration) { if (argument.getRefEntityKey().getType() != ArgumentType.ATTRIBUTE) { throw new IllegalStateException("Unsupported argument key type: " + argument.getRefEntityKey().getType()); } @@ -326,7 +328,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP ListenableFuture>> allFutures = Futures.allAsList(kvFutures); return Futures.transform(allFutures, entries -> ArgumentEntry.createGeofencingValueArgument(entries.stream() - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))), + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)), zoneGroupConfiguration), calculatedFieldCallbackExecutor ); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java index c7f830431b..f76c6855a6 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.thingsboard.script.api.tbel.TbelCfArg; +import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; @@ -61,8 +62,8 @@ public interface ArgumentEntry { return new TsRollingArgumentEntry(kvEntries, limit, timeWindow); } - static ArgumentEntry createGeofencingValueArgument(Map entityIdkvEntryMap) { - return new GeofencingArgumentEntry(entityIdkvEntryMap); + static ArgumentEntry createGeofencingValueArgument(Map entityIdkvEntryMap, GeofencingZoneGroupConfiguration zoneGroupConfiguration) { + return new GeofencingArgumentEntry(entityIdkvEntryMap, zoneGroupConfiguration); } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java index cf77d5da7d..2acdf0be4c 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java @@ -19,6 +19,7 @@ import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfTsGeofencingArg; +import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.KvEntry; @@ -31,13 +32,17 @@ import java.util.stream.Collectors; public class GeofencingArgumentEntry implements ArgumentEntry { private Map zoneStates; + private GeofencingZoneGroupConfiguration zoneGroupConfiguration; + private boolean forceResetPrevious; public GeofencingArgumentEntry() { } - public GeofencingArgumentEntry(Map entityIdKvEntryMap) { - this.zoneStates = toZones(entityIdKvEntryMap); + public GeofencingArgumentEntry(Map entityIdkvEntryMap, + GeofencingZoneGroupConfiguration zoneGroupConfiguration) { + this.zoneStates = toZones(entityIdkvEntryMap); + this.zoneGroupConfiguration = zoneGroupConfiguration; } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java index 960fdf217f..6d8a08914d 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java @@ -23,6 +23,7 @@ import lombok.Data; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.geo.Coordinates; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.utils.CalculatedFieldUtils; @@ -31,11 +32,13 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ALLOWED_ZONES_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.RESTRICTED_ZONES_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.coordinateKeys; @Data @AllArgsConstructor @@ -70,7 +73,7 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { boolean stateUpdated = false; - for (Map.Entry entry : argumentValues.entrySet()) { + for (var entry : argumentValues.entrySet()) { String key = entry.getKey(); ArgumentEntry newEntry = entry.getValue(); @@ -80,26 +83,22 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { boolean entryUpdated; if (existingEntry == null || newEntry.isForceResetPrevious()) { - switch (key) { - case ENTITY_ID_LATITUDE_ARGUMENT_KEY: - case ENTITY_ID_LONGITUDE_ARGUMENT_KEY: + entryUpdated = switch (key) { + case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> { if (!(newEntry instanceof SingleValueArgumentEntry singleValueArgumentEntry)) { throw new IllegalArgumentException(key + " argument must be a single value argument."); } arguments.put(key, singleValueArgumentEntry); - entryUpdated = true; - break; - case ALLOWED_ZONES_ARGUMENT_KEY: - case RESTRICTED_ZONES_ARGUMENT_KEY: + yield true; + } + default -> { if (!(newEntry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) { throw new IllegalArgumentException(key + " argument must be a geofencing argument entry."); } arguments.put(key, geofencingArgumentEntry); - entryUpdated = true; - break; - default: - throw new IllegalArgumentException("Unsupported argument: " + key); - } + yield true; + } + }; } else { entryUpdated = existingEntry.updateEntry(newEntry); } @@ -111,21 +110,60 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { } + // TODO: Probably returning list of CalculatedFieldResult no needed anymore, + // since logic changed to use zone groups with telemetry prefix. @Override public ListenableFuture> performCalculation(CalculatedFieldCtx ctx) { double latitude = (double) arguments.get(ENTITY_ID_LATITUDE_ARGUMENT_KEY).getValue(); double longitude = (double) arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY).getValue(); Coordinates entityCoordinates = new Coordinates(latitude, longitude); - List savedZonesStatesResults = updateGeofencingZonesState(ctx, entityCoordinates, false); - List restrictedZonesStatesResults = updateGeofencingZonesState(ctx, entityCoordinates, true); + ObjectNode resultNode = JacksonUtil.newObjectNode(); + getGeofencingArguments().forEach((argumentKey, argumentEntry) -> { + var zoneGroupConfig = argumentEntry.getZoneGroupConfiguration(); + Set zoneEvents = argumentEntry.getZoneStates() + .values() + .stream() + .map(zoneState -> zoneState.evaluate(entityCoordinates)) + .collect(Collectors.toSet()); + aggregateZoneGroupEvent(zoneEvents).ifPresent(event -> + resultNode.put(zoneGroupConfig.getReportTelemetryPrefix() + "Event", event.name()) + ); + }); + return Futures.immediateFuture(List.of(new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode))); + } - List allZoneStatesResults = - new ArrayList<>(savedZonesStatesResults.size() + restrictedZonesStatesResults.size()); - allZoneStatesResults.addAll(savedZonesStatesResults); - allZoneStatesResults.addAll(restrictedZonesStatesResults); + private Optional aggregateZoneGroupEvent(Set zoneEvents) { + boolean hasEntered = false; + boolean hasLeft = false; + boolean hasInside = false; + boolean hasOutside = false; - return Futures.immediateFuture(allZoneStatesResults); + for (GeofencingEvent event : zoneEvents) { + if (event == null) { + continue; + } + switch (event) { + case ENTERED -> hasEntered = true; + case LEFT -> hasLeft = true; + case INSIDE -> hasInside = true; + case OUTSIDE -> hasOutside = true; + } + } + + if (hasOutside && !hasInside && !hasEntered && !hasLeft) { + return Optional.of(GeofencingEvent.OUTSIDE); + } + if (hasLeft && !hasEntered && !hasInside) { + return Optional.of(GeofencingEvent.LEFT); + } + if (hasEntered && !hasLeft && !hasInside) { + return Optional.of(GeofencingEvent.ENTERED); + } + if (hasInside || hasEntered) { + return Optional.of(GeofencingEvent.INSIDE); + } + return Optional.empty(); } @Override @@ -142,26 +180,12 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { } } - private List updateGeofencingZonesState(CalculatedFieldCtx ctx, Coordinates entityCoordinates, boolean restricted) { - String zoneKey = restricted ? RESTRICTED_ZONES_ARGUMENT_KEY : ALLOWED_ZONES_ARGUMENT_KEY; - GeofencingArgumentEntry zonesEntry = (GeofencingArgumentEntry) arguments.get(zoneKey); - - if (zonesEntry == null) { - return List.of(); - } - - var results = new ArrayList(); - for (var zoneEntry : zonesEntry.getZoneStates().entrySet()) { - GeofencingZoneState state = zoneEntry.getValue(); - String event = state.evaluate(entityCoordinates); - ObjectNode stateNode = JacksonUtil.newObjectNode(); - stateNode.put("entityId", ctx.getEntityId().toString()); - stateNode.put("zoneId", state.getZoneId().toString()); - stateNode.put("restricted", restricted); - stateNode.put("event", event); - results.add(new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), stateNode)); - } - return results; + // TODO: Create a new class field to not do this on each calculation. + private Map getGeofencingArguments() { + return arguments.entrySet() + .stream() + .filter(entry -> !coordinateKeys.contains(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, entry -> (GeofencingArgumentEntry) entry.getValue())); } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java index d4a42d0645..1b3879c828 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java @@ -20,7 +20,7 @@ import lombok.EqualsAndHashCode; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.geo.Coordinates; import org.thingsboard.common.util.geo.PerimeterDefinition; -import org.thingsboard.rule.engine.util.GpsGeofencingEvents; +import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.kv.AttributeKvEntry; @@ -81,16 +81,20 @@ public class GeofencingZoneState { return false; } - public String evaluate(Coordinates entityCoordinates) { + public GeofencingEvent evaluate(Coordinates entityCoordinates) { boolean inside = perimeterDefinition.checkMatches(entityCoordinates); - // TODO: maybe handle this.inside == null as ENTERED or OUTSIDE. - // Since if this.inside == null then we don't have a state for this zone yet - // and logically say that we are OUTSIDE instead of LEFT. - if (this.inside == null || this.inside != inside) { + // Initial evaluation — no prior state + if (this.inside == null) { this.inside = inside; - return inside ? GpsGeofencingEvents.ENTERED : GpsGeofencingEvents.LEFT; + return inside ? GeofencingEvent.ENTERED : GeofencingEvent.OUTSIDE; } - return inside ? GpsGeofencingEvents.INSIDE : GpsGeofencingEvents.OUTSIDE; + // State changed + if (this.inside != inside) { + this.inside = inside; + return inside ? GeofencingEvent.ENTERED : GeofencingEvent.LEFT; + } + // State unchanged + return inside ? GeofencingEvent.INSIDE : GeofencingEvent.OUTSIDE; } } diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java index 370f7883f0..aaa68e1dd9 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java @@ -18,6 +18,8 @@ package org.thingsboard.server.utils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; +import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -28,6 +30,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldEntit import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldIdProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; import org.thingsboard.server.gen.transport.TransportProtos.GeofencingArgumentProto; +import org.thingsboard.server.gen.transport.TransportProtos.GeofencingEventProto; import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneIdProto; import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneProto; import org.thingsboard.server.gen.transport.TransportProtos.SingleValueArgumentProto; @@ -45,6 +48,7 @@ import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.TreeMap; @@ -123,12 +127,15 @@ public class CalculatedFieldUtils { private static GeofencingArgumentProto toGeofencingArgumentProto(String argName, GeofencingArgumentEntry geofencingArgumentEntry) { - GeofencingArgumentProto.Builder builder = GeofencingArgumentProto.newBuilder() - .setArgName(argName); + var zoneGroupConfiguration = geofencingArgumentEntry.getZoneGroupConfiguration(); Map zoneStates = geofencingArgumentEntry.getZoneStates(); - zoneStates.forEach((entityId, zoneState) -> { - builder.addZones(toGeofencingZoneProto(entityId, zoneState)); - }); + GeofencingArgumentProto.Builder builder = GeofencingArgumentProto.newBuilder() + .setArgName(argName) + .setTelemetryPrefix(zoneGroupConfiguration.getReportTelemetryPrefix()); + zoneStates.forEach((entityId, zoneState) -> + builder.addZones(toGeofencingZoneProto(entityId, zoneState))); + zoneGroupConfiguration.getReportEvents().forEach(event -> + builder.addReportEvents(GeofencingEventProto.forNumber(event.getProtoNumber()))); return builder.build(); } @@ -206,8 +213,14 @@ public class CalculatedFieldUtils { .stream() .map(GeofencingZoneState::new) .collect(Collectors.toMap(GeofencingZoneState::getZoneId, Function.identity())); + List geofencingEvents = proto.getReportEventsList() + .stream() + .map(geofencingEventProto -> GeofencingEvent.fromProtoNumber(geofencingEventProto.getNumber())) + .toList(); + var zoneGroupConfiguration = new GeofencingZoneGroupConfiguration(proto.getTelemetryPrefix(), geofencingEvents); GeofencingArgumentEntry geofencingArgumentEntry = new GeofencingArgumentEntry(); geofencingArgumentEntry.setZoneStates(zoneStates); + geofencingArgumentEntry.setZoneGroupConfiguration(zoneGroupConfiguration); return geofencingArgumentEntry; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java index b5482b3e96..78cb48c2ee 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java @@ -17,10 +17,14 @@ package org.thingsboard.server.common.data.cf.configuration; import lombok.Data; import lombok.EqualsAndHashCode; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.util.CollectionsUtil; +import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import static org.thingsboard.server.common.data.cf.configuration.CFArgumentDynamicSourceType.RELATION_QUERY; @@ -30,21 +34,14 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC public static final String ENTITY_ID_LATITUDE_ARGUMENT_KEY = "latitude"; public static final String ENTITY_ID_LONGITUDE_ARGUMENT_KEY = "longitude"; - public static final String ALLOWED_ZONES_ARGUMENT_KEY = "allowedZones"; - public static final String RESTRICTED_ZONES_ARGUMENT_KEY = "restrictedZones"; - private static final Set allowedKeys = Set.of( - ENTITY_ID_LATITUDE_ARGUMENT_KEY, - ENTITY_ID_LONGITUDE_ARGUMENT_KEY, - ALLOWED_ZONES_ARGUMENT_KEY, - RESTRICTED_ZONES_ARGUMENT_KEY - ); - - private static final Set requiredKeys = Set.of( + public static final Set coordinateKeys = Set.of( ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY ); + Map geofencingZoneGroupConfigurations; + @Override public CalculatedFieldType getType() { return CalculatedFieldType.GEOFENCING; @@ -56,74 +53,100 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC if (arguments == null) { throw new IllegalArgumentException("Geofencing calculated field arguments are empty!"); } - - // Check key count - if (arguments.size() < 3 || arguments.size() > 4) { - throw new IllegalArgumentException("Geofencing calculated field must contain 3 or 4 arguments: " + allowedKeys); + if (arguments.size() < 3) { + throw new IllegalArgumentException("Geofencing calculated field must contain at least 3 arguments!"); } - - // Check for unsupported argument keys - for (String key : arguments.keySet()) { - if (!allowedKeys.contains(key)) { - throw new IllegalArgumentException("Unsupported argument key: '" + key + "'. Allowed keys: " + allowedKeys); - } + if (arguments.size() > 5) { + throw new IllegalArgumentException("Geofencing calculated field size exceeds limit of 5 arguments!"); } + validateCoordinateArguments(); - // Check required fields: latitude and longitude - for (String requiredKey : requiredKeys) { - if (!arguments.containsKey(requiredKey)) { - throw new IllegalArgumentException("Missing required argument: " + requiredKey); - } + Map zoneGroupsArguments = getZoneGroupArguments(); + if (zoneGroupsArguments.isEmpty()) { + throw new IllegalArgumentException("Geofencing calculated field must contain at least one geofencing zone group defined!"); } + validateZoneGroupAruguments(zoneGroupsArguments); + validateZoneGroupConfigurations(zoneGroupsArguments); + } - // Ensure at least one of the zone types is configured - boolean hasAllowedZones = arguments.containsKey(ALLOWED_ZONES_ARGUMENT_KEY); - boolean hasRestrictedZones = arguments.containsKey(RESTRICTED_ZONES_ARGUMENT_KEY); - - if (!hasAllowedZones && !hasRestrictedZones) { - throw new IllegalArgumentException("Geofencing calculated field must contain at least one of the following arguments: 'allowedZones' or 'restrictedZones'"); + private void validateZoneGroupConfigurations(Map zoneGroupsArguments) { + if (geofencingZoneGroupConfigurations == null) { + throw new IllegalArgumentException("Geofencing calculated field zone group configurations are empty!"); } + Set usedPrefixes = new HashSet<>(); + geofencingZoneGroupConfigurations.forEach((zoneGroupName, config) -> { + Argument zoneGroupArgument = zoneGroupsArguments.get(zoneGroupName); + if (zoneGroupArgument == null) { + throw new IllegalArgumentException("Geofencing calculated field zone group configuration is not configured for zone group: " + zoneGroupName); + } + if (config == null) { + throw new IllegalArgumentException("Zone group configuration is not configured for zone group: " + zoneGroupName); + } + if (CollectionsUtil.isEmpty(config.getReportEvents())) { + throw new IllegalArgumentException("Zone group configuration report events must be specified for zone group: " + zoneGroupName); + } + String prefix = config.getReportTelemetryPrefix(); + if (StringUtils.isBlank(prefix)) { + throw new IllegalArgumentException("Report telemetry prefix should be specified for zone group: " + zoneGroupName); + } + if (!usedPrefixes.add(prefix)) { + throw new IllegalArgumentException("Duplicate report telemetry prefix found: '" + prefix + "'. Must be unique!"); + } + }); + } - for (Map.Entry entry : arguments.entrySet()) { - String argumentKey = entry.getKey(); - Argument argument = entry.getValue(); + private void validateCoordinateArguments() { + for (String coordinateKey : coordinateKeys) { + Argument argument = arguments.get(coordinateKey); if (argument == null) { - throw new IllegalArgumentException("Missing required argument: " + argumentKey); + throw new IllegalArgumentException("Missing required coordinates argument: " + coordinateKey); } - ReferencedEntityKey refEntityKey = argument.getRefEntityKey(); - if (refEntityKey == null || refEntityKey.getType() == null) { - throw new IllegalArgumentException("Missing or invalid reference entity key for argument: " + argumentKey); + ReferencedEntityKey refEntityKey = validateAndGetRefEntityKey(argument, coordinateKey); + if (!ArgumentType.TS_LATEST.equals(refEntityKey.getType())) { + throw new IllegalArgumentException("Argument '" + coordinateKey + "' must be of type TS_LATEST."); } + if (argument.getRefDynamicSource() != null) { + throw new IllegalArgumentException("Dynamic source is not allowed for argument: '" + coordinateKey + "'."); + } + } + } - switch (argumentKey) { - case ENTITY_ID_LATITUDE_ARGUMENT_KEY, - ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> { - if (!ArgumentType.TS_LATEST.equals(refEntityKey.getType())) { - throw new IllegalArgumentException("Argument '" + argumentKey + "' must be of type TS_LATEST."); - } - if (argument.getRefDynamicSource() != null) { - throw new IllegalArgumentException("Dynamic source is not allowed for argument: '" + argumentKey + "'."); - } - } - case ALLOWED_ZONES_ARGUMENT_KEY, - RESTRICTED_ZONES_ARGUMENT_KEY -> { - if (!ArgumentType.ATTRIBUTE.equals(refEntityKey.getType())) { - throw new IllegalArgumentException("Argument '" + argumentKey + "' must be of type ATTRIBUTE."); - } - var dynamicSource = argument.getRefDynamicSource(); - if (dynamicSource == null) { - continue; - } - if (!RELATION_QUERY.equals(dynamicSource)) { - throw new IllegalArgumentException("Only relation query dynamic source is supported for argument: '" + argumentKey + "'."); - } - if (argument.getRefDynamicSourceConfiguration() == null) { - throw new IllegalArgumentException("Missing dynamic source configuration for argument: '" + argumentKey + "'."); - } - argument.getRefDynamicSourceConfiguration().validate(); - } + private void validateZoneGroupAruguments(Map zoneGroupsArguments) { + zoneGroupsArguments.forEach((argumentKey, argument) -> { + if (argument == null) { + throw new IllegalArgumentException("Zone group argument is not configured: " + argumentKey); + } + ReferencedEntityKey refEntityKey = validateAndGetRefEntityKey(argument, argumentKey); + if (!ArgumentType.ATTRIBUTE.equals(refEntityKey.getType())) { + throw new IllegalArgumentException("Argument '" + argumentKey + "' must be of type ATTRIBUTE."); } + var dynamicSource = argument.getRefDynamicSource(); + if (dynamicSource == null) { + return; + } + if (!RELATION_QUERY.equals(dynamicSource)) { + throw new IllegalArgumentException("Only relation query dynamic source is supported for argument: '" + argumentKey + "'."); + } + if (argument.getRefDynamicSourceConfiguration() == null) { + throw new IllegalArgumentException("Missing dynamic source configuration for argument: '" + argumentKey + "'."); + } + argument.getRefDynamicSourceConfiguration().validate(); + }); + } + + private Map getZoneGroupArguments() { + return arguments.entrySet() + .stream() + .filter(entry -> !coordinateKeys.contains(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private static ReferencedEntityKey validateAndGetRefEntityKey(Argument argument, String argumentKey) { + ReferencedEntityKey refEntityKey = argument.getRefEntityKey(); + if (refEntityKey == null || refEntityKey.getType() == null) { + throw new IllegalArgumentException("Missing or invalid reference entity key for argument: " + argumentKey); } + return refEntityKey; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java new file mode 100644 index 0000000000..9cb51ea294 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java @@ -0,0 +1,49 @@ +/** + * 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.common.data.cf.configuration; + +import lombok.Getter; + +import java.util.Arrays; + +@Getter +public enum GeofencingEvent { + + ENTERED(0), LEFT(1), INSIDE(2), OUTSIDE(3); + + private final int protoNumber; // Corresponds to GeofencingEvent + + GeofencingEvent(int protoNumber) { + this.protoNumber = protoNumber; + } + + private static final GeofencingEvent[] BY_PROTO; + + static { + BY_PROTO = new GeofencingEvent[Arrays.stream(values()).mapToInt(GeofencingEvent::getProtoNumber).max().orElse(0) + 1]; + for (var event : values()) { + BY_PROTO[event.getProtoNumber()] = event; + } + } + + public static GeofencingEvent fromProtoNumber(int protoNumber) { + if (protoNumber < 0 || protoNumber >= BY_PROTO.length) { + throw new IllegalArgumentException("Invalid GeofencingEvent proto number " + protoNumber); + } + return BY_PROTO[protoNumber]; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingZoneGroupConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingZoneGroupConfiguration.java new file mode 100644 index 0000000000..c82151fc64 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingZoneGroupConfiguration.java @@ -0,0 +1,28 @@ +/** + * 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.common.data.cf.configuration; + +import lombok.Data; + +import java.util.List; + +@Data +public class GeofencingZoneGroupConfiguration { + + private final String reportTelemetryPrefix; + private final List reportEvents; + +} diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 87040fcf02..90332be104 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -908,9 +908,18 @@ message GeofencingZoneProto { optional bool inside = 5; } +enum GeofencingEventProto { + ENTERED = 0; + LEFT = 1; + INSIDE = 2; + OUTSIDE = 3; +} + message GeofencingArgumentProto { - string argName = 1; // e.g., "restrictedZones" or "allowedZones" - repeated GeofencingZoneProto zones = 2; + string argName = 1; + string telemetryPrefix = 2; + repeated GeofencingEventProto reportEvents = 3; + repeated GeofencingZoneProto zones = 4; } message CalculatedFieldStateProto { From 589e159b548d0f9dfe740f48778deaef3d2f1b84 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Thu, 7 Aug 2025 18:33:17 +0300 Subject: [PATCH 029/216] Added ability to filter out reporting geofencing events statuses --- .../state/GeofencingCalculatedFieldState.java | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java index 6d8a08914d..f66c4cf9c3 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java @@ -126,13 +126,37 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { .stream() .map(zoneState -> zoneState.evaluate(entityCoordinates)) .collect(Collectors.toSet()); - aggregateZoneGroupEvent(zoneEvents).ifPresent(event -> - resultNode.put(zoneGroupConfig.getReportTelemetryPrefix() + "Event", event.name()) - ); + aggregateZoneGroupEvent(zoneEvents) + .filter(geofencingEvent -> zoneGroupConfig.getReportEvents().contains(geofencingEvent)) + .ifPresent(event -> + resultNode.put(zoneGroupConfig.getReportTelemetryPrefix() + "Event", event.name()) + ); }); return Futures.immediateFuture(List.of(new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode))); } + @Override + public boolean isReady() { + return arguments.keySet().containsAll(requiredArguments) && + arguments.values().stream().noneMatch(ArgumentEntry::isEmpty); + } + + @Override + public void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize) { + if (!sizeExceedsLimit && maxStateSize > 0 && CalculatedFieldUtils.toProto(ctxId, this).getSerializedSize() > maxStateSize) { + arguments.clear(); + sizeExceedsLimit = true; + } + } + + // TODO: Create a new class field to not do this on each calculation. + private Map getGeofencingArguments() { + return arguments.entrySet() + .stream() + .filter(entry -> !coordinateKeys.contains(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, entry -> (GeofencingArgumentEntry) entry.getValue())); + } + private Optional aggregateZoneGroupEvent(Set zoneEvents) { boolean hasEntered = false; boolean hasLeft = false; @@ -166,26 +190,4 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { return Optional.empty(); } - @Override - public boolean isReady() { - return arguments.keySet().containsAll(requiredArguments) && - arguments.values().stream().noneMatch(ArgumentEntry::isEmpty); - } - - @Override - public void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize) { - if (!sizeExceedsLimit && maxStateSize > 0 && CalculatedFieldUtils.toProto(ctxId, this).getSerializedSize() > maxStateSize) { - arguments.clear(); - sizeExceedsLimit = true; - } - } - - // TODO: Create a new class field to not do this on each calculation. - private Map getGeofencingArguments() { - return arguments.entrySet() - .stream() - .filter(entry -> !coordinateKeys.contains(entry.getKey())) - .collect(Collectors.toMap(Map.Entry::getKey, entry -> (GeofencingArgumentEntry) entry.getValue())); - } - } From 71f092c4e8dbcf379c0135064abba8ddeffceb05 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Fri, 8 Aug 2025 15:14:03 +0300 Subject: [PATCH 030/216] Added dirty updates support --- .../CalculatedFieldEntityActor.java | 4 +- ...CalculatedFieldEntityMessageProcessor.java | 40 +++++------ .../CalculatedFieldManagerActor.java | 4 +- ...alculatedFieldManagerMessageProcessor.java | 52 +++++++------- ...culatedFieldScheduledInvalidationMsg.java} | 4 +- ...tityCalculatedFieldMarkStateDirtyMsg.java} | 8 +-- .../cf/CalculatedFieldProcessingService.java | 2 + ...faultCalculatedFieldProcessingService.java | 68 ++++++++++++------- .../ctx/state/BaseCalculatedFieldState.java | 4 +- .../cf/ctx/state/CalculatedFieldState.java | 4 ++ .../state/GeofencingCalculatedFieldState.java | 7 +- .../server/common/msg/MsgType.java | 4 +- 12 files changed, 111 insertions(+), 90 deletions(-) rename application/src/main/java/org/thingsboard/server/actors/calculatedField/{CalculatedFieldScheduledCheckForUpdatesMsg.java => CalculatedFieldScheduledInvalidationMsg.java} (87%) rename application/src/main/java/org/thingsboard/server/actors/calculatedField/{EntityCalculatedFieldCheckForUpdatesMsg.java => EntityCalculatedFieldMarkStateDirtyMsg.java} (80%) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java index 4812ed6652..4879fa4566 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java @@ -75,8 +75,8 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor { case CF_LINKED_TELEMETRY_MSG: processor.process((EntityCalculatedFieldLinkedTelemetryMsg) msg); break; - case CF_ENTITY_CHECK_FOR_UPDATES_MSG: - processor.process((EntityCalculatedFieldCheckForUpdatesMsg) msg); + case CF_ENTITY_MARK_STATE_DIRTY_MSG: + processor.process((EntityCalculatedFieldMarkStateDirtyMsg) msg); break; default: return false; diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 95b705adfc..6d832a9f23 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -228,29 +228,16 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } - public void process(EntityCalculatedFieldCheckForUpdatesMsg msg) throws CalculatedFieldException { - CalculatedFieldCtx cfCtx = msg.getCfCtx(); - CalculatedFieldId cfId = cfCtx.getCfId(); - log.debug("[{}][{}] Processing CF check for updates msg.", entityId, cfId); - CalculatedFieldState currentState = states.get(cfId); - try { - var stateFromDb = getStateFromDb(cfCtx); - if (currentState.equals(stateFromDb)) { - log.debug("[{}][{}] CF state is up-to-date.", entityId, cfId); - return; - } - states.put(cfId, stateFromDb); - if (stateFromDb.isSizeOk()) { - processStateIfReady(cfCtx, Collections.singletonList(cfId), stateFromDb, null, null, msg.getCallback()); - } else { - throw new RuntimeException(cfCtx.getSizeExceedsLimitMessage()); - } - } catch (Exception e) { - if (e instanceof CalculatedFieldException cfe) { - throw cfe; - } - throw CalculatedFieldException.builder().ctx(cfCtx).eventEntity(entityId).cause(e).build(); + public void process(EntityCalculatedFieldMarkStateDirtyMsg msg) throws CalculatedFieldException { + log.debug("[{}][{}] Processing entity CF invalidation msg.", entityId, msg.getCfId()); + CalculatedFieldState currentState = states.get(msg.getCfId()); + if (currentState == null) { + log.debug("[{}][{}] Failed to find CF state for entity.", entityId, msg.getCfId()); + } else { + currentState.setDirty(true); + log.debug("[{}][{}] CF state marked as dirty.", entityId, msg.getCfId()); } + msg.getCallback().onSuccess(); } private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { @@ -280,6 +267,15 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM if (state == null) { state = getOrInitState(ctx); justRestored = true; + } else if (state.isDirty()) { + log.debug("[{}][{}] Going to update dirty CF state.", entityId, ctx.getCfId()); + try { + Map dynamicArgsFromDb = cfService.fetchDynamicArgsFromDb(ctx, entityId); + dynamicArgsFromDb.forEach(newArgValues::putIfAbsent); + state.setDirty(false); + } catch (Exception e) { + throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); + } } if (state.isSizeOk()) { if (state.updateState(ctx, newArgValues) || justRestored) { diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java index 5adca78fa9..8494fb3847 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java @@ -91,8 +91,8 @@ public class CalculatedFieldManagerActor extends AbstractCalculatedFieldActor { case CF_LINKED_TELEMETRY_MSG: processor.onLinkedTelemetryMsg((CalculatedFieldLinkedTelemetryMsg) msg); break; - case CF_SCHEDULED_CHECK_FOR_UPDATES_MSG: - processor.onScheduledCheckForUpdatesMsg((CalculatedFieldScheduledCheckForUpdatesMsg) msg); + case CF_SCHEDULED_INVALIDATION_MSG: + processor.onScheduledInvalidationMsg((CalculatedFieldScheduledInvalidationMsg) msg); break; default: return false; diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index de6c38b9b9..91122f3f95 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -76,7 +76,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private final Map calculatedFields = new HashMap<>(); private final Map> entityIdCalculatedFields = new HashMap<>(); private final Map> entityIdCalculatedFieldLinks = new HashMap<>(); - private final Map> checkForCalculatedFieldUpdateTasks = new ConcurrentHashMap<>(); + private final Map> cfInvalidationScheduledTasks = new ConcurrentHashMap<>(); private final CalculatedFieldProcessingService cfExecService; private final CalculatedFieldStateService cfStateService; @@ -115,8 +115,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware calculatedFields.clear(); entityIdCalculatedFields.clear(); entityIdCalculatedFieldLinks.clear(); - checkForCalculatedFieldUpdateTasks.values().forEach(future -> future.cancel(true)); - checkForCalculatedFieldUpdateTasks.clear(); + cfInvalidationScheduledTasks.values().forEach(future -> future.cancel(true)); + cfInvalidationScheduledTasks.clear(); ctx.stop(ctx.getSelf()); } @@ -147,7 +147,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware // We use copy on write lists to safely pass the reference to another actor for the iteration. // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx); - scheduleCalculatedFieldUpdateMsgIfNeeded(cfCtx); + scheduleCalculatedFieldInvalidationMsgIfNeeded(cfCtx); msg.getCallback().onSuccess(); } @@ -336,7 +336,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware boolean hasSchedulingConfigChanges = newCfCtx.hasSchedulingConfigChanges(oldCfCtx); if (hasSchedulingConfigChanges) { - cancelCfUpdateTaskIfExists(cfId, false); + cancelCfScheduledInvalidationTaskIfExists(cfId, false); } List newCfList = new CopyOnWriteArrayList<>(); @@ -379,7 +379,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx); deleteLinks(cfCtx); - cancelCfUpdateTaskIfExists(cfId, true); + cancelCfScheduledInvalidationTaskIfExists(cfId, true); EntityId entityId = cfCtx.getEntityId(); EntityType entityType = cfCtx.getEntityId().getEntityType(); @@ -404,12 +404,12 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } } - private void cancelCfUpdateTaskIfExists(CalculatedFieldId cfId, boolean cfDeleted) { - var existingTask = checkForCalculatedFieldUpdateTasks.remove(cfId); + private void cancelCfScheduledInvalidationTaskIfExists(CalculatedFieldId cfId, boolean cfDeleted) { + var existingTask = cfInvalidationScheduledTasks.remove(cfId); if (existingTask != null) { existingTask.cancel(false); - String reason = cfDeleted ? "removal" : "update"; - log.debug("[{}][{}] Cancelled check for update task due to CF " + reason + "!", tenantId, cfId); + String reason = cfDeleted ? "deletion" : "update"; + log.debug("[{}][{}] Cancelled scheduled invalidation task due to CF " + reason + "!", tenantId, cfId); } } @@ -515,7 +515,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private void initCf(CalculatedFieldCtx cfCtx, TbCallback callback, boolean forceStateReinit) { EntityId entityId = cfCtx.getEntityId(); EntityType entityType = cfCtx.getEntityId().getEntityType(); - scheduleCalculatedFieldUpdateMsgIfNeeded(cfCtx); + scheduleCalculatedFieldInvalidationMsgIfNeeded(cfCtx); if (isProfileEntity(entityType)) { var entityIds = entityProfileCache.getEntityIdsByProfileId(entityId); if (!entityIds.isEmpty()) { @@ -533,31 +533,31 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } } - private void scheduleCalculatedFieldUpdateMsgIfNeeded(CalculatedFieldCtx cfCtx) { + private void scheduleCalculatedFieldInvalidationMsgIfNeeded(CalculatedFieldCtx cfCtx) { CalculatedField cf = cfCtx.getCalculatedField(); CalculatedFieldConfiguration cfConfig = cf.getConfiguration(); if (!cfConfig.isScheduledUpdateEnabled()) { return; } - if (checkForCalculatedFieldUpdateTasks.containsKey(cf.getId())) { - log.debug("[{}][{}] Check for update msg for CF is already scheduled!", tenantId, cf.getId()); + if (cfInvalidationScheduledTasks.containsKey(cf.getId())) { + log.debug("[{}][{}] Scheduled invalidation task for CF already exists!", tenantId, cf.getId()); return; } long refreshDynamicSourceInterval = TimeUnit.SECONDS.toMillis(cfConfig.getScheduledUpdateIntervalSec()); - var scheduledMsg = new CalculatedFieldScheduledCheckForUpdatesMsg(tenantId, cfCtx.getCfId()); + var scheduledMsg = new CalculatedFieldScheduledInvalidationMsg(tenantId, cfCtx.getCfId()); ScheduledFuture scheduledFuture = systemContext .schedulePeriodicMsgWithDelay(ctx, scheduledMsg, refreshDynamicSourceInterval, refreshDynamicSourceInterval); - checkForCalculatedFieldUpdateTasks.put(cf.getId(), scheduledFuture); - log.debug("[{}][{}] Scheduled check for update msg for CF!", tenantId, cf.getId()); + cfInvalidationScheduledTasks.put(cf.getId(), scheduledFuture); + log.debug("[{}][{}] Scheduled invalidation task for CF!", tenantId, cf.getId()); } - public void onScheduledCheckForUpdatesMsg(CalculatedFieldScheduledCheckForUpdatesMsg msg) { - log.debug("[{}] [{}] Processing CF scheduled update msg.", tenantId, msg.getCfId()); + public void onScheduledInvalidationMsg(CalculatedFieldScheduledInvalidationMsg msg) { + log.debug("[{}] [{}] Processing CF scheduled invalidation msg.", tenantId, msg.getCfId()); CalculatedFieldCtx cfCtx = calculatedFields.get(msg.getCfId()); if (cfCtx == null) { - log.debug("[{}][{}] Failed to find CF context, going to stop scheduler updates.", tenantId, msg.getCfId()); - cancelCfUpdateTaskIfExists(msg.getCfId(), true); + log.debug("[{}][{}] Failed to find CF context, going to stop scheduled invalidations for CF.", tenantId, msg.getCfId()); + cancelCfScheduledInvalidationTaskIfExists(msg.getCfId(), true); return; } EntityId entityId = cfCtx.getEntityId(); @@ -568,7 +568,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware var multiCallback = new MultipleTbCallback(entityIds.size(), msg.getCallback()); entityIds.forEach(id -> { if (isMyPartition(id, multiCallback)) { - updateCfWithDynamicSourceForEntity(id, cfCtx, multiCallback); + InitCfInvalidationForEntity(id, msg.getCfId(), multiCallback); } }); } else { @@ -576,14 +576,14 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } } else { if (isMyPartition(entityId, msg.getCallback())) { - updateCfWithDynamicSourceForEntity(entityId, cfCtx, msg.getCallback()); + InitCfInvalidationForEntity(entityId, msg.getCfId(), msg.getCallback()); } } } - private void updateCfWithDynamicSourceForEntity(EntityId entityId, CalculatedFieldCtx cfCtx, TbCallback callback) { - log.debug("Pushing entity dynamic source refresh CF msg to specific actor [{}]", entityId); - getOrCreateActor(entityId).tell(new EntityCalculatedFieldCheckForUpdatesMsg(tenantId, cfCtx, callback)); + private void InitCfInvalidationForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) { + log.debug("Pushing entity CF invalidation msg to specific actor [{}]", entityId); + getOrCreateActor(entityId).tell(new EntityCalculatedFieldMarkStateDirtyMsg(tenantId, cfId, callback)); } private void deleteCfForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) { diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldScheduledCheckForUpdatesMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldScheduledInvalidationMsg.java similarity index 87% rename from application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldScheduledCheckForUpdatesMsg.java rename to application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldScheduledInvalidationMsg.java index 95d6b6759a..08a2394119 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldScheduledCheckForUpdatesMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldScheduledInvalidationMsg.java @@ -22,14 +22,14 @@ import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; @Data -public class CalculatedFieldScheduledCheckForUpdatesMsg implements ToCalculatedFieldSystemMsg { +public class CalculatedFieldScheduledInvalidationMsg implements ToCalculatedFieldSystemMsg { private final TenantId tenantId; private final CalculatedFieldId cfId; @Override public MsgType getMsgType() { - return MsgType.CF_SCHEDULED_CHECK_FOR_UPDATES_MSG; + return MsgType.CF_SCHEDULED_INVALIDATION_MSG; } } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldCheckForUpdatesMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldMarkStateDirtyMsg.java similarity index 80% rename from application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldCheckForUpdatesMsg.java rename to application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldMarkStateDirtyMsg.java index 908680c068..ef9864aa83 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldCheckForUpdatesMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldMarkStateDirtyMsg.java @@ -16,22 +16,22 @@ package org.thingsboard.server.actors.calculatedField; import lombok.Data; +import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.queue.TbCallback; -import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; @Data -public class EntityCalculatedFieldCheckForUpdatesMsg implements ToCalculatedFieldSystemMsg { +public class EntityCalculatedFieldMarkStateDirtyMsg implements ToCalculatedFieldSystemMsg { private final TenantId tenantId; - private final CalculatedFieldCtx cfCtx; + private final CalculatedFieldId cfId; private final TbCallback callback; @Override public MsgType getMsgType() { - return MsgType.CF_ENTITY_CHECK_FOR_UPDATES_MSG; + return MsgType.CF_ENTITY_MARK_STATE_DIRTY_MSG; } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java index 847caccaff..86ed174485 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldProcessingService.java @@ -34,6 +34,8 @@ public interface CalculatedFieldProcessingService { ListenableFuture fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId); + Map fetchDynamicArgsFromDb(CalculatedFieldCtx ctx, EntityId entityId); + Map fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map arguments); void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult calculationResult, List cfIds, TbCallback callback); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index 29c4809a75..dd4049241c 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.CFArgumentDynamicSourceType; import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; import org.thingsboard.server.common.data.cf.configuration.OutputType; @@ -90,6 +91,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; @@ -132,20 +134,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP Map> argFutures = new HashMap<>(); if (ctx.getCalculatedField().getType().equals(CalculatedFieldType.GEOFENCING)) { - var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); - var zoneGroupConfigs = configuration.getGeofencingZoneGroupConfigurations(); - for (var entry : ctx.getArguments().entrySet()) { - switch (entry.getKey()) { - case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> - argFutures.put(entry.getKey(), fetchKvEntry(ctx.getTenantId(), resolveEntityId(entityId, entry), entry.getValue())); - default -> { - var zoneGroupConfiguration = zoneGroupConfigs.get(entry.getKey()); - var resolvedEntityIdsFuture = resolveGeofencingEntityIds(ctx.getTenantId(), entityId, entry); - argFutures.put(entry.getKey(), Futures.transformAsync(resolvedEntityIdsFuture, resolvedEntityIds -> - fetchGeofencingKvEntry(ctx.getTenantId(), resolvedEntityIds, entry.getValue(), zoneGroupConfiguration), calculatedFieldCallbackExecutor)); - } - } - } + fetchGeofencingCalculatedFieldArguments(ctx, entityId, argFutures, false); } else { for (var entry : ctx.getArguments().entrySet()) { var argEntityId = resolveEntityId(entityId, entry); @@ -156,22 +145,45 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP return Futures.whenAllComplete(argFutures.values()).call(() -> { var result = createStateByType(ctx); - result.updateState(ctx, argFutures.entrySet().stream() - .collect(Collectors.toMap( - Entry::getKey, // Keep the key as is - entry -> { - try { - // Resolve the future to get the value - return entry.getValue().get(); - } catch (ExecutionException | InterruptedException e) { - throw new RuntimeException("Error getting future result for key: " + entry.getKey(), e); - } - } - ))); + result.updateState(ctx, resolveArgumentFutures(argFutures)); return result; }, calculatedFieldCallbackExecutor); } + @Override + public Map fetchDynamicArgsFromDb(CalculatedFieldCtx ctx, EntityId entityId) { + // only geofencing calculated fields supports dynamic arguments scheduled updates + if (!ctx.getCalculatedField().getType().equals(CalculatedFieldType.GEOFENCING)) { + return Map.of(); + } + Map> argFutures = new HashMap<>(); + fetchGeofencingCalculatedFieldArguments(ctx, entityId, argFutures, true); + return resolveArgumentFutures(argFutures); + } + + private void fetchGeofencingCalculatedFieldArguments(CalculatedFieldCtx ctx, EntityId entityId, Map> argFutures, boolean dynamicArgumentsOnly) { + var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); + var zoneGroupConfigs = configuration.getGeofencingZoneGroupConfigurations(); + Set> entries = ctx.getArguments().entrySet(); + if (dynamicArgumentsOnly) { + entries = entries.stream() + .filter(entry -> CFArgumentDynamicSourceType.RELATION_QUERY.equals(entry.getValue().getRefDynamicSource())) + .collect(Collectors.toSet()); + } + for (var entry : entries) { + switch (entry.getKey()) { + case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> + argFutures.put(entry.getKey(), fetchKvEntry(ctx.getTenantId(), resolveEntityId(entityId, entry), entry.getValue())); + default -> { + var zoneGroupConfiguration = zoneGroupConfigs.get(entry.getKey()); + var resolvedEntityIdsFuture = resolveGeofencingEntityIds(ctx.getTenantId(), entityId, entry); + argFutures.put(entry.getKey(), Futures.transformAsync(resolvedEntityIdsFuture, resolvedEntityIds -> + fetchGeofencingKvEntry(ctx.getTenantId(), resolvedEntityIds, entry.getValue(), zoneGroupConfiguration), calculatedFieldCallbackExecutor)); + } + } + } + } + @Override public Map fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map arguments) { Map> argFutures = new HashMap<>(); @@ -180,6 +192,10 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP var argValueFuture = fetchKvEntry(tenantId, argEntityId, entry.getValue()); argFutures.put(entry.getKey(), argValueFuture); } + return resolveArgumentFutures(argFutures); + } + + private Map resolveArgumentFutures(Map> argFutures) { return argFutures.entrySet().stream() .collect(Collectors.toMap( Entry::getKey, // Keep the key as is diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index eb87d375c5..fa7e628ab3 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -35,13 +35,15 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { protected long latestTimestamp = -1; + private boolean dirty; + public BaseCalculatedFieldState(List requiredArguments) { this.requiredArguments = requiredArguments; this.arguments = new HashMap<>(); } public BaseCalculatedFieldState() { - this(new ArrayList<>(), new HashMap<>(), false, -1); + this(new ArrayList<>(), new HashMap<>(), false, -1, false); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java index 77e630baaa..bb7515d7f5 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java @@ -47,6 +47,10 @@ public interface CalculatedFieldState { long getLatestTimestamp(); + void setDirty(boolean dirty); + + boolean isDirty(); + void setRequiredArguments(List requiredArguments); boolean updateState(CalculatedFieldCtx ctx, Map argumentValues); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java index f66c4cf9c3..a98d6b65b1 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java @@ -46,13 +46,14 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { private List requiredArguments; private Map arguments; - - protected boolean sizeExceedsLimit; + private boolean sizeExceedsLimit; private long latestTimestamp = -1; + private boolean dirty; + public GeofencingCalculatedFieldState() { - this(new ArrayList<>(), new HashMap<>(), false, -1); + this(new ArrayList<>(), new HashMap<>(), false, -1, false); } public GeofencingCalculatedFieldState(List argNames) { diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java index bfcd3f3071..fc0e2262bb 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java @@ -152,8 +152,8 @@ public enum MsgType { CF_ENTITY_INIT_CF_MSG, CF_ENTITY_DELETE_MSG, - CF_SCHEDULED_CHECK_FOR_UPDATES_MSG, - CF_ENTITY_CHECK_FOR_UPDATES_MSG; + CF_SCHEDULED_INVALIDATION_MSG, + CF_ENTITY_MARK_STATE_DIRTY_MSG; @Getter private final boolean ignoreOnStart; From fb84eb06959781c73335f83d29d0fb7058d3fc73 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Fri, 8 Aug 2025 17:02:29 +0300 Subject: [PATCH 031/216] typos fix --- .../CalculatedFieldManagerMessageProcessor.java | 1 - .../GeofencingCalculatedFieldConfiguration.java | 2 +- .../tenant/profile/DefaultTenantProfileConfiguration.java | 8 ++++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index 91122f3f95..5af4e08785 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -124,7 +124,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware log.debug("[{}] Processing CF actor init message.", msg.getTenantId().getId()); initEntityProfileCache(); initCalculatedFields(); - // TODO: implement cache for 1:1 relations to use in the CFs that based on a relation queries? msg.getCallback().onSuccess(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java index 78cb48c2ee..38d4bf6ca2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java @@ -40,7 +40,7 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC ENTITY_ID_LONGITUDE_ARGUMENT_KEY ); - Map geofencingZoneGroupConfigurations; + private Map geofencingZoneGroupConfigurations; @Override public CalculatedFieldType getType() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index 9da0e27dc6..f35fca23f9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -172,10 +172,10 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long maxCalculatedFieldsPerEntity = 5; @Schema(example = "10") private long maxArgumentsPerCF = 10; - @Schema(example = "300") - private int minAllowedScheduledUpdateIntervalInSecForCF = 60; - @Schema(example = "300") - private int maxAllowedScheduledUpdateIntervalInSecForCF = 3600; + @Schema(example = "3600") + private int minAllowedScheduledUpdateIntervalInSecForCF = 3600; + @Schema(example = "86400") + private int maxAllowedScheduledUpdateIntervalInSecForCF = 86400; @Builder.Default @Min(value = 1, message = "must be at least 1") @Schema(example = "1000") From 409328dbe3f3d0d9b917a7c039a7d8d5ad3a4df7 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Fri, 8 Aug 2025 17:28:57 +0300 Subject: [PATCH 032/216] removed no needed logic from geofencing arugment --- ...faultCalculatedFieldProcessingService.java | 46 ++++++++----------- .../service/cf/ctx/state/ArgumentEntry.java | 5 +- .../cf/ctx/state/GeofencingArgumentEntry.java | 6 +-- .../state/GeofencingCalculatedFieldState.java | 7 ++- .../server/utils/CalculatedFieldUtils.java | 16 +------ ...eofencingCalculatedFieldConfiguration.java | 2 + .../cf/configuration/GeofencingEvent.java | 29 +----------- common/proto/src/main/proto/queue.proto | 9 ---- 8 files changed, 33 insertions(+), 87 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index dd4049241c..ade1d3eefd 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -36,8 +36,6 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CFArgumentDynamicSourceType; -import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; @@ -131,18 +129,18 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP @Override public ListenableFuture fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId) { - Map> argFutures = new HashMap<>(); - - if (ctx.getCalculatedField().getType().equals(CalculatedFieldType.GEOFENCING)) { - fetchGeofencingCalculatedFieldArguments(ctx, entityId, argFutures, false); - } else { - for (var entry : ctx.getArguments().entrySet()) { - var argEntityId = resolveEntityId(entityId, entry); - var argValueFuture = fetchKvEntry(ctx.getTenantId(), argEntityId, entry.getValue()); - argFutures.put(entry.getKey(), argValueFuture); + Map> argFutures = switch (ctx.getCalculatedField().getType()) { + case GEOFENCING -> fetchGeofencingCalculatedFieldArguments(ctx, entityId, false); + case SIMPLE, SCRIPT -> { + Map> futures = new HashMap<>(); + for (var entry : ctx.getArguments().entrySet()) { + var argEntityId = resolveEntityId(entityId, entry); + var argValueFuture = fetchKvEntry(ctx.getTenantId(), argEntityId, entry.getValue()); + futures.put(entry.getKey(), argValueFuture); + } + yield futures; } - } - + }; return Futures.whenAllComplete(argFutures.values()).call(() -> { var result = createStateByType(ctx); result.updateState(ctx, resolveArgumentFutures(argFutures)); @@ -156,14 +154,11 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP if (!ctx.getCalculatedField().getType().equals(CalculatedFieldType.GEOFENCING)) { return Map.of(); } - Map> argFutures = new HashMap<>(); - fetchGeofencingCalculatedFieldArguments(ctx, entityId, argFutures, true); - return resolveArgumentFutures(argFutures); + return resolveArgumentFutures(fetchGeofencingCalculatedFieldArguments(ctx, entityId, true)); } - private void fetchGeofencingCalculatedFieldArguments(CalculatedFieldCtx ctx, EntityId entityId, Map> argFutures, boolean dynamicArgumentsOnly) { - var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); - var zoneGroupConfigs = configuration.getGeofencingZoneGroupConfigurations(); + private Map> fetchGeofencingCalculatedFieldArguments(CalculatedFieldCtx ctx, EntityId entityId, boolean dynamicArgumentsOnly) { + Map> argFutures = new HashMap<>(); Set> entries = ctx.getArguments().entrySet(); if (dynamicArgumentsOnly) { entries = entries.stream() @@ -175,13 +170,13 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> argFutures.put(entry.getKey(), fetchKvEntry(ctx.getTenantId(), resolveEntityId(entityId, entry), entry.getValue())); default -> { - var zoneGroupConfiguration = zoneGroupConfigs.get(entry.getKey()); var resolvedEntityIdsFuture = resolveGeofencingEntityIds(ctx.getTenantId(), entityId, entry); argFutures.put(entry.getKey(), Futures.transformAsync(resolvedEntityIdsFuture, resolvedEntityIds -> - fetchGeofencingKvEntry(ctx.getTenantId(), resolvedEntityIds, entry.getValue(), zoneGroupConfiguration), calculatedFieldCallbackExecutor)); + fetchGeofencingKvEntry(ctx.getTenantId(), resolvedEntityIds, entry.getValue()), calculatedFieldCallbackExecutor)); } } } + return argFutures; } @Override @@ -321,12 +316,11 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP }; } - private ListenableFuture fetchGeofencingKvEntry(TenantId tenantId, List geofencingEntities, - Argument argument, GeofencingZoneGroupConfiguration zoneGroupConfiguration) { + private ListenableFuture fetchGeofencingKvEntry(TenantId tenantId, List geofencingEntities, Argument argument) { if (argument.getRefEntityKey().getType() != ArgumentType.ATTRIBUTE) { throw new IllegalStateException("Unsupported argument key type: " + argument.getRefEntityKey().getType()); } - List>> kvFutures = geofencingEntities.stream() + List>> kvFutures = geofencingEntities.stream() .map(entityId -> { var attributesFuture = attributesService.find( tenantId, @@ -341,10 +335,10 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP ); }).collect(Collectors.toList()); - ListenableFuture>> allFutures = Futures.allAsList(kvFutures); + ListenableFuture>> allFutures = Futures.allAsList(kvFutures); return Futures.transform(allFutures, entries -> ArgumentEntry.createGeofencingValueArgument(entries.stream() - .collect(Collectors.toMap(Entry::getKey, Entry::getValue)), zoneGroupConfiguration), + .collect(Collectors.toMap(Entry::getKey, Entry::getValue))), calculatedFieldCallbackExecutor ); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java index f76c6855a6..c7f830431b 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.thingsboard.script.api.tbel.TbelCfArg; -import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; @@ -62,8 +61,8 @@ public interface ArgumentEntry { return new TsRollingArgumentEntry(kvEntries, limit, timeWindow); } - static ArgumentEntry createGeofencingValueArgument(Map entityIdkvEntryMap, GeofencingZoneGroupConfiguration zoneGroupConfiguration) { - return new GeofencingArgumentEntry(entityIdkvEntryMap, zoneGroupConfiguration); + static ArgumentEntry createGeofencingValueArgument(Map entityIdkvEntryMap) { + return new GeofencingArgumentEntry(entityIdkvEntryMap); } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java index 2acdf0be4c..4b88419fbb 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java @@ -19,7 +19,6 @@ import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfTsGeofencingArg; -import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.KvEntry; @@ -32,17 +31,14 @@ import java.util.stream.Collectors; public class GeofencingArgumentEntry implements ArgumentEntry { private Map zoneStates; - private GeofencingZoneGroupConfiguration zoneGroupConfiguration; private boolean forceResetPrevious; public GeofencingArgumentEntry() { } - public GeofencingArgumentEntry(Map entityIdkvEntryMap, - GeofencingZoneGroupConfiguration zoneGroupConfiguration) { + public GeofencingArgumentEntry(Map entityIdkvEntryMap) { this.zoneStates = toZones(entityIdkvEntryMap); - this.zoneGroupConfiguration = zoneGroupConfiguration; } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java index a98d6b65b1..8a209dd138 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java @@ -23,7 +23,9 @@ import lombok.Data; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.geo.Coordinates; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; +import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.utils.CalculatedFieldUtils; @@ -119,9 +121,12 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { double longitude = (double) arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY).getValue(); Coordinates entityCoordinates = new Coordinates(latitude, longitude); + var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); + Map geofencingZoneGroupConfigurations = configuration.getGeofencingZoneGroupConfigurations(); + ObjectNode resultNode = JacksonUtil.newObjectNode(); getGeofencingArguments().forEach((argumentKey, argumentEntry) -> { - var zoneGroupConfig = argumentEntry.getZoneGroupConfiguration(); + var zoneGroupConfig = geofencingZoneGroupConfigurations.get(argumentKey); Set zoneEvents = argumentEntry.getZoneStates() .values() .stream() diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java index aaa68e1dd9..4876fa8feb 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java @@ -18,8 +18,6 @@ package org.thingsboard.server.utils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.CalculatedFieldType; -import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; -import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -30,7 +28,6 @@ import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldEntit import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldIdProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; import org.thingsboard.server.gen.transport.TransportProtos.GeofencingArgumentProto; -import org.thingsboard.server.gen.transport.TransportProtos.GeofencingEventProto; import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneIdProto; import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneProto; import org.thingsboard.server.gen.transport.TransportProtos.SingleValueArgumentProto; @@ -48,7 +45,6 @@ import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.TreeMap; @@ -127,15 +123,11 @@ public class CalculatedFieldUtils { private static GeofencingArgumentProto toGeofencingArgumentProto(String argName, GeofencingArgumentEntry geofencingArgumentEntry) { - var zoneGroupConfiguration = geofencingArgumentEntry.getZoneGroupConfiguration(); Map zoneStates = geofencingArgumentEntry.getZoneStates(); GeofencingArgumentProto.Builder builder = GeofencingArgumentProto.newBuilder() - .setArgName(argName) - .setTelemetryPrefix(zoneGroupConfiguration.getReportTelemetryPrefix()); + .setArgName(argName); zoneStates.forEach((entityId, zoneState) -> builder.addZones(toGeofencingZoneProto(entityId, zoneState))); - zoneGroupConfiguration.getReportEvents().forEach(event -> - builder.addReportEvents(GeofencingEventProto.forNumber(event.getProtoNumber()))); return builder.build(); } @@ -213,14 +205,8 @@ public class CalculatedFieldUtils { .stream() .map(GeofencingZoneState::new) .collect(Collectors.toMap(GeofencingZoneState::getZoneId, Function.identity())); - List geofencingEvents = proto.getReportEventsList() - .stream() - .map(geofencingEventProto -> GeofencingEvent.fromProtoNumber(geofencingEventProto.getNumber())) - .toList(); - var zoneGroupConfiguration = new GeofencingZoneGroupConfiguration(proto.getTelemetryPrefix(), geofencingEvents); GeofencingArgumentEntry geofencingArgumentEntry = new GeofencingArgumentEntry(); geofencingArgumentEntry.setZoneStates(zoneStates); - geofencingArgumentEntry.setZoneGroupConfiguration(zoneGroupConfiguration); return geofencingArgumentEntry; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java index 38d4bf6ca2..64fdc05144 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java @@ -40,6 +40,8 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC ENTITY_ID_LONGITUDE_ARGUMENT_KEY ); + private String zoneRelationType; + private boolean trackZoneRelations; private Map geofencingZoneGroupConfigurations; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java index 9cb51ea294..e812918df7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java @@ -15,35 +15,8 @@ */ package org.thingsboard.server.common.data.cf.configuration; -import lombok.Getter; - -import java.util.Arrays; - -@Getter public enum GeofencingEvent { - ENTERED(0), LEFT(1), INSIDE(2), OUTSIDE(3); - - private final int protoNumber; // Corresponds to GeofencingEvent - - GeofencingEvent(int protoNumber) { - this.protoNumber = protoNumber; - } - - private static final GeofencingEvent[] BY_PROTO; - - static { - BY_PROTO = new GeofencingEvent[Arrays.stream(values()).mapToInt(GeofencingEvent::getProtoNumber).max().orElse(0) + 1]; - for (var event : values()) { - BY_PROTO[event.getProtoNumber()] = event; - } - } - - public static GeofencingEvent fromProtoNumber(int protoNumber) { - if (protoNumber < 0 || protoNumber >= BY_PROTO.length) { - throw new IllegalArgumentException("Invalid GeofencingEvent proto number " + protoNumber); - } - return BY_PROTO[protoNumber]; - } + ENTERED, LEFT, INSIDE, OUTSIDE; } diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 90332be104..10ffb10793 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -908,17 +908,8 @@ message GeofencingZoneProto { optional bool inside = 5; } -enum GeofencingEventProto { - ENTERED = 0; - LEFT = 1; - INSIDE = 2; - OUTSIDE = 3; -} - message GeofencingArgumentProto { string argName = 1; - string telemetryPrefix = 2; - repeated GeofencingEventProto reportEvents = 3; repeated GeofencingZoneProto zones = 4; } From 8e1cde1a65ec203d33cd55aede8aac847277c47b Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Fri, 8 Aug 2025 18:27:42 +0300 Subject: [PATCH 033/216] UI: Imp create new for entity autocomplete --- .../home/components/ai-model/ai-model-dialog.component.ts | 5 +++++ .../components/rule-node/external/ai-config.component.html | 2 +- .../components/rule-node/external/ai-config.component.ts | 5 +++-- .../mobile/applications/mobile-app-dialog.component.ts | 4 ++++ .../pages/mobile/bundes/mobile-bundle-dialog.component.html | 4 ++-- .../pages/mobile/bundes/mobile-bundle-dialog.component.ts | 5 +++-- .../components/entity/entity-autocomplete.component.html | 3 +++ .../components/entity/entity-autocomplete.component.ts | 6 +++--- 8 files changed, 24 insertions(+), 10 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts index db5d1d7e23..9be6cfd797 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts @@ -40,6 +40,7 @@ import { map } from 'rxjs/operators'; export interface AIModelDialogData { AIModel?: AiModel; isAdd?: boolean; + name?: string; } @Component({ @@ -110,6 +111,10 @@ export class AIModelDialogComponent extends DialogComponent { diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html index 80519cea28..5dad0cacf7 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html @@ -29,7 +29,7 @@ labelText="rule-node-config.ai.model" (entityChanged)="onEntityChange($event)" [entityType]="entityType.AI_MODEL" - (createNew)="createModelAi('modelId')" + (createNew)="createModelAi($event, 'modelId')" formControlName="modelId"> diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts index 47313da81d..49be7df8b6 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts @@ -99,12 +99,13 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { return this.translate.instant(`rule-node-config.ai.response-format-hint-${this.aiConfigForm.get('responseFormat.type').value}`); } - createModelAi(formControl: string) { + createModelAi(name: string, formControl: string) { this.dialog.open(AIModelDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { - isAdd: true + isAdd: true, + name } }).afterClosed() .subscribe((model) => { diff --git a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.ts index f5a9dc4a5a..423979b4bc 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/applications/mobile-app-dialog.component.ts @@ -29,6 +29,7 @@ import { MobileAppService } from '@core/http/mobile-app.service'; export interface MobileAppDialogData { platformType: PlatformType; + name?: string } @Component({ @@ -55,6 +56,9 @@ export class MobileAppDialogComponent extends DialogComponent { this.mobileAppComponent.entityForm.markAsDirty(); + if (this.data.name) { + this.mobileAppComponent.entityForm.get('title').patchValue(this.data.name, {emitEvent: false}); + } this.mobileAppComponent.entityForm.patchValue({platformType: this.data.platformType}); this.mobileAppComponent.entityForm.get('platformType').disable({emitEvent: false}); this.mobileAppComponent.isEdit = true; diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html index 6d4bd01d1b..87f2481565 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.html @@ -58,7 +58,7 @@ labelText="mobile.android-application" [entityType]="entityType.MOBILE_APP" [entitySubtype]="platformType.ANDROID" - (createNew)="createApplication('androidAppId', platformType.ANDROID)" + (createNew)="createApplication($event, 'androidAppId', platformType.ANDROID)" formControlName="androidAppId"> diff --git a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.ts index 3e31401703..a866685282 100644 --- a/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/mobile/bundes/mobile-bundle-dialog.component.ts @@ -148,12 +148,13 @@ export class MobileBundleDialogComponent extends DialogComponent(MobileAppDialogComponent, { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { - platformType + platformType, + name } }).afterClosed() .subscribe((app) => { diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html index 92a316efe9..37882a4879 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html @@ -71,6 +71,9 @@ {{ noEntitiesMatchingText | translate: {entity: searchText} }} + + entity.create-new-key + diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts index 8cb785c6f1..9137a3b2d7 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts @@ -142,7 +142,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit entityChanged = new EventEmitter>(); @Output() - createNew = new EventEmitter(); + createNew = new EventEmitter(); @ViewChild('entityInput', {static: true}) entityInput: ElementRef; @@ -451,9 +451,9 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit return entityType; } - createNewEntity($event: Event) { + createNewEntity($event: Event, searchText?: string) { $event.stopPropagation(); - this.createNew.emit(); + this.createNew.emit(searchText); } get showEntityLink(): boolean { From 3643b54985a525ac79673674b5bf3a398a83b35d Mon Sep 17 00:00:00 2001 From: dshvaika Date: Fri, 8 Aug 2025 19:48:29 +0300 Subject: [PATCH 034/216] Added relation creation support --- ...CalculatedFieldEntityMessageProcessor.java | 26 +---- ...alculatedFieldManagerMessageProcessor.java | 10 +- .../cf/DefaultCalculatedFieldCache.java | 4 +- .../cf/ctx/state/CalculatedFieldCtx.java | 6 +- .../cf/ctx/state/CalculatedFieldState.java | 2 +- .../state/GeofencingCalculatedFieldState.java | 102 +++++++++++++++--- .../ctx/state/ScriptCalculatedFieldState.java | 4 +- .../ctx/state/SimpleCalculatedFieldState.java | 4 +- .../state/ScriptCalculatedFieldStateTest.java | 7 +- .../state/SimpleCalculatedFieldStateTest.java | 18 ++-- ...eofencingCalculatedFieldConfiguration.java | 5 +- .../cf/configuration/GeofencingEvent.java | 10 +- 12 files changed, 134 insertions(+), 64 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 6d832a9f23..ff5799b91a 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -321,33 +321,17 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM boolean stateSizeChecked = false; try { if (ctx.isInitialized() && state.isReady()) { - List calculationResults = state.performCalculation(ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS); + CalculatedFieldResult calculationResult = state.performCalculation(ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS); state.checkStateSize(ctxId, ctx.getMaxStateSize()); stateSizeChecked = true; if (state.isSizeOk()) { - if (calculationResults.isEmpty()) { - callback.onSuccess(); + if (!calculationResult.isEmpty()) { + cfService.pushMsgToRuleEngine(tenantId, entityId, calculationResult, cfIdList, callback); } else { - TbCallback effectiveCallback = calculationResults.size() > 1 ? - new MultipleTbCallback(calculationResults.size(), callback) : callback; - for (CalculatedFieldResult calculationResult : calculationResults) { - if (calculationResult.isEmpty()) { - effectiveCallback.onSuccess(); - } else { - cfService.pushMsgToRuleEngine(tenantId, entityId, calculationResult, cfIdList, effectiveCallback); - } - } + callback.onSuccess(); } if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) { - if (calculationResults.isEmpty()) { - systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, - state.getArguments(), tbMsgId, tbMsgType, null, null); - } else { - for (CalculatedFieldResult calculationResult : calculationResults) { - systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, - state.getArguments(), tbMsgId, tbMsgType, calculationResult.getResultAsString(), null); - } - } + systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, calculationResult.getResult().toString(), null); } } } else { diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index 5af4e08785..76acdc329b 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -136,7 +136,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware public void onFieldInitMsg(CalculatedFieldInitMsg msg) throws CalculatedFieldException { log.debug("[{}] Processing CF init message.", msg.getCf().getId()); var cf = msg.getCf(); - var cfCtx = new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService()); + var cfCtx = getCfCtx(cf); try { cfCtx.init(); } catch (Exception e) { @@ -297,7 +297,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware log.debug("[{}] Failed to lookup CF by id [{}]", tenantId, cfId); callback.onSuccess(); } else { - var cfCtx = new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService()); + var cfCtx = getCfCtx(cf); try { cfCtx.init(); } catch (Exception e) { @@ -313,6 +313,10 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } } + private CalculatedFieldCtx getCfCtx(CalculatedField cf) { + return new CalculatedFieldCtx(cf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService(), systemContext.getRelationService()); + } + private void onCfUpdated(ComponentLifecycleMsg msg, TbCallback callback) throws CalculatedFieldException { var cfId = new CalculatedFieldId(msg.getEntityId().getId()); var oldCfCtx = calculatedFields.get(cfId); @@ -324,7 +328,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware log.debug("[{}] Failed to lookup CF by id [{}]", tenantId, cfId); callback.onSuccess(); } else { - var newCfCtx = new CalculatedFieldCtx(newCf, systemContext.getTbelInvokeService(), systemContext.getApiLimitService()); + var newCfCtx = getCfCtx(newCf); try { newCfCtx.init(); } catch (Exception e) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java index 95fa7aebb1..2fb85ada37 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldCache.java @@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.msg.cf.CalculatedFieldInitMsg; import org.thingsboard.server.common.msg.cf.CalculatedFieldLinkInitMsg; import org.thingsboard.server.dao.cf.CalculatedFieldService; +import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; @@ -56,6 +57,7 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { private final CalculatedFieldService calculatedFieldService; private final TbelInvokeService tbelInvokeService; private final ApiLimitService apiLimitService; + private final RelationService relationService; @Lazy private final ActorSystemContext actorSystemContext; @@ -119,7 +121,7 @@ public class DefaultCalculatedFieldCache implements CalculatedFieldCache { if (ctx == null) { CalculatedField calculatedField = getCalculatedField(calculatedFieldId); if (calculatedField != null) { - ctx = new CalculatedFieldCtx(calculatedField, tbelInvokeService, apiLimitService); + ctx = new CalculatedFieldCtx(calculatedField, tbelInvokeService, apiLimitService, relationService); calculatedFieldsCtx.put(calculatedFieldId, ctx); log.debug("[{}] Put calculated field ctx into cache: {}", calculatedFieldId, ctx); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 0fa653cf67..d77413840e 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.util.ProtoUtils; +import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; @@ -68,13 +69,15 @@ public class CalculatedFieldCtx { private CalculatedFieldScriptEngine calculatedFieldScriptEngine; private ThreadLocal customExpression; + private RelationService relationService; + private boolean initialized; private long maxDataPointsPerRollingArg; private long maxStateSize; private long maxSingleValueArgumentSize; - public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService, ApiLimitService apiLimitService) { + public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService, ApiLimitService apiLimitService, RelationService relationService) { this.calculatedField = calculatedField; this.cfId = calculatedField.getId(); @@ -102,6 +105,7 @@ public class CalculatedFieldCtx { this.expression = configuration.getExpression(); this.useLatestTs = CalculatedFieldType.SIMPLE.equals(calculatedField.getType()) && ((SimpleCalculatedFieldConfiguration) configuration).isUseLatestTs(); this.tbelInvokeService = tbelInvokeService; + this.relationService = relationService; this.maxDataPointsPerRollingArg = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg); this.maxStateSize = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxStateSizeInKBytes) * 1024; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java index bb7515d7f5..2fdd0c00d6 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java @@ -55,7 +55,7 @@ public interface CalculatedFieldState { boolean updateState(CalculatedFieldCtx ctx, Map argumentValues); - ListenableFuture> performCalculation(CalculatedFieldCtx ctx); + ListenableFuture performCalculation(CalculatedFieldCtx ctx); @JsonIgnore boolean isReady(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java index 8a209dd138..32c49eeb54 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java @@ -18,6 +18,7 @@ package org.thingsboard.server.service.cf.ctx.state; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import lombok.AllArgsConstructor; import lombok.Data; import org.thingsboard.common.util.JacksonUtil; @@ -25,13 +26,15 @@ import org.thingsboard.common.util.geo.Coordinates; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; -import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.utils.CalculatedFieldUtils; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -112,33 +115,93 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { return stateUpdated; } - - // TODO: Probably returning list of CalculatedFieldResult no needed anymore, - // since logic changed to use zone groups with telemetry prefix. @Override - public ListenableFuture> performCalculation(CalculatedFieldCtx ctx) { + public ListenableFuture performCalculation(CalculatedFieldCtx ctx) { double latitude = (double) arguments.get(ENTITY_ID_LATITUDE_ARGUMENT_KEY).getValue(); double longitude = (double) arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY).getValue(); Coordinates entityCoordinates = new Coordinates(latitude, longitude); var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); - Map geofencingZoneGroupConfigurations = configuration.getGeofencingZoneGroupConfigurations(); + if (configuration.isTrackRelationToZones()) { + // TODO: currently creates relation to device profile if CF created for profile) + return calculateWithRelations(ctx, entityCoordinates, configuration); + } + return calculateWithoutRelations(ctx, entityCoordinates, configuration); + } + + private ListenableFuture calculateWithRelations( + CalculatedFieldCtx ctx, + Coordinates entityCoordinates, + GeofencingCalculatedFieldConfiguration configuration) { + var geofencingZoneGroupConfigurations = configuration.getGeofencingZoneGroupConfigurations(); + + Map zoneEventMap = new HashMap<>(); ObjectNode resultNode = JacksonUtil.newObjectNode(); + getGeofencingArguments().forEach((argumentKey, argumentEntry) -> { var zoneGroupConfig = geofencingZoneGroupConfigurations.get(argumentKey); - Set zoneEvents = argumentEntry.getZoneStates() - .values() - .stream() - .map(zoneState -> zoneState.evaluate(entityCoordinates)) + Set groupEvents = new HashSet<>(); + + argumentEntry.getZoneStates().forEach((zoneId, zoneState) -> { + GeofencingEvent event = zoneState.evaluate(entityCoordinates); + zoneEventMap.put(zoneId, event); + groupEvents.add(event); + }); + + aggregateZoneGroupEvent(groupEvents) + .filter(zoneGroupConfig.getReportEvents()::contains) + .ifPresent(geofencingGroupEvent -> + resultNode.put(zoneGroupConfig.getReportTelemetryPrefix() + "Event", geofencingGroupEvent.name())); + }); + + var result = calculationResult(ctx, resultNode); + + List> relationFutures = zoneEventMap.entrySet().stream() + .filter(entry -> entry.getValue().isTransitionEvent()) + .map(entry -> { + EntityRelation relation = toRelation(entry.getKey(), ctx, configuration); + return switch (entry.getValue()) { + case ENTERED -> ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), relation); + case LEFT -> ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), relation); + default -> throw new IllegalStateException("Unexpected transition event: " + entry.getValue()); + }; + }) + .toList(); + + if (relationFutures.isEmpty()) { + return Futures.immediateFuture(result); + } + + return Futures.whenAllComplete(relationFutures).call(() -> + new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode), + MoreExecutors.directExecutor()); + } + + private ListenableFuture calculateWithoutRelations( + CalculatedFieldCtx ctx, + Coordinates entityCoordinates, + GeofencingCalculatedFieldConfiguration configuration) { + + var geofencingZoneGroupConfigurations = configuration.getGeofencingZoneGroupConfigurations(); + ObjectNode resultNode = JacksonUtil.newObjectNode(); + + getGeofencingArguments().forEach((argumentKey, argumentEntry) -> { + var zoneGroupConfig = geofencingZoneGroupConfigurations.get(argumentKey); + Set groupEvents = argumentEntry.getZoneStates().values().stream() + .map(zs -> zs.evaluate(entityCoordinates)) .collect(Collectors.toSet()); - aggregateZoneGroupEvent(zoneEvents) - .filter(geofencingEvent -> zoneGroupConfig.getReportEvents().contains(geofencingEvent)) - .ifPresent(event -> - resultNode.put(zoneGroupConfig.getReportTelemetryPrefix() + "Event", event.name()) - ); + aggregateZoneGroupEvent(groupEvents) + .filter(zoneGroupConfig.getReportEvents()::contains) + .ifPresent(e -> resultNode.put( + zoneGroupConfig.getReportTelemetryPrefix() + "Event", + e.name())); }); - return Futures.immediateFuture(List.of(new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode))); + return Futures.immediateFuture(calculationResult(ctx, resultNode)); + } + + private CalculatedFieldResult calculationResult(CalculatedFieldCtx ctx, ObjectNode resultNode) { + return new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode); } @Override @@ -196,4 +259,11 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { return Optional.empty(); } + private EntityRelation toRelation(EntityId zoneId, CalculatedFieldCtx ctx, GeofencingCalculatedFieldConfiguration configuration) { + return switch (configuration.getZoneRelationDirection()) { + case TO -> new EntityRelation(zoneId, ctx.getEntityId(), configuration.getZoneRelationType()); + case FROM -> new EntityRelation(ctx.getEntityId(), zoneId, configuration.getZoneRelationType()); + }; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java index b1095cf13f..84dce627ae 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java @@ -53,7 +53,7 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState { } @Override - public ListenableFuture> performCalculation(CalculatedFieldCtx ctx) { + public ListenableFuture performCalculation(CalculatedFieldCtx ctx) { Map arguments = new LinkedHashMap<>(); List args = new ArrayList<>(ctx.getArgNames().size() + 1); args.add(new Object()); // first element is a ctx, but we will set it later; @@ -70,7 +70,7 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState { ListenableFuture resultFuture = ctx.getCalculatedFieldScriptEngine().executeJsonAsync(args.toArray()); Output output = ctx.getOutput(); return Futures.transform(resultFuture, - result -> List.of(new CalculatedFieldResult(output.getType(), output.getScope(), result)), + result -> new CalculatedFieldResult(output.getType(), output.getScope(), result), MoreExecutors.directExecutor() ); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java index 6a5ddb3c70..577ff80219 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java @@ -52,7 +52,7 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { } @Override - public ListenableFuture> performCalculation(CalculatedFieldCtx ctx) { + public ListenableFuture performCalculation(CalculatedFieldCtx ctx) { var expr = ctx.getCustomExpression().get(); for (Map.Entry entry : this.arguments.entrySet()) { @@ -76,7 +76,7 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { Object result = formatResult(expressionResult, output.getDecimalsByDefault()); JsonNode outputResult = createResultJson(ctx.isUseLatestTs(), output.getName(), result); - return Futures.immediateFuture(List.of(new CalculatedFieldResult(output.getType(), output.getScope(), outputResult))); + return Futures.immediateFuture(new CalculatedFieldResult(output.getType(), output.getScope(), outputResult)); } private Object formatResult(double expressionResult, Integer decimals) { diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java index 15770d80f2..b82d407d3e 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java @@ -78,7 +78,7 @@ public class ScriptCalculatedFieldStateTest { @BeforeEach void setUp() { when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); - ctx = new CalculatedFieldCtx(getCalculatedField(), tbelInvokeService, apiLimitService); + ctx = new CalculatedFieldCtx(getCalculatedField(), tbelInvokeService, apiLimitService, null); ctx.init(); state = new ScriptCalculatedFieldState(ctx.getArgNames()); } @@ -125,10 +125,9 @@ public class ScriptCalculatedFieldStateTest { void testPerformCalculation() throws ExecutionException, InterruptedException { state.arguments = new HashMap<>(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry)); - List resultList = state.performCalculation(ctx).get(); + CalculatedFieldResult result = state.performCalculation(ctx).get(); - assertThat(resultList).isNotNull().hasSize(1); - CalculatedFieldResult result = resultList.get(0); + assertThat(result).isNotNull(); Output output = getCalculatedFieldConfig().getOutput(); assertThat(result.getType()).isEqualTo(output.getType()); assertThat(result.getScope()).isEqualTo(output.getScope()); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java index b20abf8cdd..3f69b6fc3a 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java @@ -42,7 +42,6 @@ import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.service.cf.CalculatedFieldResult; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -72,7 +71,7 @@ public class SimpleCalculatedFieldStateTest { @BeforeEach void setUp() { when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); - ctx = new CalculatedFieldCtx(getCalculatedField(), null, apiLimitService); + ctx = new CalculatedFieldCtx(getCalculatedField(), null, apiLimitService, null); ctx.init(); state = new SimpleCalculatedFieldState(ctx.getArgNames()); } @@ -135,10 +134,9 @@ public class SimpleCalculatedFieldStateTest { "key3", key3ArgEntry )); - List resultList = state.performCalculation(ctx).get(); + CalculatedFieldResult result = state.performCalculation(ctx).get(); - assertThat(resultList).isNotNull().hasSize(1); - CalculatedFieldResult result = resultList.get(0); + assertThat(result).isNotNull(); Output output = getCalculatedFieldConfig().getOutput(); assertThat(result.getType()).isEqualTo(output.getType()); assertThat(result.getScope()).isEqualTo(output.getScope()); @@ -166,10 +164,9 @@ public class SimpleCalculatedFieldStateTest { "key3", key3ArgEntry )); - List resultList = state.performCalculation(ctx).get(); + CalculatedFieldResult result = state.performCalculation(ctx).get(); - assertThat(resultList).isNotNull().hasSize(1); - CalculatedFieldResult result = resultList.get(0); + assertThat(result).isNotNull(); Output output = getCalculatedFieldConfig().getOutput(); assertThat(result.getType()).isEqualTo(output.getType()); assertThat(result.getScope()).isEqualTo(output.getScope()); @@ -188,10 +185,9 @@ public class SimpleCalculatedFieldStateTest { output.setDecimalsByDefault(3); ctx.setOutput(output); - List resultList = state.performCalculation(ctx).get(); + CalculatedFieldResult result = state.performCalculation(ctx).get(); - assertThat(resultList).isNotNull().hasSize(1); - CalculatedFieldResult result = resultList.get(0); + assertThat(result).isNotNull(); assertThat(result.getType()).isEqualTo(output.getType()); assertThat(result.getScope()).isEqualTo(output.getScope()); assertThat(result.getResult()).isEqualTo(JacksonUtil.valueToTree(Map.of("output", 49.546))); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java index 64fdc05144..d9b3621ceb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java @@ -19,6 +19,7 @@ import lombok.Data; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.util.CollectionsUtil; import java.util.HashSet; @@ -40,8 +41,9 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC ENTITY_ID_LONGITUDE_ARGUMENT_KEY ); + private boolean trackRelationToZones; private String zoneRelationType; - private boolean trackZoneRelations; + private EntitySearchDirection zoneRelationDirection; private Map geofencingZoneGroupConfigurations; @Override @@ -50,6 +52,7 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC } // TODO: update validate method in PE version. + // Add relation tracking configuration validation @Override public void validate() { if (arguments == null) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java index e812918df7..d770520daa 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java @@ -15,8 +15,16 @@ */ package org.thingsboard.server.common.data.cf.configuration; +import lombok.Getter; + +@Getter public enum GeofencingEvent { - ENTERED, LEFT, INSIDE, OUTSIDE; + ENTERED(true), LEFT(true), INSIDE(false), OUTSIDE(false); + + private final boolean transitionEvent; + GeofencingEvent(boolean transitionEvent) { + this.transitionEvent = transitionEvent; + } } From e30adf0447450a6e2796392fd5b06e06bdba6c0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=97=AD?= Date: Sun, 10 Aug 2025 10:30:49 +0800 Subject: [PATCH 035/216] Fix: Improve Edge session cleanup to prevent resource leaks In unstable network environments, Edge devices may frequently disconnect and reconnect. The previous session cleanup logic could fail to stop the Kafka consumer, creating a 'zombie consumer'. This commit introduces a multi-layered defense: 1. Proactively evicts stale members from the Kafka consumer group upon new connection to ensure immediate functionality. 2. Adds a background task to persistently try and clean up session objects that failed to destroy, preventing memory/thread leaks. --- .../service/edge/rpc/EdgeGrpcService.java | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index eaef1f7c7d..e1a193bdfa 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -69,17 +69,8 @@ import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import java.io.IOException; import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; +import java.util.*; +import java.util.concurrent.*; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; @@ -102,6 +93,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private final ConcurrentMap> sessionEdgeEventChecks = new ConcurrentHashMap<>(); private final ConcurrentMap> localSyncEdgeRequests = new ConcurrentHashMap<>(); private final ConcurrentMap edgeEventsMigrationProcessed = new ConcurrentHashMap<>(); + private final Queue zombieSessions = new ConcurrentLinkedQueue<>(); @Value("${edges.rpc.port}") private int rpcPort; @@ -166,6 +158,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private ScheduledExecutorService executorService; + private ScheduledExecutorService zombieSessionsExecutorService; + @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) public void onStartUp() { log.info("Initializing Edge RPC service!"); @@ -197,7 +191,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i this.edgeEventProcessingExecutorService = ThingsBoardExecutors.newScheduledThreadPool(schedulerPoolSize, "edge-event-check-scheduler"); this.sendDownlinkExecutorService = ThingsBoardExecutors.newScheduledThreadPool(sendSchedulerPoolSize, "edge-send-scheduler"); this.executorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edge-service"); + this.zombieSessionsExecutorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("zombie-sessions"); this.executorService.scheduleAtFixedRate(this::destroyKafkaSessionIfDisconnectedAndConsumerActive, 60, 60, TimeUnit.SECONDS); + this.zombieSessionsExecutorService.scheduleAtFixedRate(this::cleanupZombieSessions, 30, 60, TimeUnit.SECONDS); log.info("Edge RPC service initialized!"); } @@ -520,11 +516,11 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i edgeIdServiceIdCache.evict(edgeId); } - private void destroySession(EdgeGrpcSession session) { + private boolean destroySession(EdgeGrpcSession session) { try (session) { for (int i = 0; i < DESTROY_SESSION_MAX_ATTEMPTS; i++) { if (session.destroy()) { - break; + return true; } else { try { Thread.sleep(100); @@ -532,6 +528,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i } } } + return false; } private void save(TenantId tenantId, EdgeId edgeId, String key, long value) { @@ -663,4 +660,28 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i log.warn("Failed to cleanup kafka sessions", e); } } + + private void cleanupZombieSessions() { + int zombiesToProcess = zombieSessions.size(); + if (zombiesToProcess == 0) { + return; + } + log.info("Found {} zombie sessions in the queue. Starting cleanup cycle.", zombiesToProcess); + for (int i = 0; i < zombiesToProcess; i++) { + EdgeGrpcSession zombie = zombieSessions.poll(); + if (zombie == null) { + break; + } + log.warn("[{}] Attempting to clean up zombie session [{}] for edge [{}].", + zombie.getTenantId(), zombie.getSessionId(), zombie.getEdge().getId()); + if (!destroySession(zombie)) { + log.warn("[{}] Zombie session [{}] cleanup failed again. Re-queuing for next attempt.", + zombie.getTenantId(), zombie.getSessionId()); + zombieSessions.add(zombie); + } else { + log.info("[{}] Successfully cleaned up zombie session [{}].", + zombie.getTenantId(), zombie.getSessionId()); + } + } + } } From 4523efbcdd73993c3822f68e587ac33157f663e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=97=AD?= Date: Sun, 10 Aug 2025 11:42:47 +0800 Subject: [PATCH 036/216] Fix: Improve Edge session cleanup to prevent resource leaks In unstable network environments, Edge devices may frequently disconnect and reconnect. The previous session cleanup logic could fail to stop the Kafka consumer, creating a 'zombie consumer'. This commit introduces a multi-layered defense: 1. Proactively evicts stale members from the Kafka consumer group upon new connection to ensure immediate functionality. 2. Adds a background task to persistently try and clean up session objects that failed to destroy, preventing memory/thread leaks. --- .../thingsboard/server/service/edge/rpc/EdgeGrpcService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index e1a193bdfa..4217fd509b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -219,6 +219,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i if (executorService != null) { executorService.shutdownNow(); } + if(zombieSessionsExecutorService != null){ + zombieSessionsExecutorService.shutdownNow(); + } } @Override From 09d08216a77824afe36592b0e9840f79c6a6b118 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Mon, 11 Aug 2025 09:15:44 +0300 Subject: [PATCH 037/216] UI: Ref create new link --- .../components/entity/entity-autocomplete.component.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html index 37882a4879..db7300530e 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html @@ -71,9 +71,11 @@ {{ noEntitiesMatchingText | translate: {entity: searchText} }} - - entity.create-new-key - + @if (allowCreateNew) { + + entity.create-new-key + + } From baba433f0f4580fb2a8790ae2bdf5a74dedf2d49 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Mon, 11 Aug 2025 13:23:17 +0300 Subject: [PATCH 038/216] Added validation for new configuration + fixed relation creation for profile entities --- ...CalculatedFieldEntityMessageProcessor.java | 2 +- .../ctx/state/BaseCalculatedFieldState.java | 4 +--- .../cf/ctx/state/CalculatedFieldState.java | 11 ++++++---- .../state/GeofencingCalculatedFieldState.java | 20 +++++++++---------- .../ctx/state/ScriptCalculatedFieldState.java | 3 ++- .../ctx/state/SimpleCalculatedFieldState.java | 3 ++- .../state/ScriptCalculatedFieldStateTest.java | 2 +- .../state/SimpleCalculatedFieldStateTest.java | 8 ++++---- ...eofencingCalculatedFieldConfiguration.java | 18 ++++++++++++++--- 9 files changed, 42 insertions(+), 29 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index ff5799b91a..2da66afe31 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -321,7 +321,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM boolean stateSizeChecked = false; try { if (ctx.isInitialized() && state.isReady()) { - CalculatedFieldResult calculationResult = state.performCalculation(ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS); + CalculatedFieldResult calculationResult = state.performCalculation(entityId, ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS); state.checkStateSize(ctxId, ctx.getMaxStateSize()); stateSizeChecked = true; if (state.isSizeOk()) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index fa7e628ab3..eb87d375c5 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -35,15 +35,13 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { protected long latestTimestamp = -1; - private boolean dirty; - public BaseCalculatedFieldState(List requiredArguments) { this.requiredArguments = requiredArguments; this.arguments = new HashMap<>(); } public BaseCalculatedFieldState() { - this(new ArrayList<>(), new HashMap<>(), false, -1, false); + this(new ArrayList<>(), new HashMap<>(), false, -1); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java index 2fdd0c00d6..e58ca699e2 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; @@ -47,15 +48,18 @@ public interface CalculatedFieldState { long getLatestTimestamp(); - void setDirty(boolean dirty); + default void setDirty(boolean dirty) { + } - boolean isDirty(); + default boolean isDirty() { + return false; + } void setRequiredArguments(List requiredArguments); boolean updateState(CalculatedFieldCtx ctx, Map argumentValues); - ListenableFuture performCalculation(CalculatedFieldCtx ctx); + ListenableFuture performCalculation(EntityId entityId, CalculatedFieldCtx ctx); @JsonIgnore boolean isReady(); @@ -70,7 +74,6 @@ public interface CalculatedFieldState { void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize); default void checkArgumentSize(String name, ArgumentEntry entry, CalculatedFieldCtx ctx) { - // TODO: Do we need to restrict the size of Geofencing arguments? Number of zones? if (entry instanceof TsRollingArgumentEntry || entry instanceof GeofencingArgumentEntry) { return; } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java index 32c49eeb54..f0b1bb7594 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java @@ -43,7 +43,6 @@ import java.util.stream.Collectors; import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.coordinateKeys; @Data @AllArgsConstructor @@ -116,20 +115,20 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { } @Override - public ListenableFuture performCalculation(CalculatedFieldCtx ctx) { + public ListenableFuture performCalculation(EntityId entityId, CalculatedFieldCtx ctx) { double latitude = (double) arguments.get(ENTITY_ID_LATITUDE_ARGUMENT_KEY).getValue(); double longitude = (double) arguments.get(ENTITY_ID_LONGITUDE_ARGUMENT_KEY).getValue(); Coordinates entityCoordinates = new Coordinates(latitude, longitude); var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); - if (configuration.isTrackRelationToZones()) { - // TODO: currently creates relation to device profile if CF created for profile) - return calculateWithRelations(ctx, entityCoordinates, configuration); + if (configuration.isCreateRelationsWithMatchedZones()) { + return calculateWithRelations(entityId, ctx, entityCoordinates, configuration); } return calculateWithoutRelations(ctx, entityCoordinates, configuration); } private ListenableFuture calculateWithRelations( + EntityId entityId, CalculatedFieldCtx ctx, Coordinates entityCoordinates, GeofencingCalculatedFieldConfiguration configuration) { @@ -160,7 +159,7 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { List> relationFutures = zoneEventMap.entrySet().stream() .filter(entry -> entry.getValue().isTransitionEvent()) .map(entry -> { - EntityRelation relation = toRelation(entry.getKey(), ctx, configuration); + EntityRelation relation = toRelation(entry.getKey(), entityId, configuration); return switch (entry.getValue()) { case ENTERED -> ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), relation); case LEFT -> ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), relation); @@ -218,11 +217,10 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { } } - // TODO: Create a new class field to not do this on each calculation. private Map getGeofencingArguments() { return arguments.entrySet() .stream() - .filter(entry -> !coordinateKeys.contains(entry.getKey())) + .filter(entry -> entry.getValue().getType().equals(ArgumentEntryType.GEOFENCING)) .collect(Collectors.toMap(Map.Entry::getKey, entry -> (GeofencingArgumentEntry) entry.getValue())); } @@ -259,10 +257,10 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { return Optional.empty(); } - private EntityRelation toRelation(EntityId zoneId, CalculatedFieldCtx ctx, GeofencingCalculatedFieldConfiguration configuration) { + private EntityRelation toRelation(EntityId zoneId, EntityId entityId, GeofencingCalculatedFieldConfiguration configuration) { return switch (configuration.getZoneRelationDirection()) { - case TO -> new EntityRelation(zoneId, ctx.getEntityId(), configuration.getZoneRelationType()); - case FROM -> new EntityRelation(ctx.getEntityId(), zoneId, configuration.getZoneRelationType()); + case TO -> new EntityRelation(zoneId, entityId, configuration.getZoneRelationType()); + case FROM -> new EntityRelation(entityId, zoneId, configuration.getZoneRelationType()); }; } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java index 84dce627ae..e1f1305c48 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java @@ -27,6 +27,7 @@ import org.thingsboard.script.api.tbel.TbelCfCtx; import org.thingsboard.script.api.tbel.TbelCfSingleValueArg; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.cf.CalculatedFieldResult; import java.util.ArrayList; @@ -53,7 +54,7 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState { } @Override - public ListenableFuture performCalculation(CalculatedFieldCtx ctx) { + public ListenableFuture performCalculation(EntityId entityId, CalculatedFieldCtx ctx) { Map arguments = new LinkedHashMap<>(); List args = new ArrayList<>(ctx.getArgNames().size() + 1); args.add(new Object()); // first element is a ctx, but we will set it later; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java index 577ff80219..76839a3cbc 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java @@ -25,6 +25,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.TbUtils; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.BasicKvEntry; import org.thingsboard.server.service.cf.CalculatedFieldResult; @@ -52,7 +53,7 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { } @Override - public ListenableFuture performCalculation(CalculatedFieldCtx ctx) { + public ListenableFuture performCalculation(EntityId entityId, CalculatedFieldCtx ctx) { var expr = ctx.getCustomExpression().get(); for (Map.Entry entry : this.arguments.entrySet()) { diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java index b82d407d3e..280bac6bd4 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldStateTest.java @@ -125,7 +125,7 @@ public class ScriptCalculatedFieldStateTest { void testPerformCalculation() throws ExecutionException, InterruptedException { state.arguments = new HashMap<>(Map.of("deviceTemperature", deviceTemperatureArgEntry, "assetHumidity", assetHumidityArgEntry)); - CalculatedFieldResult result = state.performCalculation(ctx).get(); + CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); assertThat(result).isNotNull(); Output output = getCalculatedFieldConfig().getOutput(); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java index 3f69b6fc3a..8c631ecf6f 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java @@ -134,7 +134,7 @@ public class SimpleCalculatedFieldStateTest { "key3", key3ArgEntry )); - CalculatedFieldResult result = state.performCalculation(ctx).get(); + CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); assertThat(result).isNotNull(); Output output = getCalculatedFieldConfig().getOutput(); @@ -151,7 +151,7 @@ public class SimpleCalculatedFieldStateTest { "key3", key3ArgEntry )); - assertThatThrownBy(() -> state.performCalculation(ctx)) + assertThatThrownBy(() -> state.performCalculation(ctx.getEntityId(), ctx)) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Argument 'key2' is not a number."); } @@ -164,7 +164,7 @@ public class SimpleCalculatedFieldStateTest { "key3", key3ArgEntry )); - CalculatedFieldResult result = state.performCalculation(ctx).get(); + CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); assertThat(result).isNotNull(); Output output = getCalculatedFieldConfig().getOutput(); @@ -185,7 +185,7 @@ public class SimpleCalculatedFieldStateTest { output.setDecimalsByDefault(3); ctx.setOutput(output); - CalculatedFieldResult result = state.performCalculation(ctx).get(); + CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); assertThat(result).isNotNull(); assertThat(result.getType()).isEqualTo(output.getType()); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java index d9b3621ceb..652d6b2182 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java @@ -41,7 +41,7 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC ENTITY_ID_LONGITUDE_ARGUMENT_KEY ); - private boolean trackRelationToZones; + private boolean createRelationsWithMatchedZones; private String zoneRelationType; private EntitySearchDirection zoneRelationDirection; private Map geofencingZoneGroupConfigurations; @@ -52,7 +52,6 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC } // TODO: update validate method in PE version. - // Add relation tracking configuration validation @Override public void validate() { if (arguments == null) { @@ -72,6 +71,19 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC } validateZoneGroupAruguments(zoneGroupsArguments); validateZoneGroupConfigurations(zoneGroupsArguments); + validateZoneRelationsConfiguration(); + } + + private void validateZoneRelationsConfiguration() { + if (!createRelationsWithMatchedZones) { + return; + } + if (StringUtils.isBlank(zoneRelationType)) { + throw new IllegalArgumentException("Zone relation type must be specified when to maintain relations with matched zones!"); + } + if (zoneRelationDirection == null) { + throw new IllegalArgumentException("Zone relation direction must be specified to maintain relations with matched zones!"); + } } private void validateZoneGroupConfigurations(Map zoneGroupsArguments) { @@ -146,7 +158,7 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } - private static ReferencedEntityKey validateAndGetRefEntityKey(Argument argument, String argumentKey) { + private ReferencedEntityKey validateAndGetRefEntityKey(Argument argument, String argumentKey) { ReferencedEntityKey refEntityKey = argument.getRefEntityKey(); if (refEntityKey == null || refEntityKey.getType() == null) { throw new IllegalArgumentException("Missing or invalid reference entity key for argument: " + argumentKey); From efc20a93aa039eb0bdf212446e4e78d50d41898e Mon Sep 17 00:00:00 2001 From: dshvaika Date: Mon, 11 Aug 2025 17:44:40 +0300 Subject: [PATCH 039/216] Added mock tests for Geofencing CF state and utils logic to/from proto --- .../state/GeofencingCalculatedFieldState.java | 8 +- .../cf/ctx/state/GeofencingZoneState.java | 11 +- .../server/utils/CalculatedFieldUtils.java | 1 - .../GeofencingCalculatedFieldStateTest.java | 360 ++++++++++++++++++ .../utils/CalculatedFieldUtilsTest.java | 108 ++++++ .../BaseCalculatedFieldConfiguration.java | 6 - .../CalculatedFieldConfiguration.java | 9 +- ...eofencingCalculatedFieldConfiguration.java | 9 +- ...lationQueryDynamicSourceConfiguration.java | 3 +- 9 files changed, 494 insertions(+), 21 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java create mode 100644 application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java index f0b1bb7594..3de0cc6c31 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java @@ -49,7 +49,7 @@ import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalc public class GeofencingCalculatedFieldState implements CalculatedFieldState { private List requiredArguments; - private Map arguments; + Map arguments; private boolean sizeExceedsLimit; private long latestTimestamp = -1; @@ -91,14 +91,16 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { entryUpdated = switch (key) { case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> { if (!(newEntry instanceof SingleValueArgumentEntry singleValueArgumentEntry)) { - throw new IllegalArgumentException(key + " argument must be a single value argument."); + throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " + + "Only SINGLE_VALUE type is allowed."); } arguments.put(key, singleValueArgumentEntry); yield true; } default -> { if (!(newEntry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) { - throw new IllegalArgumentException(key + " argument must be a geofencing argument entry."); + throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " + + "Only GEOFENCING type is allowed."); } arguments.put(key, geofencingArgumentEntry); yield true; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java index 1b3879c828..3182a342e8 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java @@ -25,7 +25,6 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.KvEntry; -import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneProto; import java.util.UUID; @@ -44,13 +43,11 @@ public class GeofencingZoneState { public GeofencingZoneState(EntityId zoneId, KvEntry entry) { this.zoneId = zoneId; - if (entry instanceof TsKvEntry tsKvEntry) { - this.ts = tsKvEntry.getTs(); - this.version = tsKvEntry.getVersion(); - } else if (entry instanceof AttributeKvEntry attributeKvEntry) { - this.ts = attributeKvEntry.getLastUpdateTs(); - this.version = attributeKvEntry.getVersion(); + if (!(entry instanceof AttributeKvEntry attributeKvEntry)) { + throw new IllegalArgumentException("Unsupported KvEntry type for geofencing zone state: " + entry.getClass().getSimpleName()); } + this.ts = attributeKvEntry.getLastUpdateTs(); + this.version = attributeKvEntry.getVersion(); this.perimeterDefinition = JacksonUtil.fromString(entry.getJsonValue().orElseThrow(), PerimeterDefinition.class); } diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java index 4876fa8feb..eeb5318104 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java @@ -121,7 +121,6 @@ public class CalculatedFieldUtils { return builder.build(); } - private static GeofencingArgumentProto toGeofencingArgumentProto(String argName, GeofencingArgumentEntry geofencingArgumentEntry) { Map zoneStates = geofencingArgumentEntry.getZoneStates(); GeofencingArgumentProto.Builder builder = GeofencingArgumentProto.newBuilder() diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java new file mode 100644 index 0000000000..1850efcd11 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -0,0 +1,360 @@ +/** + * 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.service.cf.ctx.state; + +import com.google.common.util.concurrent.Futures; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.cf.CalculatedField; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.CFArgumentDynamicSourceType; +import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; +import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.OutputType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import org.thingsboard.server.service.cf.CalculatedFieldResult; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutionException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; + +@ExtendWith(MockitoExtension.class) +public class GeofencingCalculatedFieldStateTest { + + private final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("8f83eeca-b5cd-4955-9241-09d1393768c6")); + private final DeviceId DEVICE_ID = new DeviceId(UUID.fromString("688b529d-cfbe-4430-91c5-60b4f4e5d3cf")); + private final AssetId ZONE_1_ID = new AssetId(UUID.fromString("c0e3031c-7df1-45e4-9590-cfd621a4d714")); + private final AssetId ZONE_2_ID = new AssetId(UUID.fromString("e7da6200-2096-4038-a343-ade9ea4fa3e4")); + + private final SingleValueArgumentEntry latitudeArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 10, new DoubleDataEntry("latitude", 50.4730), 145L); + private final SingleValueArgumentEntry longitudeArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 6, new DoubleDataEntry("longitude", 30.5050), 165L); + + private final JsonDataEntry allowedZoneDataEntry = new JsonDataEntry("zone", """ + {"type":"POLYGON","polygonsDefinition":"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"}"""); + private final BaseAttributeKvEntry allowedZoneAttributeKvEntry = new BaseAttributeKvEntry(allowedZoneDataEntry, System.currentTimeMillis(), 0L); + private final GeofencingArgumentEntry geofencingAllowedZoneArgEntry = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, allowedZoneAttributeKvEntry)); + + private final JsonDataEntry restrictedZoneDataEntry = new JsonDataEntry("zone", """ + {"type":"POLYGON","polygonsDefinition":"[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"}"""); + private final BaseAttributeKvEntry restrictedZoneAttributeKvEntry = new BaseAttributeKvEntry(restrictedZoneDataEntry, System.currentTimeMillis(), 0L); + private final GeofencingArgumentEntry geofencingRestrictedZoneArgEntry = new GeofencingArgumentEntry(Map.of(ZONE_2_ID, restrictedZoneAttributeKvEntry)); + + + private GeofencingCalculatedFieldState state; + private CalculatedFieldCtx ctx; + + @Mock + private ApiLimitService apiLimitService; + @Mock + private RelationService relationService; + + @BeforeEach + void setUp() { + when(apiLimitService.getLimit(any(), any())).thenReturn(1000L); + ctx = new CalculatedFieldCtx(getCalculatedField(), null, apiLimitService, relationService); + ctx.init(); + state = new GeofencingCalculatedFieldState(ctx.getArgNames()); + } + + @Test + void testType() { + assertThat(state.getType()).isEqualTo(CalculatedFieldType.GEOFENCING); + } + + @Test + void testUpdateState() { + state.arguments = new HashMap<>(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry + )); + + Map newArgs = Map.of("allowedZones", geofencingAllowedZoneArgEntry); + boolean stateUpdated = state.updateState(ctx, newArgs); + + assertThat(stateUpdated).isTrue(); + assertThat(state.getArguments()).containsExactlyInAnyOrderEntriesOf( + Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, + "allowedZones", geofencingAllowedZoneArgEntry + ) + ); + } + + @Test + void testUpdateStateWithInvalidArgumentTypeForLatitudeArgument() { + assertThatThrownBy(() -> state.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for latitude argument: GEOFENCING. Only SINGLE_VALUE type is allowed."); + } + + @Test + void testUpdateStateWithInvalidArgumentTypeForLongitudeArgument() { + assertThatThrownBy(() -> state.updateState(ctx, Map.of(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for longitude argument: GEOFENCING. Only SINGLE_VALUE type is allowed."); + } + + @Test + void testUpdateStateWithInvalidArgumentTypeForGeofencingArgument() { + assertThatThrownBy(() -> state.updateState(ctx, Map.of("someArgumentName", latitudeArgEntry))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for someArgumentName argument: SINGLE_VALUE. Only GEOFENCING type is allowed."); + } + + @Test + void testUpdateStateWhenUpdateExistingSingleValueArgumentEntry() { + state.arguments = new HashMap<>(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry)); + + SingleValueArgumentEntry newArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 190L); + Map newArgs = Map.of("latitude", newArgEntry); + boolean stateUpdated = state.updateState(ctx, newArgs); + + assertThat(stateUpdated).isTrue(); + assertThat(state.getArguments()).isEqualTo(newArgs); + } + + // TODO: write opposite test for this. See TODO in the GeofencingZoneState class. + @Test + void testUpdateStateWhenUpdateExistingGeofencingValueArgumentEntryWithTheSameValue() { + state.arguments = new HashMap<>(Map.of("allowedZones", geofencingAllowedZoneArgEntry)); + + Map newArgs = Map.of("allowedZones", geofencingAllowedZoneArgEntry); + + boolean stateUpdated = state.updateState(ctx, newArgs); + + assertThat(stateUpdated).isFalse(); + assertThat(state.getArguments()).isEqualTo(newArgs); + } + + @Test + void testUpdateStateWhenUpdateExistingSingleValueArgumentEntryWithValueOfAnotherType() { + state.arguments = new HashMap<>(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry)); + + assertThatThrownBy(() -> state.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, geofencingAllowedZoneArgEntry))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for single value argument entry: GEOFENCING"); + } + + + @Test + void testUpdateStateWhenUpdateExistingGeofencingValueArgumentEntryWithValueOfAnotherType() { + state.arguments = new HashMap<>(Map.of("allowedZones", geofencingAllowedZoneArgEntry)); + + assertThatThrownBy(() -> state.updateState(ctx, Map.of("allowedZones", latitudeArgEntry))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for geofencing argument entry: SINGLE_VALUE"); + } + + @Test + void testIsReadyWhenNotAllArgPresent() { + assertThat(state.isReady()).isFalse(); + } + + @Test + void testIsReadyWhenAllArgPresent() { + state.arguments = new HashMap<>(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, + "allowedZones", geofencingAllowedZoneArgEntry, + "restrictedZones", geofencingRestrictedZoneArgEntry + )); + assertThat(state.isReady()).isTrue(); + } + + @Test + void testIsReadyWhenEmptyEntryPresents() { + state.arguments = new HashMap<>(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, + "allowedZones", geofencingAllowedZoneArgEntry, + "restrictedZones", geofencingRestrictedZoneArgEntry + )); + + state.getArguments().put("noParkingZones", new GeofencingArgumentEntry()); + + assertThat(state.isReady()).isFalse(); + } + + @Test + void testPerformCalculation() throws ExecutionException, InterruptedException { + state.arguments = new HashMap<>(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, + "allowedZones", geofencingAllowedZoneArgEntry, + "restrictedZones", geofencingRestrictedZoneArgEntry + )); + + Output output = ctx.getOutput(); + var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); + + when(relationService.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); + when(relationService.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); + + CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); + + assertThat(result).isNotNull(); + assertThat(result.getType()).isEqualTo(output.getType()); + assertThat(result.getScope()).isEqualTo(output.getScope()); + assertThat(result.getResult()).isEqualTo( + JacksonUtil.newObjectNode() + .put("allowedZoneEvent", "ENTERED") + .put("restrictedZoneEvent", "OUTSIDE") + ); + + SingleValueArgumentEntry newLatitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 146L); + SingleValueArgumentEntry newLongitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", 30.5110), 166L); + + // move the device to new coordinates → leaves allowed, enters restricted + state.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude)); + + CalculatedFieldResult result2 = state.performCalculation(ctx.getEntityId(), ctx).get(); + + assertThat(result2).isNotNull(); + assertThat(result2.getType()).isEqualTo(output.getType()); + assertThat(result2.getScope()).isEqualTo(output.getScope()); + assertThat(result2.getResult()).isEqualTo( + JacksonUtil.newObjectNode() + .put("allowedZoneEvent", "LEFT") + .put("restrictedZoneEvent", "ENTERED") + ); + + + // Check relations are created and deleted correctly for both iterations. + ArgumentCaptor saveCaptor = ArgumentCaptor.forClass(EntityRelation.class); + verify(relationService, times(2)).saveRelationAsync(eq(ctx.getTenantId()), saveCaptor.capture()); + List saveValues = saveCaptor.getAllValues(); + assertThat(saveValues).hasSize(2); + + EntityRelation relationFromFirstIteration = saveValues.get(0); + assertThat(relationFromFirstIteration.getTo()).isEqualTo(ctx.getEntityId()); + assertThat(relationFromFirstIteration.getFrom()).isEqualTo(ZONE_1_ID); + assertThat(relationFromFirstIteration.getType()).isEqualTo(configuration.getZoneRelationType()); + + EntityRelation relationFromSecondIteration = saveValues.get(1); + assertThat(relationFromSecondIteration.getTo()).isEqualTo(ctx.getEntityId()); + assertThat(relationFromSecondIteration.getFrom()).isEqualTo(ZONE_2_ID); + assertThat(relationFromSecondIteration.getType()).isEqualTo(configuration.getZoneRelationType()); + + ArgumentCaptor deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class); + verify(relationService).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); + EntityRelation leftRelation = deleteCaptor.getValue(); + assertThat(leftRelation.getFrom()).isEqualTo(ZONE_1_ID); + assertThat(leftRelation.getTo()).isEqualTo(ctx.getEntityId()); + } + + private CalculatedField getCalculatedField() { + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setTenantId(TENANT_ID); + calculatedField.setEntityId(DEVICE_ID); + calculatedField.setType(CalculatedFieldType.GEOFENCING); + calculatedField.setName("Test Geofencing Calculated Field"); + calculatedField.setConfigurationVersion(1); + calculatedField.setConfiguration(getCalculatedFieldConfig()); + calculatedField.setVersion(1L); + return calculatedField; + } + + private CalculatedFieldConfiguration getCalculatedFieldConfig() { + var config = new GeofencingCalculatedFieldConfiguration(); + + Argument argument1 = new Argument(); + argument1.setRefEntityId(DEVICE_ID); + var refEntityKey1 = new ReferencedEntityKey("latitude", ArgumentType.TS_LATEST, null); + argument1.setRefEntityKey(refEntityKey1); + + Argument argument2 = new Argument(); + argument2.setRefEntityId(DEVICE_ID); + var refEntityKey2 = new ReferencedEntityKey("longitude", ArgumentType.TS_LATEST, null); + argument2.setRefEntityKey(refEntityKey2); + + Argument argument3 = new Argument(); + var refEntityKey3 = new ReferencedEntityKey("zone", ArgumentType.ATTRIBUTE, null); + var refDynamicSourceConfiguration3 = new RelationQueryDynamicSourceConfiguration(); + refDynamicSourceConfiguration3.setDirection(EntitySearchDirection.TO); + refDynamicSourceConfiguration3.setRelationType("AllowedZone"); + refDynamicSourceConfiguration3.setMaxLevel(1); + refDynamicSourceConfiguration3.setFetchLastLevelOnly(true); + argument3.setRefEntityKey(refEntityKey3); + argument3.setRefDynamicSource(CFArgumentDynamicSourceType.RELATION_QUERY); + argument3.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration3); + + Argument argument4 = new Argument(); + var refEntityKey4 = new ReferencedEntityKey("zone", ArgumentType.ATTRIBUTE, null); + var refDynamicSourceConfiguration4 = new RelationQueryDynamicSourceConfiguration(); + refDynamicSourceConfiguration4.setDirection(EntitySearchDirection.TO); + refDynamicSourceConfiguration4.setRelationType("RestrictedZone"); + refDynamicSourceConfiguration4.setMaxLevel(1); + refDynamicSourceConfiguration4.setFetchLastLevelOnly(true); + argument4.setRefEntityKey(refEntityKey4); + argument4.setRefDynamicSource(CFArgumentDynamicSourceType.RELATION_QUERY); + argument4.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration4); + + config.setArguments(Map.of("latitude", argument1, "longitude", argument2, "allowedZones", argument3, "restrictedZones", argument4)); + + List reportEvents = Arrays.stream(GeofencingEvent.values()).toList(); + GeofencingZoneGroupConfiguration allowedZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", reportEvents); + GeofencingZoneGroupConfiguration restrictedZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("restrictedZone", reportEvents); + config.setGeofencingZoneGroupConfigurations(Map.of("allowedZones", allowedZoneGroupConfiguration, "restrictedZones", restrictedZoneGroupConfiguration)); + + config.setCreateRelationsWithMatchedZones(true); + config.setZoneRelationType("CurrentZone"); + config.setZoneRelationDirection(EntitySearchDirection.TO); + + // TODO: Does CF possible to save with null? + config.setExpression("latitude + longitude"); + + Output output = new Output(); + output.setType(OutputType.TIME_SERIES); + config.setOutput(output); + return config; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java new file mode 100644 index 0000000000..3d51420f2e --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java @@ -0,0 +1,108 @@ +/** + * 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.utils; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.CalculatedFieldId; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; +import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.GeofencingZoneState; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto; + +@ExtendWith(MockitoExtension.class) +class CalculatedFieldUtilsTest { + + private static final TenantId TENANT_ID = TenantId.fromUUID(UUID.fromString("0a69e1e2-fcbc-4234-a4cd-3844bf54035c")); + private static final CalculatedFieldId CF_ID = CalculatedFieldId.fromString("ec0e91b9-6f27-4e93-946a-5fbc2707d8bc"); + private static final DeviceId DEVICE_ID = DeviceId.fromString("1e03bd38-2010-4739-9362-160c288e36c4"); + + @Test + void toProtoAndFromProto_shouldMapGeofencingArgumentsAndZones() { + // given + CalculatedFieldEntityCtxId stateId = mock(CalculatedFieldEntityCtxId.class); + given(stateId.tenantId()).willReturn(TENANT_ID); + given(stateId.cfId()).willReturn(CF_ID); + given(stateId.entityId()).willReturn(DEVICE_ID); + + // Build a geofencing argument with two zones (one with inside=true, one with inside=null) + GeofencingArgumentEntry geofencingArgumentEntry = new GeofencingArgumentEntry(); + Map zoneStates = new LinkedHashMap<>(); + + UUID zoneId1 = UUID.fromString("624a8fff-71a2-4847-a100-ff1cf52dbe71"); + UUID zoneId2 = UUID.fromString("e2adf6ce-9478-40b1-b0e9-4a6860cc46bb"); + + AssetId z1 = new AssetId(zoneId1); + AssetId z2 = new AssetId(zoneId2); + + JsonDataEntry zone1 = new JsonDataEntry("zone", "{\"type\":\"POLYGON\",\"polygonsDefinition\":\"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]\"}"); + JsonDataEntry zone2 = new JsonDataEntry("zone", "{\"type\":\"POLYGON\",\"polygonsDefinition\":\"[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]\"}"); + + BaseAttributeKvEntry zone1PerimeterAttribute = new BaseAttributeKvEntry(zone1, System.currentTimeMillis(), 0L); + BaseAttributeKvEntry zone2PerimeterAttribute = new BaseAttributeKvEntry(zone2, System.currentTimeMillis(), 0L); + + GeofencingZoneState s1 = new GeofencingZoneState(z1, zone1PerimeterAttribute); + s1.setInside(true); + GeofencingZoneState s2 = new GeofencingZoneState(z2, zone2PerimeterAttribute); + + zoneStates.put(z1, s1); + zoneStates.put(z2, s2); + geofencingArgumentEntry.setZoneStates(zoneStates); + + // Create cf state with the geofencing argument and add it to the state map + CalculatedFieldState state = new GeofencingCalculatedFieldState(List.of("geofencingArgumentTest")); + state.updateState(mock(CalculatedFieldCtx.class), Map.of("geofencingArgumentTest", geofencingArgumentEntry)); + + // when + CalculatedFieldStateProto proto = toProto(stateId, state); + + // then + CalculatedFieldState fromProto = CalculatedFieldUtils.fromProto(proto); + assertThat(fromProto) + .usingRecursiveComparison() + .ignoringFields("requiredArguments") + .isEqualTo(state); + + ArgumentEntry fromProtoArgument = fromProto.getArguments().get("geofencingArgumentTest"); + assertThat(fromProtoArgument).isInstanceOf(GeofencingArgumentEntry.class); + GeofencingArgumentEntry fromProtoGeoArgument = (GeofencingArgumentEntry) fromProtoArgument; + assertThat(fromProtoGeoArgument.getZoneStates()).hasSize(2); + assertThat(fromProtoGeoArgument.getZoneStates().get(z1).getInside()).isTrue(); + assertThat(fromProtoGeoArgument.getZoneStates().get(z2).getInside()).isNull(); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java index 71d8bba6cb..7053add5a7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java @@ -33,8 +33,6 @@ public abstract class BaseCalculatedFieldConfiguration implements CalculatedFiel protected String expression; protected Output output; - protected int scheduledUpdateIntervalSec; - @Override public List getReferencedEntities() { return arguments.values().stream() @@ -71,8 +69,4 @@ public abstract class BaseCalculatedFieldConfiguration implements CalculatedFiel } } - public boolean isScheduledUpdateEnabled() { - return scheduledUpdateIntervalSec > 0 && arguments.values().stream().anyMatch(arg -> arg.getRefDynamicSource() != null); - } - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java index 9103391326..3459e11c0c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java @@ -66,8 +66,13 @@ public interface CalculatedFieldConfiguration { return false; } - void setScheduledUpdateIntervalSec(int scheduledUpdateIntervalSec); + @JsonIgnore + default void setScheduledUpdateIntervalSec(int scheduledUpdateIntervalSec) { + } - int getScheduledUpdateIntervalSec(); + @JsonIgnore + default int getScheduledUpdateIntervalSec() { + return 0; + } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java index 652d6b2182..32cbe6baa3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java @@ -36,11 +36,13 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC public static final String ENTITY_ID_LATITUDE_ARGUMENT_KEY = "latitude"; public static final String ENTITY_ID_LONGITUDE_ARGUMENT_KEY = "longitude"; - public static final Set coordinateKeys = Set.of( + private static final Set coordinateKeys = Set.of( ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY ); + private int scheduledUpdateIntervalSec; + private boolean createRelationsWithMatchedZones; private String zoneRelationType; private EntitySearchDirection zoneRelationDirection; @@ -51,6 +53,11 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC return CalculatedFieldType.GEOFENCING; } + @Override + public boolean isScheduledUpdateEnabled() { + return scheduledUpdateIntervalSec > 0 && arguments.values().stream().anyMatch(arg -> arg.getRefDynamicSource() != null); + } + // TODO: update validate method in PE version. @Override public void validate() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java index b6e085395e..a75b7994ba 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; +import org.thingsboard.server.common.data.util.CollectionsUtil; import java.util.Collections; import java.util.List; @@ -56,7 +57,7 @@ public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynami @Override public boolean isSimpleRelation() { - return maxLevel == 1 && (entityTypes == null || entityTypes.isEmpty()); + return maxLevel == 1 && CollectionsUtil.isEmpty(entityTypes); } @Override From 5f12fc5a4f8dc443ca9a61494ca18259ab64c877 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Tue, 12 Aug 2025 11:37:23 +0300 Subject: [PATCH 040/216] Added test for GeofencingValueArgumentEntry --- .../GeofencingValueArgumentEntryTest.java | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java new file mode 100644 index 0000000000..4491716b19 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java @@ -0,0 +1,189 @@ +/** + * 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.service.cf.ctx.state; + +import io.hypersistence.utils.hibernate.type.json.internal.JacksonUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.thingsboard.common.util.geo.PerimeterDefinition; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; + +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class GeofencingValueArgumentEntryTest { + + private final AssetId ZONE_1_ID = new AssetId(UUID.fromString("c0e3031c-7df1-45e4-9590-cfd621a4d714")); + private final AssetId ZONE_2_ID = new AssetId(UUID.fromString("e7da6200-2096-4038-a343-ade9ea4fa3e4")); + + private final JsonDataEntry allowedZoneDataEntry = new JsonDataEntry("zone", """ + {"type":"POLYGON","polygonsDefinition":"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"}"""); + private final BaseAttributeKvEntry allowedZoneAttributeKvEntry = new BaseAttributeKvEntry(allowedZoneDataEntry, 363L, 155L); + + private final JsonDataEntry restrictedZoneDataEntry = new JsonDataEntry("zone", """ + {"type":"POLYGON","polygonsDefinition":"[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"}"""); + private final BaseAttributeKvEntry restrictedZoneAttributeKvEntry = new BaseAttributeKvEntry(restrictedZoneDataEntry, 363L, 155L); + + private GeofencingArgumentEntry entry; + + @BeforeEach + void setUp() { + entry = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, allowedZoneAttributeKvEntry, ZONE_2_ID, restrictedZoneAttributeKvEntry)); + } + + @Test + void testArgumentEntryType() { + assertThat(entry.getType()).isEqualTo(ArgumentEntryType.GEOFENCING); + } + + @Test + void testUpdateEntryWhenSingleEntryPassed() { + assertThatThrownBy(() -> entry.updateEntry(new SingleValueArgumentEntry())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for geofencing argument entry: SINGLE_VALUE"); + } + + @Test + void testUpdateEntryWhenRollingEntryPassed() { + assertThatThrownBy(() -> entry.updateEntry(new TsRollingArgumentEntry(5, 30000L))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported argument entry type for geofencing argument entry: TS_ROLLING"); + } + + @Test + void testUpdateEntryWithTheSameTs() { + BaseAttributeKvEntry differentValueSameTs = new BaseAttributeKvEntry(new JsonDataEntry("zone", """ + {"type":"POLYGON","polygonsDefinition":"[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"}"""), 363L, 156L); + var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueSameTs, ZONE_2_ID, restrictedZoneAttributeKvEntry)); + assertThat(entry.updateEntry(updated)).isFalse(); + } + + @Test + @SuppressWarnings("unchecked") + void testUpdateEntryWhenNewVersionIsNull() { + BaseAttributeKvEntry differentValueNewVersionIsNull = new BaseAttributeKvEntry(new JsonDataEntry("zone", """ + {"type":"POLYGON","polygonsDefinition":"[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"}"""), 364L, null); + var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsNull, ZONE_2_ID, restrictedZoneAttributeKvEntry)); + + assertThat(entry.updateEntry(updated)).isTrue(); + assertThat(entry.getValue()).isInstanceOf(Map.class); + + Map value = (Map) entry.getValue(); + assertThat(value).hasSize(2); + assertThat(value.get(ZONE_1_ID).getVersion()).isNull(); + assertThat(value.get(ZONE_1_ID).getTs()).isEqualTo(364L); + assertThat(value.get(ZONE_1_ID).getPerimeterDefinition()) + .isEqualTo(JacksonUtil.fromString(differentValueNewVersionIsNull.getJsonValue().get(), PerimeterDefinition.class)); + + assertThat(value.get(ZONE_2_ID).getVersion()).isEqualTo(155L); + assertThat(value.get(ZONE_2_ID).getTs()).isEqualTo(363L); + assertThat(value.get(ZONE_2_ID).getPerimeterDefinition()) + .isEqualTo(JacksonUtil.fromString(restrictedZoneAttributeKvEntry.getJsonValue().get(), PerimeterDefinition.class)); + } + + @Test + @SuppressWarnings("unchecked") + void testUpdateEntryWhenNewVersionIsGreaterThanCurrent() { + BaseAttributeKvEntry differentValueNewVersionIsSet = new BaseAttributeKvEntry(new JsonDataEntry("zone", """ + {"type":"POLYGON","polygonsDefinition":"[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"}"""), 364L, 156L); + var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsSet, ZONE_2_ID, restrictedZoneAttributeKvEntry)); + + assertThat(entry.updateEntry(updated)).isTrue(); + assertThat(entry.getValue()).isInstanceOf(Map.class); + + Map value = (Map) entry.getValue(); + assertThat(value).hasSize(2); + assertThat(value.get(ZONE_1_ID).getVersion()).isEqualTo(156L); + assertThat(value.get(ZONE_1_ID).getTs()).isEqualTo(364L); + assertThat(value.get(ZONE_1_ID).getPerimeterDefinition()) + .isEqualTo(JacksonUtil.fromString(differentValueNewVersionIsSet.getJsonValue().get(), PerimeterDefinition.class)); + + assertThat(value.get(ZONE_2_ID).getVersion()).isEqualTo(155L); + assertThat(value.get(ZONE_2_ID).getTs()).isEqualTo(363L); + assertThat(value.get(ZONE_2_ID).getPerimeterDefinition()) + .isEqualTo(JacksonUtil.fromString(restrictedZoneAttributeKvEntry.getJsonValue().get(), PerimeterDefinition.class)); + } + + @Test + void testUpdateEntryWhenNewVersionIsLessThanCurrent() { + BaseAttributeKvEntry differentValueNewVersionIsSet = new BaseAttributeKvEntry(new JsonDataEntry("zone", """ + {"type":"POLYGON","polygonsDefinition":"[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"}"""), 364L, 154L); + var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsSet, ZONE_2_ID, restrictedZoneAttributeKvEntry)); + + assertThat(entry.updateEntry(updated)).isFalse(); + } + + @Test + void testUpdateEntryWhenNewTsAndVersionIsGreaterThenCurrentAndValueWasNotChanged() { + BaseAttributeKvEntry newTsAndTheSameValue = new BaseAttributeKvEntry(allowedZoneDataEntry, 364L, 156L); + var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, newTsAndTheSameValue, ZONE_2_ID, restrictedZoneAttributeKvEntry)); + + assertThat(entry.updateEntry(updated)).isTrue(); + } + + @Test + void testUpdateEntryWithOldTs() { + BaseAttributeKvEntry oldTsAndTheSameValue = new BaseAttributeKvEntry(allowedZoneDataEntry, 362L, 156L); + var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, oldTsAndTheSameValue, ZONE_2_ID, restrictedZoneAttributeKvEntry)); + + assertThat(entry.updateEntry(updated)).isFalse(); + } + + @Test + void testUpdateEntryWithNewZone() { + final AssetId NEW_ZONE_ID = new AssetId(UUID.fromString("a3eacf1a-6af3-4e9f-87c4-502bb25c7dc3")); + BaseAttributeKvEntry newZone = new BaseAttributeKvEntry(new JsonDataEntry("zone", """ + {"type":"POLYGON","polygonsDefinition":"[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"}"""), 364L, 156L); + var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, allowedZoneAttributeKvEntry, ZONE_2_ID, restrictedZoneAttributeKvEntry, NEW_ZONE_ID, newZone)); + assertThat(entry.updateEntry(updated)).isTrue(); + } + + @Test + void testIsEmpty() { + GeofencingArgumentEntry geofencingArgumentEntry = new GeofencingArgumentEntry(); + assertThat(geofencingArgumentEntry.isEmpty()).isTrue(); + } + + @Test + void testIsEmptyWithEmptyMap() { + GeofencingArgumentEntry geofencingArgumentEntry = new GeofencingArgumentEntry(Map.of()); + assertThat(geofencingArgumentEntry.isEmpty()).isTrue(); + } + + @Test + void testInvalidKvEntryDataTypeForZoneResultInEmptyArgument() { + BaseAttributeKvEntry invalidZoneEntry = new BaseAttributeKvEntry(new StringDataEntry("zone", "someString"), 363L, 155L); + GeofencingArgumentEntry geofencingArgumentEntry = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, invalidZoneEntry)); + assertThat(geofencingArgumentEntry.isEmpty()).isTrue(); + } + + @Test + void testNotParsableToPerimeterJsonKvEntryResultInEmptyArgument() { + BaseAttributeKvEntry invalidZoneEntry = new BaseAttributeKvEntry(new JsonDataEntry("zone", "\"{}\""), 363L, 155L); + GeofencingArgumentEntry geofencingArgumentEntry = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, invalidZoneEntry)); + assertThat(geofencingArgumentEntry.isEmpty()).isTrue(); + } + + // TODO: should we test to TBEL logic? + +} From d5f78e6db06fd670064077a7eba8c433c5bf5122 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Tue, 12 Aug 2025 11:49:55 +0300 Subject: [PATCH 041/216] Added test for GeofencingZoneState --- .../cf/ctx/state/GeofencingZoneStateTest.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java new file mode 100644 index 0000000000..fe3c2eff16 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java @@ -0,0 +1,86 @@ +/** + * 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.service.cf.ctx.state; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.thingsboard.common.util.geo.Coordinates; +import org.thingsboard.server.common.data.id.AssetId; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GeofencingZoneStateTest { + + private final AssetId ZONE_ID = new AssetId(UUID.fromString("628730fd-d625-417f-9c6d-ae9fe4addbdb")); + private final String POLYGON = """ + {"type":"POLYGON","polygonsDefinition":"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"} + """; + + private GeofencingZoneState state; + + @BeforeEach + void setUp() { + state = new GeofencingZoneState(ZONE_ID, new BaseAttributeKvEntry(new JsonDataEntry("zone", POLYGON), 100L, 1L)); + } + + @Test + void evaluate_initialInside_thenInsideAgain() { + var inside = new Coordinates(50.4730, 30.5050); + // first evaluation: no prior state -> ENTERED + assertThat(state.evaluate(inside)).isEqualTo(GeofencingEvent.ENTERED); + // same position again -> INSIDE (steady state) + assertThat(state.evaluate(inside)).isEqualTo(GeofencingEvent.INSIDE); + } + + @Test + void evaluate_initialOutside_thenOutsideAgain() { + var outside = new Coordinates(50.4760, 30.5110); + // first evaluation: no prior state -> OUTSIDE + assertThat(state.evaluate(outside)).isEqualTo(GeofencingEvent.OUTSIDE); + // same position again -> OUTSIDE (steady state) + assertThat(state.evaluate(outside)).isEqualTo(GeofencingEvent.OUTSIDE); + } + + @Test + void evaluate_inside_thenLeave() { + var inside = new Coordinates(50.4730, 30.5050); + var outside = new Coordinates(50.4760, 30.5110); + // enter + assertThat(state.evaluate(inside)).isEqualTo(GeofencingEvent.ENTERED); + // leave -> LEFT + assertThat(state.evaluate(outside)).isEqualTo(GeofencingEvent.LEFT); + // still outside -> OUTSIDE + assertThat(state.evaluate(outside)).isEqualTo(GeofencingEvent.OUTSIDE); + } + + @Test + void evaluate_outside_thenEnter() { + var outside = new Coordinates(50.4760, 30.5110); + var inside = new Coordinates(50.4730, 30.5050); + // start outside + assertThat(state.evaluate(outside)).isEqualTo(GeofencingEvent.OUTSIDE); + // cross boundary -> ENTERED + assertThat(state.evaluate(inside)).isEqualTo(GeofencingEvent.ENTERED); + // remain inside -> INSIDE + assertThat(state.evaluate(inside)).isEqualTo(GeofencingEvent.INSIDE); + } + +} From 40276c6c1582484e258c8341804b058211136698 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Tue, 12 Aug 2025 12:58:11 +0300 Subject: [PATCH 042/216] Added new integration test --- .../cf/CalculatedFieldIntegrationTest.java | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index c8b8b0244b..da40cd4c5d 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.cf; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Test; @@ -29,22 +30,33 @@ import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.CFArgumentDynamicSourceType; +import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; +import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.controller.CalculatedFieldControllerTest; import org.thingsboard.server.dao.service.DaoSqlTest; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DaoSqlTest public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTest { @@ -606,6 +618,139 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); } + @Test + public void testGeofencingCalculatedField_SingleZonePerGroup() throws Exception { + // --- Arrange entities --- + Device device = createDevice("GF Device", "sn-geo-1"); + + // Allowed zone polygon (square) + String allowedPolygon = """ + {"type":"POLYGON","polygonsDefinition":"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"} + """; + // Restricted zone polygon (square) + String restrictedPolygon = """ + {"type":"POLYGON","polygonsDefinition":"[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"} + """; + + Asset allowedZoneAsset = createAsset("Allowed Zone", null); + doPost("/api/plugins/telemetry/ASSET/" + allowedZoneAsset.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, + JacksonUtil.toJsonNode("{\"zone\":" + allowedPolygon + "}")).andExpect(status().isOk());; + + Asset restrictedZoneAsset = createAsset("Restricted Zone", null); + doPost("/api/plugins/telemetry/ASSET/" + restrictedZoneAsset.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, + JacksonUtil.toJsonNode("{\"zone\":" + restrictedPolygon + "}")).andExpect(status().isOk());; + + // Relations from device to zones + EntityRelation deviceToAllowedZoneRelation = new EntityRelation(); + deviceToAllowedZoneRelation.setFrom(device.getId()); + deviceToAllowedZoneRelation.setTo(allowedZoneAsset.getId()); + deviceToAllowedZoneRelation.setType("AllowedZone"); + + EntityRelation deviceToRestrictedZoneRelation = new EntityRelation(); + deviceToRestrictedZoneRelation.setFrom(device.getId()); + deviceToRestrictedZoneRelation.setTo(restrictedZoneAsset.getId()); + deviceToRestrictedZoneRelation.setType("RestrictedZone"); + + doPost("/api/relation", deviceToAllowedZoneRelation).andExpect(status().isOk()); + doPost("/api/relation", deviceToRestrictedZoneRelation).andExpect(status().isOk()); + + // Initial device coordinates (inside Allowed, outside Restricted) + doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", + JacksonUtil.toJsonNode("{\"latitude\":50.4730,\"longitude\":30.5050}")); + + // --- Build CF: GEOFENCING --- + CalculatedField cf = new CalculatedField(); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("Geofencing CF"); + cf.setDebugSettings(DebugSettings.off()); + + GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); + + // Coordinates: TS_LATEST on the device + Argument lat = new Argument(); + lat.setRefEntityKey(new ReferencedEntityKey("latitude", ArgumentType.TS_LATEST, null)); + Argument lon = new Argument(); + lon.setRefEntityKey(new ReferencedEntityKey("longitude", ArgumentType.TS_LATEST, null)); + + // Zone groups: ATTRIBUTE on specific assets (one zone per group) + Argument allowedZones = new Argument(); + var allowedZonesRefDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); + allowedZonesRefDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); + allowedZonesRefDynamicSourceConfiguration.setRelationType("AllowedZone"); + allowedZonesRefDynamicSourceConfiguration.setMaxLevel(1); + allowedZonesRefDynamicSourceConfiguration.setFetchLastLevelOnly(true); + allowedZones.setRefEntityKey(new ReferencedEntityKey("zone", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + allowedZones.setRefDynamicSource(CFArgumentDynamicSourceType.RELATION_QUERY); + allowedZones.setRefDynamicSourceConfiguration(allowedZonesRefDynamicSourceConfiguration); + + Argument restrictedZones = new Argument(); + var restrictedZonesRefDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); + restrictedZonesRefDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); + restrictedZonesRefDynamicSourceConfiguration.setRelationType("RestrictedZone"); + restrictedZonesRefDynamicSourceConfiguration.setMaxLevel(1); + restrictedZonesRefDynamicSourceConfiguration.setFetchLastLevelOnly(true); + restrictedZones.setRefEntityKey(new ReferencedEntityKey("zone", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + restrictedZones.setRefDynamicSource(CFArgumentDynamicSourceType.RELATION_QUERY); + restrictedZones.setRefDynamicSourceConfiguration(restrictedZonesRefDynamicSourceConfiguration); + + cfg.setArguments(Map.of( + GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY, lat, + GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY, lon, + "allowedZones", allowedZones, + "restrictedZones", restrictedZones + )); + + // Zone group reporting config + List reportEvents = Arrays.stream(GeofencingEvent.values()).toList(); + + GeofencingZoneGroupConfiguration allowedCfg = new GeofencingZoneGroupConfiguration("allowedZone", reportEvents); + GeofencingZoneGroupConfiguration restrictedCfg = new GeofencingZoneGroupConfiguration("restrictedZone", reportEvents); + + cfg.setGeofencingZoneGroupConfigurations(Map.of( + "allowedZones", allowedCfg, + "restrictedZones", restrictedCfg + )); + + // Output to server attributes + Output out = new Output(); + out.setType(OutputType.ATTRIBUTES); + out.setScope(AttributeScope.SERVER_SCOPE); + cfg.setOutput(out); + + cf.setConfiguration(cfg); + + doPost("/api/calculatedField", cf, CalculatedField.class); + + // --- Assert initial evaluation (ENTERED / OUTSIDE) --- + await().alias("initial geofencing evaluation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = getServerAttributes(device.getId(), "allowedZoneEvent", "restrictedZoneEvent"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZoneEvent", "ENTERED") + .containsEntry("restrictedZoneEvent", "OUTSIDE"); + }); + + // --- Move device into Restricted zone (and outside Allowed) --- + doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", + JacksonUtil.toJsonNode("{\"latitude\":50.4760,\"longitude\":30.5110}")); + + // --- Assert transition (LEFT / ENTERED) --- + await().alias("transition evaluation after movement") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = getServerAttributes(device.getId(), "allowedZoneEvent", "restrictedZoneEvent"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZoneEvent", "LEFT") + .containsEntry("restrictedZoneEvent", "ENTERED"); + }); + } + private ObjectNode getLatestTelemetry(EntityId entityId, String... keys) throws Exception { return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/timeseries?keys=" + String.join(",", keys), ObjectNode.class); } @@ -621,4 +766,12 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes return doPost("/api/asset", asset, Asset.class); } + private static Map kv(ArrayNode attrs) { + Map m = new HashMap<>(); + for (JsonNode n : attrs) { + m.put(n.get("key").asText(), n.get("value").asText()); + } + return m; + } + } From bc789884430ab0b484319db49116255d74512b53 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Tue, 12 Aug 2025 13:06:46 +0300 Subject: [PATCH 043/216] Removed no used dynamicEntityArguments from ctx --- .../server/service/cf/ctx/state/CalculatedFieldCtx.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index d77413840e..334ec18266 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -60,7 +60,6 @@ public class CalculatedFieldCtx { private final Map arguments; private final Map mainEntityArguments; private final Map> linkedEntityArguments; - private final Map dynamicEntityArguments; private final List argNames; private Output output; private String expression; @@ -88,13 +87,13 @@ public class CalculatedFieldCtx { this.arguments = configuration.getArguments(); this.mainEntityArguments = new HashMap<>(); this.linkedEntityArguments = new HashMap<>(); - this.dynamicEntityArguments = new HashMap<>(); for (Map.Entry entry : arguments.entrySet()) { var refId = entry.getValue().getRefEntityId(); var refKey = entry.getValue().getRefEntityKey(); if (refId == null && entry.getValue().getRefDynamicSource() != null) { - dynamicEntityArguments.put(refKey, entry.getKey()); - } else if (refId == null || refId.equals(calculatedField.getEntityId())) { + continue; + } + if (refId == null || refId.equals(calculatedField.getEntityId())) { mainEntityArguments.put(refKey, entry.getKey()); } else { linkedEntityArguments.computeIfAbsent(refId, key -> new HashMap<>()).put(refKey, entry.getKey()); From 9641461541172b4ec3d21d306870ecbb2e457627 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Tue, 12 Aug 2025 13:12:44 +0300 Subject: [PATCH 044/216] rollback new methods created sicne not used after refactoring --- ...CalculatedFieldEntityMessageProcessor.java | 30 +++++++------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 2da66afe31..8b62c5b6ba 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -59,9 +59,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; -import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; @@ -295,25 +293,17 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM CalculatedFieldState state = states.get(ctx.getCfId()); if (state != null) { return state; + } else { + ListenableFuture stateFuture = cfService.fetchStateFromDb(ctx, entityId); + // Ugly but necessary. We do not expect to often fetch data from DB. Only once per pair lifetime. + // This call happens while processing the CF pack from the queue consumer. So the timeout should be relatively low. + // Alternatively, we can fetch the state outside the actor system and push separate command to create this actor, + // but this will significantly complicate the code. + state = stateFuture.get(1, TimeUnit.MINUTES); + state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); + states.put(ctx.getCfId(), state); + return state; } - return updateStateFromDb(ctx); - } - - private CalculatedFieldState updateStateFromDb(CalculatedFieldCtx ctx) throws InterruptedException, ExecutionException, TimeoutException { - CalculatedFieldState stateFromDb = getStateFromDb(ctx); - states.put(ctx.getCfId(), stateFromDb); - return stateFromDb; - } - - private CalculatedFieldState getStateFromDb(CalculatedFieldCtx ctx) throws InterruptedException, ExecutionException, TimeoutException { - ListenableFuture stateFuture = cfService.fetchStateFromDb(ctx, entityId); - // Ugly but necessary. We do not expect to often fetch data from DB. Only once per pair lifetime. - // This call happens while processing the CF pack from the queue consumer. So the timeout should be relatively low. - // Alternatively, we can fetch the state outside the actor system and push separate command to create this actor, - // but this will significantly complicate the code. - CalculatedFieldState state = stateFuture.get(1, TimeUnit.MINUTES); - state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); - return state; } private void processStateIfReady(CalculatedFieldCtx ctx, List cfIdList, CalculatedFieldState state, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException { From ef9ad6751c7e3d371cd462bfec6bf0605e179572 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Tue, 12 Aug 2025 13:34:17 +0300 Subject: [PATCH 045/216] Fixes after self-review before create WIP PR --- .../CalculatedFieldEntityMessageProcessor.java | 2 +- application/src/main/resources/logback.xml | 3 ++- .../CfArgumentDynamicSourceConfiguration.java | 8 -------- .../RelationQueryDynamicSourceConfiguration.java | 2 -- common/proto/src/main/proto/queue.proto | 2 +- 5 files changed, 4 insertions(+), 13 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 8b62c5b6ba..74474e8a6d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -302,8 +302,8 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM state = stateFuture.get(1, TimeUnit.MINUTES); state.checkStateSize(new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId), ctx.getMaxStateSize()); states.put(ctx.getCfId(), state); - return state; } + return state; } private void processStateIfReady(CalculatedFieldCtx ctx, List cfIdList, CalculatedFieldState state, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException { diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml index 5478d65d93..f5a8d47df1 100644 --- a/application/src/main/resources/logback.xml +++ b/application/src/main/resources/logback.xml @@ -56,7 +56,8 @@ - + + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java index 7af6283536..3fe432917b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java @@ -19,8 +19,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.relation.EntityRelationsQuery; @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -38,10 +36,4 @@ public interface CfArgumentDynamicSourceConfiguration { default void validate() {} - @JsonIgnore - boolean isSimpleRelation(); - - @JsonIgnore - EntityRelationsQuery toEntityRelationsQuery(EntityId rootEntityId); - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java index a75b7994ba..fdf8815591 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java @@ -55,12 +55,10 @@ public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynami } } - @Override public boolean isSimpleRelation() { return maxLevel == 1 && CollectionsUtil.isEmpty(entityTypes); } - @Override public EntityRelationsQuery toEntityRelationsQuery(EntityId rootEntityId) { if (isSimpleRelation()) { throw new IllegalArgumentException("Entity relations query can't be created for a simple relation!"); diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 10ffb10793..2b6e0701d5 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -910,7 +910,7 @@ message GeofencingZoneProto { message GeofencingArgumentProto { string argName = 1; - repeated GeofencingZoneProto zones = 4; + repeated GeofencingZoneProto zones = 2; } message CalculatedFieldStateProto { From 06515a5e13cfc34859534f14e41dc48565d020a4 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Tue, 12 Aug 2025 16:23:38 +0300 Subject: [PATCH 046/216] Renamed actor message types and java fields for clarity --- ...latedFieldDynamicArgumentsRefreshMsg.java} | 4 +- .../CalculatedFieldEntityActor.java | 4 +- ...CalculatedFieldEntityMessageProcessor.java | 4 +- .../CalculatedFieldManagerActor.java | 4 +- ...alculatedFieldManagerMessageProcessor.java | 56 +++++++++---------- ...latedFieldDynamicArgumentsRefreshMsg.java} | 4 +- .../server/common/msg/MsgType.java | 4 +- 7 files changed, 38 insertions(+), 42 deletions(-) rename application/src/main/java/org/thingsboard/server/actors/calculatedField/{CalculatedFieldScheduledInvalidationMsg.java => CalculatedFieldDynamicArgumentsRefreshMsg.java} (87%) rename application/src/main/java/org/thingsboard/server/actors/calculatedField/{EntityCalculatedFieldMarkStateDirtyMsg.java => EntityCalculatedFieldDynamicArgumentsRefreshMsg.java} (87%) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldScheduledInvalidationMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldDynamicArgumentsRefreshMsg.java similarity index 87% rename from application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldScheduledInvalidationMsg.java rename to application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldDynamicArgumentsRefreshMsg.java index 08a2394119..301fe22dfb 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldScheduledInvalidationMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldDynamicArgumentsRefreshMsg.java @@ -22,14 +22,14 @@ import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; @Data -public class CalculatedFieldScheduledInvalidationMsg implements ToCalculatedFieldSystemMsg { +public class CalculatedFieldDynamicArgumentsRefreshMsg implements ToCalculatedFieldSystemMsg { private final TenantId tenantId; private final CalculatedFieldId cfId; @Override public MsgType getMsgType() { - return MsgType.CF_SCHEDULED_INVALIDATION_MSG; + return MsgType.CF_DYNAMIC_ARGUMENTS_REFRESH_MSG; } } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java index 4879fa4566..2a5f3c3cfd 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java @@ -75,8 +75,8 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor { case CF_LINKED_TELEMETRY_MSG: processor.process((EntityCalculatedFieldLinkedTelemetryMsg) msg); break; - case CF_ENTITY_MARK_STATE_DIRTY_MSG: - processor.process((EntityCalculatedFieldMarkStateDirtyMsg) msg); + case CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG: + processor.process((EntityCalculatedFieldDynamicArgumentsRefreshMsg) msg); break; default: return false; diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 74474e8a6d..4b277eb3a3 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -226,8 +226,8 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } - public void process(EntityCalculatedFieldMarkStateDirtyMsg msg) throws CalculatedFieldException { - log.debug("[{}][{}] Processing entity CF invalidation msg.", entityId, msg.getCfId()); + public void process(EntityCalculatedFieldDynamicArgumentsRefreshMsg msg) throws CalculatedFieldException { + log.debug("[{}][{}] Processing CF dynamic arguments refresh msg.", entityId, msg.getCfId()); CalculatedFieldState currentState = states.get(msg.getCfId()); if (currentState == null) { log.debug("[{}][{}] Failed to find CF state for entity.", entityId, msg.getCfId()); diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java index 8494fb3847..c752333f69 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java @@ -91,8 +91,8 @@ public class CalculatedFieldManagerActor extends AbstractCalculatedFieldActor { case CF_LINKED_TELEMETRY_MSG: processor.onLinkedTelemetryMsg((CalculatedFieldLinkedTelemetryMsg) msg); break; - case CF_SCHEDULED_INVALIDATION_MSG: - processor.onScheduledInvalidationMsg((CalculatedFieldScheduledInvalidationMsg) msg); + case CF_DYNAMIC_ARGUMENTS_REFRESH_MSG: + processor.onDynamicArgumentsRefreshMsg((CalculatedFieldDynamicArgumentsRefreshMsg) msg); break; default: return false; diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index 76acdc329b..8a8fc43503 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -76,7 +76,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private final Map calculatedFields = new HashMap<>(); private final Map> entityIdCalculatedFields = new HashMap<>(); private final Map> entityIdCalculatedFieldLinks = new HashMap<>(); - private final Map> cfInvalidationScheduledTasks = new ConcurrentHashMap<>(); + private final Map> cfDynamicArgumentsRefreshTasks = new ConcurrentHashMap<>(); private final CalculatedFieldProcessingService cfExecService; private final CalculatedFieldStateService cfStateService; @@ -115,8 +115,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware calculatedFields.clear(); entityIdCalculatedFields.clear(); entityIdCalculatedFieldLinks.clear(); - cfInvalidationScheduledTasks.values().forEach(future -> future.cancel(true)); - cfInvalidationScheduledTasks.clear(); + cfDynamicArgumentsRefreshTasks.values().forEach(future -> future.cancel(true)); + cfDynamicArgumentsRefreshTasks.clear(); ctx.stop(ctx.getSelf()); } @@ -146,7 +146,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware // We use copy on write lists to safely pass the reference to another actor for the iteration. // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx); - scheduleCalculatedFieldInvalidationMsgIfNeeded(cfCtx); + scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx); msg.getCallback().onSuccess(); } @@ -339,7 +339,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware boolean hasSchedulingConfigChanges = newCfCtx.hasSchedulingConfigChanges(oldCfCtx); if (hasSchedulingConfigChanges) { - cancelCfScheduledInvalidationTaskIfExists(cfId, false); + cancelCfDynamicArgumentsRefreshTaskIfExists(cfId, false); } List newCfList = new CopyOnWriteArrayList<>(); @@ -382,7 +382,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx); deleteLinks(cfCtx); - cancelCfScheduledInvalidationTaskIfExists(cfId, true); + cancelCfDynamicArgumentsRefreshTaskIfExists(cfId, true); EntityId entityId = cfCtx.getEntityId(); EntityType entityType = cfCtx.getEntityId().getEntityType(); @@ -407,12 +407,12 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } } - private void cancelCfScheduledInvalidationTaskIfExists(CalculatedFieldId cfId, boolean cfDeleted) { - var existingTask = cfInvalidationScheduledTasks.remove(cfId); + private void cancelCfDynamicArgumentsRefreshTaskIfExists(CalculatedFieldId cfId, boolean cfDeleted) { + var existingTask = cfDynamicArgumentsRefreshTasks.remove(cfId); if (existingTask != null) { existingTask.cancel(false); String reason = cfDeleted ? "deletion" : "update"; - log.debug("[{}][{}] Cancelled scheduled invalidation task due to CF " + reason + "!", tenantId, cfId); + log.debug("[{}][{}] Cancelled dynamic arguments refresh task due to CF " + reason + "!", tenantId, cfId); } } @@ -455,7 +455,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware var targetEntityId = link.entityId(); var targetEntityType = targetEntityId.getEntityType(); var cf = calculatedFields.get(link.cfId()); - if (EntityType.DEVICE_PROFILE.equals(targetEntityType) || EntityType.ASSET_PROFILE.equals(targetEntityType)) { + if (isProfileEntity(targetEntityType)) { // iterate over all entities that belong to profile and push the message for corresponding CF var entityIds = entityProfileCache.getEntityIdsByProfileId(targetEntityId); if (!entityIds.isEmpty()) { @@ -518,7 +518,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private void initCf(CalculatedFieldCtx cfCtx, TbCallback callback, boolean forceStateReinit) { EntityId entityId = cfCtx.getEntityId(); EntityType entityType = cfCtx.getEntityId().getEntityType(); - scheduleCalculatedFieldInvalidationMsgIfNeeded(cfCtx); + scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx); if (isProfileEntity(entityType)) { var entityIds = entityProfileCache.getEntityIdsByProfileId(entityId); if (!entityIds.isEmpty()) { @@ -536,31 +536,31 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } } - private void scheduleCalculatedFieldInvalidationMsgIfNeeded(CalculatedFieldCtx cfCtx) { + private void scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(CalculatedFieldCtx cfCtx) { CalculatedField cf = cfCtx.getCalculatedField(); CalculatedFieldConfiguration cfConfig = cf.getConfiguration(); if (!cfConfig.isScheduledUpdateEnabled()) { return; } - if (cfInvalidationScheduledTasks.containsKey(cf.getId())) { - log.debug("[{}][{}] Scheduled invalidation task for CF already exists!", tenantId, cf.getId()); + if (cfDynamicArgumentsRefreshTasks.containsKey(cf.getId())) { + log.debug("[{}][{}] Dynamic arguments refresh task for CF already exists!", tenantId, cf.getId()); return; } long refreshDynamicSourceInterval = TimeUnit.SECONDS.toMillis(cfConfig.getScheduledUpdateIntervalSec()); - var scheduledMsg = new CalculatedFieldScheduledInvalidationMsg(tenantId, cfCtx.getCfId()); + var scheduledMsg = new CalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfCtx.getCfId()); ScheduledFuture scheduledFuture = systemContext .schedulePeriodicMsgWithDelay(ctx, scheduledMsg, refreshDynamicSourceInterval, refreshDynamicSourceInterval); - cfInvalidationScheduledTasks.put(cf.getId(), scheduledFuture); - log.debug("[{}][{}] Scheduled invalidation task for CF!", tenantId, cf.getId()); + cfDynamicArgumentsRefreshTasks.put(cf.getId(), scheduledFuture); + log.debug("[{}][{}] Scheduled dynamic arguments refresh task for CF!", tenantId, cf.getId()); } - public void onScheduledInvalidationMsg(CalculatedFieldScheduledInvalidationMsg msg) { - log.debug("[{}] [{}] Processing CF scheduled invalidation msg.", tenantId, msg.getCfId()); + public void onDynamicArgumentsRefreshMsg(CalculatedFieldDynamicArgumentsRefreshMsg msg) { + log.debug("[{}] [{}] Processing CF dynamic arguments refresh task.", tenantId, msg.getCfId()); CalculatedFieldCtx cfCtx = calculatedFields.get(msg.getCfId()); if (cfCtx == null) { - log.debug("[{}][{}] Failed to find CF context, going to stop scheduled invalidations for CF.", tenantId, msg.getCfId()); - cancelCfScheduledInvalidationTaskIfExists(msg.getCfId(), true); + log.debug("[{}][{}] Failed to find CF context, going to stop dynamic arguments refresh task for CF.", tenantId, msg.getCfId()); + cancelCfDynamicArgumentsRefreshTaskIfExists(msg.getCfId(), true); return; } EntityId entityId = cfCtx.getEntityId(); @@ -571,7 +571,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware var multiCallback = new MultipleTbCallback(entityIds.size(), msg.getCallback()); entityIds.forEach(id -> { if (isMyPartition(id, multiCallback)) { - InitCfInvalidationForEntity(id, msg.getCfId(), multiCallback); + dynamicArgumentsRefreshForEntity(id, msg.getCfId(), multiCallback); } }); } else { @@ -579,14 +579,14 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } } else { if (isMyPartition(entityId, msg.getCallback())) { - InitCfInvalidationForEntity(entityId, msg.getCfId(), msg.getCallback()); + dynamicArgumentsRefreshForEntity(entityId, msg.getCfId(), msg.getCallback()); } } } - private void InitCfInvalidationForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) { - log.debug("Pushing entity CF invalidation msg to specific actor [{}]", entityId); - getOrCreateActor(entityId).tell(new EntityCalculatedFieldMarkStateDirtyMsg(tenantId, cfId, callback)); + private void dynamicArgumentsRefreshForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) { + log.debug("Pushing CF dynamic arguments refresh msg to specific actor [{}]", entityId); + getOrCreateActor(entityId).tell(new EntityCalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfId, callback)); } private void deleteCfForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) { @@ -652,10 +652,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware log.error("Failed to process calculated field record: {}", cf, e); } }); - // TODO: why we need to do this loop if we do this inside the onFieldInitMsg? - calculatedFields.values().forEach(cf -> { - entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cf); - }); PageDataIterable cfls = new PageDataIterable<>(pageLink -> cfDaoService.findAllCalculatedFieldLinksByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize()); cfls.forEach(link -> { onLinkInitMsg(new CalculatedFieldLinkInitMsg(link.getTenantId(), link)); diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldMarkStateDirtyMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldDynamicArgumentsRefreshMsg.java similarity index 87% rename from application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldMarkStateDirtyMsg.java rename to application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldDynamicArgumentsRefreshMsg.java index ef9864aa83..fdf864611f 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldMarkStateDirtyMsg.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldDynamicArgumentsRefreshMsg.java @@ -23,7 +23,7 @@ import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; import org.thingsboard.server.common.msg.queue.TbCallback; @Data -public class EntityCalculatedFieldMarkStateDirtyMsg implements ToCalculatedFieldSystemMsg { +public class EntityCalculatedFieldDynamicArgumentsRefreshMsg implements ToCalculatedFieldSystemMsg { private final TenantId tenantId; private final CalculatedFieldId cfId; @@ -31,7 +31,7 @@ public class EntityCalculatedFieldMarkStateDirtyMsg implements ToCalculatedField @Override public MsgType getMsgType() { - return MsgType.CF_ENTITY_MARK_STATE_DIRTY_MSG; + return MsgType.CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG; } } diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java index fc0e2262bb..a2eb5303be 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java @@ -152,8 +152,8 @@ public enum MsgType { CF_ENTITY_INIT_CF_MSG, CF_ENTITY_DELETE_MSG, - CF_SCHEDULED_INVALIDATION_MSG, - CF_ENTITY_MARK_STATE_DIRTY_MSG; + CF_DYNAMIC_ARGUMENTS_REFRESH_MSG, + CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG; @Getter private final boolean ignoreOnStart; From ac3f81195d07f2fe2614268c3195d76041429b05 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Tue, 12 Aug 2025 17:27:10 +0300 Subject: [PATCH 047/216] Refactored code duplicates in CalculatedFieldManagerMessageProcessor --- ...alculatedFieldManagerMessageProcessor.java | 154 ++++++++---------- 1 file changed, 64 insertions(+), 90 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index 8a8fc43503..c07d2b538e 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -64,6 +64,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Function; import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto; @@ -378,33 +380,12 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware if (cfCtx == null) { log.debug("[{}] CF was already deleted [{}]", tenantId, cfId); callback.onSuccess(); - } else { - entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx); - deleteLinks(cfCtx); - - cancelCfDynamicArgumentsRefreshTaskIfExists(cfId, true); - - EntityId entityId = cfCtx.getEntityId(); - EntityType entityType = cfCtx.getEntityId().getEntityType(); - if (isProfileEntity(entityType)) { - var entityIds = entityProfileCache.getEntityIdsByProfileId(entityId); - if (!entityIds.isEmpty()) { - //TODO: no need to do this if we cache all created actors and know which one belong to us; - var multiCallback = new MultipleTbCallback(entityIds.size(), callback); - entityIds.forEach(id -> { - if (isMyPartition(id, multiCallback)) { - deleteCfForEntity(id, cfId, multiCallback); - } - }); - } else { - callback.onSuccess(); - } - } else { - if (isMyPartition(entityId, callback)) { - deleteCfForEntity(entityId, cfId, callback); - } - } + return; } + entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx); + deleteLinks(cfCtx); + cancelCfDynamicArgumentsRefreshTaskIfExists(cfId, true); + applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> deleteCfForEntity(id, cfId, cb)); } private void cancelCfDynamicArgumentsRefreshTaskIfExists(CalculatedFieldId cfId, boolean cfDeleted) { @@ -452,31 +433,10 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } for (var linkProto : linksList) { var link = fromProto(linkProto); - var targetEntityId = link.entityId(); - var targetEntityType = targetEntityId.getEntityType(); var cf = calculatedFields.get(link.cfId()); - if (isProfileEntity(targetEntityType)) { - // iterate over all entities that belong to profile and push the message for corresponding CF - var entityIds = entityProfileCache.getEntityIdsByProfileId(targetEntityId); - if (!entityIds.isEmpty()) { - MultipleTbCallback multipleCallback = new MultipleTbCallback(entityIds.size(), callback); - var newMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, multipleCallback); - entityIds.forEach(entityId -> { - if (isMyPartition(entityId, multipleCallback)) { - log.debug("Pushing linked telemetry msg to specific actor [{}]", entityId); - getOrCreateActor(entityId).tell(newMsg); - } - }); - } else { - callback.onSuccess(); - } - } else { - if (isMyPartition(targetEntityId, callback)) { - log.debug("Pushing linked telemetry msg to specific actor [{}]", targetEntityId); - var newMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, callback); - getOrCreateActor(targetEntityId).tell(newMsg); - } - } + applyToTargetCfEntityActors(link, callback, + cb -> new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, callback), + this::linkedTelemetryMsgForEntity); } } @@ -516,24 +476,8 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } private void initCf(CalculatedFieldCtx cfCtx, TbCallback callback, boolean forceStateReinit) { - EntityId entityId = cfCtx.getEntityId(); - EntityType entityType = cfCtx.getEntityId().getEntityType(); scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx); - if (isProfileEntity(entityType)) { - var entityIds = entityProfileCache.getEntityIdsByProfileId(entityId); - if (!entityIds.isEmpty()) { - var multiCallback = new MultipleTbCallback(entityIds.size(), callback); - entityIds.forEach(id -> { - if (isMyPartition(id, multiCallback)) { - initCfForEntity(id, cfCtx, forceStateReinit, multiCallback); - } - }); - } else { - callback.onSuccess(); - } - } else if (isMyPartition(entityId, callback)) { - initCfForEntity(entityId, cfCtx, forceStateReinit, callback); - } + applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> initCfForEntity(id, cfCtx, forceStateReinit, cb)); } private void scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(CalculatedFieldCtx cfCtx) { @@ -563,32 +507,19 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware cancelCfDynamicArgumentsRefreshTaskIfExists(msg.getCfId(), true); return; } - EntityId entityId = cfCtx.getEntityId(); - EntityType entityType = entityId.getEntityType(); - if (isProfileEntity(entityType)) { - var entityIds = entityProfileCache.getEntityIdsByProfileId(entityId); - if (!entityIds.isEmpty()) { - var multiCallback = new MultipleTbCallback(entityIds.size(), msg.getCallback()); - entityIds.forEach(id -> { - if (isMyPartition(id, multiCallback)) { - dynamicArgumentsRefreshForEntity(id, msg.getCfId(), multiCallback); - } - }); - } else { - msg.getCallback().onSuccess(); - } - } else { - if (isMyPartition(entityId, msg.getCallback())) { - dynamicArgumentsRefreshForEntity(entityId, msg.getCfId(), msg.getCallback()); - } - } + applyToTargetCfEntityActors(cfCtx, msg.getCallback(), (id, cb) -> refreshDynamicArgumentsForEntity(id, msg.getCfId(), cb)); } - private void dynamicArgumentsRefreshForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) { + private void refreshDynamicArgumentsForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) { log.debug("Pushing CF dynamic arguments refresh msg to specific actor [{}]", entityId); getOrCreateActor(entityId).tell(new EntityCalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfId, callback)); } + private void linkedTelemetryMsgForEntity(EntityId entityId, EntityCalculatedFieldLinkedTelemetryMsg msg) { + log.debug("Pushing linked telemetry msg to specific actor [{}]", entityId); + getOrCreateActor(entityId).tell(msg); + } + private void deleteCfForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) { log.debug("Pushing delete CF msg to specific actor [{}]", entityId); getOrCreateActor(entityId).tell(new CalculatedFieldEntityDeleteMsg(tenantId, cfId, callback)); @@ -653,9 +584,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } }); PageDataIterable cfls = new PageDataIterable<>(pageLink -> cfDaoService.findAllCalculatedFieldLinksByTenantId(tenantId, pageLink), cfSettings.getInitTenantFetchPackSize()); - cfls.forEach(link -> { - onLinkInitMsg(new CalculatedFieldLinkInitMsg(link.getTenantId(), link)); - }); + cfls.forEach(link -> onLinkInitMsg(new CalculatedFieldLinkInitMsg(link.getTenantId(), link))); } private void initEntityProfileCache() { @@ -679,4 +608,49 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } } + private void applyToTargetCfEntityActors(CalculatedFieldCtx calculatedFieldCtx, + TbCallback callback, + BiConsumer action) { + if (isProfileEntity(calculatedFieldCtx.getEntityId().getEntityType())) { + var ids = entityProfileCache.getEntityIdsByProfileId(calculatedFieldCtx.getEntityId()); + if (ids.isEmpty()) { + callback.onSuccess(); + return; + } + var multiCallback = new MultipleTbCallback(ids.size(), callback); + ids.forEach(id -> { + if (isMyPartition(id, multiCallback)) { + action.accept(id, multiCallback); + } + }); + return; + } + if (isMyPartition(calculatedFieldCtx.getEntityId(), callback)) { + action.accept(calculatedFieldCtx.getEntityId(), callback); + } + } + + private void applyToTargetCfEntityActors(CalculatedFieldEntityCtxId link, TbCallback callback, + Function messageFactory, BiConsumer action) { + if (isProfileEntity(link.entityId().getEntityType())) { + var ids = entityProfileCache.getEntityIdsByProfileId(link.entityId()); + if (ids.isEmpty()) { + callback.onSuccess(); + return; + } + var multiCallback = new MultipleTbCallback(ids.size(), callback); + var msg = messageFactory.apply(multiCallback); + ids.forEach(id -> { + if (isMyPartition(id, multiCallback)) { + action.accept(id, msg); + } + }); + return; + } + if (isMyPartition(link.entityId(), callback)) { + var msg = messageFactory.apply(callback); + action.accept(link.entityId(), msg); + } + } + } From 67f08da7a002f9e6daf6abfd19a661ca91fe13c2 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Tue, 12 Aug 2025 18:56:53 +0300 Subject: [PATCH 048/216] Updated validation logic for existing and geofencing CF --- ...faultCalculatedFieldProcessingService.java | 11 +++++----- .../cf/ctx/state/CalculatedFieldCtx.java | 2 +- .../cf/CalculatedFieldIntegrationTest.java | 3 --- .../GeofencingCalculatedFieldStateTest.java | 3 --- .../data/cf/configuration/Argument.java | 6 +++++- .../BaseCalculatedFieldConfiguration.java | 8 ++----- ...eofencingCalculatedFieldConfiguration.java | 21 ++++--------------- 7 files changed, 17 insertions(+), 37 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index ade1d3eefd..995180e507 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -35,7 +35,6 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.CFArgumentDynamicSourceType; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; @@ -162,7 +161,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP Set> entries = ctx.getArguments().entrySet(); if (dynamicArgumentsOnly) { entries = entries.stream() - .filter(entry -> CFArgumentDynamicSourceType.RELATION_QUERY.equals(entry.getValue().getRefDynamicSource())) + .filter(entry -> entry.getValue().hasDynamicSource()) .collect(Collectors.toSet()); } for (var entry : entries) { @@ -293,13 +292,13 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP if (value.getRefEntityId() != null) { return Futures.immediateFuture(List.of(value.getRefEntityId())); } - var refDynamicSource = value.getRefDynamicSource(); - if (refDynamicSource == null) { + if (!value.hasDynamicSource()) { return Futures.immediateFuture(List.of(entityId)); } - return switch (value.getRefDynamicSource()) { + var refDynamicSourceConfiguration = value.getRefDynamicSourceConfiguration(); + return switch (refDynamicSourceConfiguration.getType()) { case RELATION_QUERY -> { - var configuration = (RelationQueryDynamicSourceConfiguration) value.getRefDynamicSourceConfiguration(); + var configuration = (RelationQueryDynamicSourceConfiguration) refDynamicSourceConfiguration; if (configuration.isSimpleRelation()) { yield switch (configuration.getDirection()) { case FROM -> diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 334ec18266..a5d9fe4b3a 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -90,7 +90,7 @@ public class CalculatedFieldCtx { for (Map.Entry entry : arguments.entrySet()) { var refId = entry.getValue().getRefEntityId(); var refKey = entry.getValue().getRefEntityKey(); - if (refId == null && entry.getValue().getRefDynamicSource() != null) { + if (refId == null && entry.getValue().hasDynamicSource()) { continue; } if (refId == null || refId.equals(calculatedField.getEntityId())) { diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index da40cd4c5d..1980ea26cd 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -30,7 +30,6 @@ import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.CFArgumentDynamicSourceType; import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; @@ -681,7 +680,6 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes allowedZonesRefDynamicSourceConfiguration.setMaxLevel(1); allowedZonesRefDynamicSourceConfiguration.setFetchLastLevelOnly(true); allowedZones.setRefEntityKey(new ReferencedEntityKey("zone", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); - allowedZones.setRefDynamicSource(CFArgumentDynamicSourceType.RELATION_QUERY); allowedZones.setRefDynamicSourceConfiguration(allowedZonesRefDynamicSourceConfiguration); Argument restrictedZones = new Argument(); @@ -691,7 +689,6 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes restrictedZonesRefDynamicSourceConfiguration.setMaxLevel(1); restrictedZonesRefDynamicSourceConfiguration.setFetchLastLevelOnly(true); restrictedZones.setRefEntityKey(new ReferencedEntityKey("zone", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); - restrictedZones.setRefDynamicSource(CFArgumentDynamicSourceType.RELATION_QUERY); restrictedZones.setRefDynamicSourceConfiguration(restrictedZonesRefDynamicSourceConfiguration); cfg.setArguments(Map.of( diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index 1850efcd11..218742539c 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -27,7 +27,6 @@ import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.CFArgumentDynamicSourceType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; @@ -323,7 +322,6 @@ public class GeofencingCalculatedFieldStateTest { refDynamicSourceConfiguration3.setMaxLevel(1); refDynamicSourceConfiguration3.setFetchLastLevelOnly(true); argument3.setRefEntityKey(refEntityKey3); - argument3.setRefDynamicSource(CFArgumentDynamicSourceType.RELATION_QUERY); argument3.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration3); Argument argument4 = new Argument(); @@ -334,7 +332,6 @@ public class GeofencingCalculatedFieldStateTest { refDynamicSourceConfiguration4.setMaxLevel(1); refDynamicSourceConfiguration4.setFetchLastLevelOnly(true); argument4.setRefEntityKey(refEntityKey4); - argument4.setRefDynamicSource(CFArgumentDynamicSourceType.RELATION_QUERY); argument4.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration4); config.setArguments(Map.of("latitude", argument1, "longitude", argument2, "allowedZones", argument3, "restrictedZones", argument4)); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java index 6fc8c46961..3b8ec3308a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java @@ -26,7 +26,7 @@ public class Argument { @Nullable private EntityId refEntityId; - private CFArgumentDynamicSourceType refDynamicSource; + // TODO: add upgrade in PE version -> CFArgumentDynamicSourceType to CFArgumentDynamicSourceConfiguration private CfArgumentDynamicSourceConfiguration refDynamicSourceConfiguration; private ReferencedEntityKey refEntityKey; private String defaultValue; @@ -34,4 +34,8 @@ public class Argument { private Integer limit; private Long timeWindow; + public boolean hasDynamicSource() { + return refDynamicSourceConfiguration != null; + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java index 7053add5a7..ef6450f5b3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/BaseCalculatedFieldConfiguration.java @@ -58,14 +58,10 @@ public abstract class BaseCalculatedFieldConfiguration implements CalculatedFiel return link; } - // TODO: update validate method in PE version. @Override public void validate() { - boolean hasDynamicSourceRelationQuery = arguments.values() - .stream() - .anyMatch(arg -> CFArgumentDynamicSourceType.RELATION_QUERY.equals(arg.getRefDynamicSource())); - if (hasDynamicSourceRelationQuery) { - throw new IllegalArgumentException("Calculated field with type: '" + getType() + "' doesn't support arguments with 'RELATION_QUERY' dynamic source type!"); + if (arguments.values().stream().anyMatch(Argument::hasDynamicSource)) { + throw new IllegalArgumentException("Calculated field with type: '" + getType() + "' doesn't support dynamic source configuration!"); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java index 32cbe6baa3..39780f09a6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java @@ -27,8 +27,6 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; -import static org.thingsboard.server.common.data.cf.configuration.CFArgumentDynamicSourceType.RELATION_QUERY; - @Data @EqualsAndHashCode(callSuper = true) public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements CalculatedFieldConfiguration { @@ -55,7 +53,7 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC @Override public boolean isScheduledUpdateEnabled() { - return scheduledUpdateIntervalSec > 0 && arguments.values().stream().anyMatch(arg -> arg.getRefDynamicSource() != null); + return scheduledUpdateIntervalSec > 0 && arguments.values().stream().anyMatch(Argument::hasDynamicSource); } // TODO: update validate method in PE version. @@ -67,9 +65,6 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC if (arguments.size() < 3) { throw new IllegalArgumentException("Geofencing calculated field must contain at least 3 arguments!"); } - if (arguments.size() > 5) { - throw new IllegalArgumentException("Geofencing calculated field size exceeds limit of 5 arguments!"); - } validateCoordinateArguments(); Map zoneGroupsArguments = getZoneGroupArguments(); @@ -129,7 +124,7 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC if (!ArgumentType.TS_LATEST.equals(refEntityKey.getType())) { throw new IllegalArgumentException("Argument '" + coordinateKey + "' must be of type TS_LATEST."); } - if (argument.getRefDynamicSource() != null) { + if (argument.hasDynamicSource()) { throw new IllegalArgumentException("Dynamic source is not allowed for argument: '" + coordinateKey + "'."); } } @@ -144,17 +139,9 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC if (!ArgumentType.ATTRIBUTE.equals(refEntityKey.getType())) { throw new IllegalArgumentException("Argument '" + argumentKey + "' must be of type ATTRIBUTE."); } - var dynamicSource = argument.getRefDynamicSource(); - if (dynamicSource == null) { - return; - } - if (!RELATION_QUERY.equals(dynamicSource)) { - throw new IllegalArgumentException("Only relation query dynamic source is supported for argument: '" + argumentKey + "'."); - } - if (argument.getRefDynamicSourceConfiguration() == null) { - throw new IllegalArgumentException("Missing dynamic source configuration for argument: '" + argumentKey + "'."); + if (argument.hasDynamicSource()) { + argument.getRefDynamicSourceConfiguration().validate(); } - argument.getRefDynamicSourceConfiguration().validate(); }); } From dd18359c54776cd0090166ed358c26fd07e4b83a Mon Sep 17 00:00:00 2001 From: dshvaika Date: Tue, 12 Aug 2025 19:55:21 +0300 Subject: [PATCH 049/216] Replaced GeofencingZoneIdProto with EntityTypeProto and msb and lsb --- .../cf/ctx/state/GeofencingZoneState.java | 8 ++++++-- .../server/utils/CalculatedFieldUtils.java | 15 ++++----------- common/proto/src/main/proto/queue.proto | 16 ++++++---------- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java index 3182a342e8..12493152dc 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java @@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneProto; import java.util.UUID; @@ -52,8 +53,7 @@ public class GeofencingZoneState { } public GeofencingZoneState(GeofencingZoneProto proto) { - this.zoneId = EntityIdFactory.getByTypeAndUuid(proto.getZoneId().getType(), - new UUID(proto.getZoneId().getZoneIdMSB(), proto.getZoneId().getZoneIdLSB())); + this.zoneId = toZoneId(proto); this.ts = proto.getTs(); this.version = proto.getVersion(); this.perimeterDefinition = JacksonUtil.fromString(proto.getPerimeterDefinition(), PerimeterDefinition.class); @@ -94,4 +94,8 @@ public class GeofencingZoneState { return inside ? GeofencingEvent.INSIDE : GeofencingEvent.OUTSIDE; } + private EntityId toZoneId(GeofencingZoneProto proto) { + return EntityIdFactory.getByTypeAndUuid(ProtoUtils.fromProto(proto.getZoneType()), new UUID(proto.getZoneIdMSB(), proto.getZoneIdLSB())); + } + } diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java index eeb5318104..7658409662 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java @@ -24,11 +24,11 @@ import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.BasicKvEntry; import org.thingsboard.server.common.util.KvProtoUtil; +import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldEntityCtxIdProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldIdProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldStateProto; import org.thingsboard.server.gen.transport.TransportProtos.GeofencingArgumentProto; -import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneIdProto; import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneProto; import org.thingsboard.server.gen.transport.TransportProtos.SingleValueArgumentProto; import org.thingsboard.server.gen.transport.TransportProtos.TsDoubleValProto; @@ -132,7 +132,9 @@ public class CalculatedFieldUtils { private static GeofencingZoneProto toGeofencingZoneProto(EntityId entityId, GeofencingZoneState zoneState) { GeofencingZoneProto.Builder builder = GeofencingZoneProto.newBuilder() - .setZoneId(toGeofencingZoneIdProto(entityId)) + .setZoneType(ProtoUtils.toProto(entityId.getEntityType())) + .setZoneIdMSB(entityId.getId().getMostSignificantBits()) + .setZoneIdLSB(entityId.getId().getLeastSignificantBits()) .setTs(zoneState.getTs()) .setVersion(zoneState.getVersion()) .setPerimeterDefinition(JacksonUtil.toString(zoneState.getPerimeterDefinition())); @@ -142,15 +144,6 @@ public class CalculatedFieldUtils { return builder.build(); } - private static GeofencingZoneIdProto toGeofencingZoneIdProto(EntityId zoneId) { - return GeofencingZoneIdProto.newBuilder() - .setType(zoneId.getEntityType().name()) - .setZoneIdLSB(zoneId.getId().getLeastSignificantBits()) - .setZoneIdMSB(zoneId.getId().getMostSignificantBits()) - .build(); - } - - public static CalculatedFieldState fromProto(CalculatedFieldStateProto proto) { if (StringUtils.isEmpty(proto.getType())) { return null; diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index 2b6e0701d5..2ea1db2a0a 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -894,18 +894,14 @@ message TsRollingArgumentProto { repeated TsDoubleValProto tsValue = 4; } -message GeofencingZoneIdProto { - string type = 1; +message GeofencingZoneProto { + EntityTypeProto zoneType = 1; int64 zoneIdMSB = 2; int64 zoneIdLSB = 3; -} - -message GeofencingZoneProto { - GeofencingZoneIdProto zoneId = 1; - int64 ts = 2; - string perimeterDefinition = 3; - int64 version = 4; - optional bool inside = 5; + int64 ts = 4; + string perimeterDefinition = 5; + int64 version = 6; + optional bool inside = 7; } message GeofencingArgumentProto { From d2b9e1066f2a3f7ad2e0428538c1f176444952d3 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Wed, 13 Aug 2025 12:55:21 +0300 Subject: [PATCH 050/216] Added geofencing CF configuration test --- .../state/GeofencingCalculatedFieldState.java | 4 +- .../cf/CalculatedFieldIntegrationTest.java | 8 +- .../GeofencingCalculatedFieldStateTest.java | 8 +- .../cf/ctx/state/GeofencingZoneStateTest.java | 6 +- ...eofencingCalculatedFieldConfiguration.java | 42 +- ...ation.java => ZoneGroupConfiguration.java} | 2 +- ...ncingCalculatedFieldConfigurationTest.java | 472 ++++++++++++++++++ 7 files changed, 503 insertions(+), 39 deletions(-) rename common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/{GeofencingZoneGroupConfiguration.java => ZoneGroupConfiguration.java} (94%) create mode 100644 common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java index 3de0cc6c31..9e598db69a 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java @@ -135,7 +135,7 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { Coordinates entityCoordinates, GeofencingCalculatedFieldConfiguration configuration) { - var geofencingZoneGroupConfigurations = configuration.getGeofencingZoneGroupConfigurations(); + var geofencingZoneGroupConfigurations = configuration.getZoneGroupConfigurations(); Map zoneEventMap = new HashMap<>(); ObjectNode resultNode = JacksonUtil.newObjectNode(); @@ -184,7 +184,7 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { Coordinates entityCoordinates, GeofencingCalculatedFieldConfiguration configuration) { - var geofencingZoneGroupConfigurations = configuration.getGeofencingZoneGroupConfigurations(); + var geofencingZoneGroupConfigurations = configuration.getZoneGroupConfigurations(); ObjectNode resultNode = JacksonUtil.newObjectNode(); getGeofencingArguments().forEach((argumentKey, argumentEntry) -> { diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index 1980ea26cd..8a8d5e389e 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; -import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ZoneGroupConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; @@ -701,10 +701,10 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes // Zone group reporting config List reportEvents = Arrays.stream(GeofencingEvent.values()).toList(); - GeofencingZoneGroupConfiguration allowedCfg = new GeofencingZoneGroupConfiguration("allowedZone", reportEvents); - GeofencingZoneGroupConfiguration restrictedCfg = new GeofencingZoneGroupConfiguration("restrictedZone", reportEvents); + ZoneGroupConfiguration allowedCfg = new ZoneGroupConfiguration("allowedZone", reportEvents); + ZoneGroupConfiguration restrictedCfg = new ZoneGroupConfiguration("restrictedZone", reportEvents); - cfg.setGeofencingZoneGroupConfigurations(Map.of( + cfg.setZoneGroupConfigurations(Map.of( "allowedZones", allowedCfg, "restrictedZones", restrictedCfg )); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index 218742539c..0d7a2971d0 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; -import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ZoneGroupConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; @@ -337,9 +337,9 @@ public class GeofencingCalculatedFieldStateTest { config.setArguments(Map.of("latitude", argument1, "longitude", argument2, "allowedZones", argument3, "restrictedZones", argument4)); List reportEvents = Arrays.stream(GeofencingEvent.values()).toList(); - GeofencingZoneGroupConfiguration allowedZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", reportEvents); - GeofencingZoneGroupConfiguration restrictedZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("restrictedZone", reportEvents); - config.setGeofencingZoneGroupConfigurations(Map.of("allowedZones", allowedZoneGroupConfiguration, "restrictedZones", restrictedZoneGroupConfiguration)); + ZoneGroupConfiguration allowedZoneGroupConfiguration = new ZoneGroupConfiguration("allowedZone", reportEvents); + ZoneGroupConfiguration restrictedZoneGroupConfiguration = new ZoneGroupConfiguration("restrictedZone", reportEvents); + config.setZoneGroupConfigurations(Map.of("allowedZones", allowedZoneGroupConfiguration, "restrictedZones", restrictedZoneGroupConfiguration)); config.setCreateRelationsWithMatchedZones(true); config.setZoneRelationType("CurrentZone"); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java index fe3c2eff16..e31e62deef 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java @@ -30,14 +30,14 @@ import static org.assertj.core.api.Assertions.assertThat; public class GeofencingZoneStateTest { private final AssetId ZONE_ID = new AssetId(UUID.fromString("628730fd-d625-417f-9c6d-ae9fe4addbdb")); - private final String POLYGON = """ - {"type":"POLYGON","polygonsDefinition":"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"} - """; private GeofencingZoneState state; @BeforeEach void setUp() { + String POLYGON = """ + {"type":"POLYGON","polygonsDefinition":"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"} + """; state = new GeofencingZoneState(ZONE_ID, new BaseAttributeKvEntry(new JsonDataEntry("zone", POLYGON), 100L, 1L)); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java index 39780f09a6..b115f3e334 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java @@ -44,7 +44,7 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC private boolean createRelationsWithMatchedZones; private String zoneRelationType; private EntitySearchDirection zoneRelationDirection; - private Map geofencingZoneGroupConfigurations; + private Map zoneGroupConfigurations; @Override public CalculatedFieldType getType() { @@ -60,13 +60,9 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC @Override public void validate() { if (arguments == null) { - throw new IllegalArgumentException("Geofencing calculated field arguments are empty!"); - } - if (arguments.size() < 3) { - throw new IllegalArgumentException("Geofencing calculated field must contain at least 3 arguments!"); + throw new IllegalArgumentException("Geofencing calculated field arguments must be specified!"); } validateCoordinateArguments(); - Map zoneGroupsArguments = getZoneGroupArguments(); if (zoneGroupsArguments.isEmpty()) { throw new IllegalArgumentException("Geofencing calculated field must contain at least one geofencing zone group defined!"); @@ -81,32 +77,30 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC return; } if (StringUtils.isBlank(zoneRelationType)) { - throw new IllegalArgumentException("Zone relation type must be specified when to maintain relations with matched zones!"); + throw new IllegalArgumentException("Zone relation type must be specified to create relations with matched zones!"); } if (zoneRelationDirection == null) { - throw new IllegalArgumentException("Zone relation direction must be specified to maintain relations with matched zones!"); + throw new IllegalArgumentException("Zone relation direction must be specified to create relations with matched zones!"); } } private void validateZoneGroupConfigurations(Map zoneGroupsArguments) { - if (geofencingZoneGroupConfigurations == null) { - throw new IllegalArgumentException("Geofencing calculated field zone group configurations are empty!"); + if (zoneGroupConfigurations == null || zoneGroupConfigurations.isEmpty()) { + throw new IllegalArgumentException("Zone groups configuration should be specified!"); } Set usedPrefixes = new HashSet<>(); - geofencingZoneGroupConfigurations.forEach((zoneGroupName, config) -> { - Argument zoneGroupArgument = zoneGroupsArguments.get(zoneGroupName); - if (zoneGroupArgument == null) { - throw new IllegalArgumentException("Geofencing calculated field zone group configuration is not configured for zone group: " + zoneGroupName); - } + + zoneGroupsArguments.forEach((zoneGroupName, zoneGroupArgument) -> { + ZoneGroupConfiguration config = zoneGroupConfigurations.get(zoneGroupName); if (config == null) { - throw new IllegalArgumentException("Zone group configuration is not configured for zone group: " + zoneGroupName); + throw new IllegalArgumentException("Zone group configuration is not configured for '" + zoneGroupName + "' argument!"); } if (CollectionsUtil.isEmpty(config.getReportEvents())) { - throw new IllegalArgumentException("Zone group configuration report events must be specified for zone group: " + zoneGroupName); + throw new IllegalArgumentException("Zone group configuration report events must be specified for '" + zoneGroupName + "' argument!"); } String prefix = config.getReportTelemetryPrefix(); if (StringUtils.isBlank(prefix)) { - throw new IllegalArgumentException("Report telemetry prefix should be specified for zone group: " + zoneGroupName); + throw new IllegalArgumentException("Report telemetry prefix should be specified for '" + zoneGroupName + "' argument!"); } if (!usedPrefixes.add(prefix)) { throw new IllegalArgumentException("Duplicate report telemetry prefix found: '" + prefix + "'. Must be unique!"); @@ -118,26 +112,23 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC for (String coordinateKey : coordinateKeys) { Argument argument = arguments.get(coordinateKey); if (argument == null) { - throw new IllegalArgumentException("Missing required coordinates argument: " + coordinateKey); + throw new IllegalArgumentException("Missing required coordinates argument: " + coordinateKey + "!"); } ReferencedEntityKey refEntityKey = validateAndGetRefEntityKey(argument, coordinateKey); if (!ArgumentType.TS_LATEST.equals(refEntityKey.getType())) { - throw new IllegalArgumentException("Argument '" + coordinateKey + "' must be of type TS_LATEST."); + throw new IllegalArgumentException("Argument '" + coordinateKey + "' must be of type TS_LATEST!"); } if (argument.hasDynamicSource()) { - throw new IllegalArgumentException("Dynamic source is not allowed for argument: '" + coordinateKey + "'."); + throw new IllegalArgumentException("Dynamic source is not allowed for '" + coordinateKey + "' argument!"); } } } private void validateZoneGroupAruguments(Map zoneGroupsArguments) { zoneGroupsArguments.forEach((argumentKey, argument) -> { - if (argument == null) { - throw new IllegalArgumentException("Zone group argument is not configured: " + argumentKey); - } ReferencedEntityKey refEntityKey = validateAndGetRefEntityKey(argument, argumentKey); if (!ArgumentType.ATTRIBUTE.equals(refEntityKey.getType())) { - throw new IllegalArgumentException("Argument '" + argumentKey + "' must be of type ATTRIBUTE."); + throw new IllegalArgumentException("Argument '" + argumentKey + "' must be of type ATTRIBUTE!"); } if (argument.hasDynamicSource()) { argument.getRefDynamicSourceConfiguration().validate(); @@ -148,6 +139,7 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC private Map getZoneGroupArguments() { return arguments.entrySet() .stream() + .filter(entry -> entry.getValue() != null) .filter(entry -> !coordinateKeys.contains(entry.getKey())) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingZoneGroupConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ZoneGroupConfiguration.java similarity index 94% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingZoneGroupConfiguration.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ZoneGroupConfiguration.java index c82151fc64..cc5eb70eea 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingZoneGroupConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ZoneGroupConfiguration.java @@ -20,7 +20,7 @@ import lombok.Data; import java.util.List; @Data -public class GeofencingZoneGroupConfiguration { +public class ZoneGroupConfiguration { private final String reportTelemetryPrefix; private final List reportEvents; diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java new file mode 100644 index 0000000000..f4b4a66300 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java @@ -0,0 +1,472 @@ +/** + * 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.common.data.cf.configuration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; + +@ExtendWith(MockitoExtension.class) +public class GeofencingCalculatedFieldConfigurationTest { + + @Test + void typeShouldBeGeofencing() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + assertThat(cfg.getType()).isEqualTo(CalculatedFieldType.GEOFENCING); + } + + @Test + void validateShouldThrowWhenArgumentsNull() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setArguments(null); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Geofencing calculated field arguments must be specified!"); + } + + @Test + void validateShouldThrowWhenLatitudeArgIsMissing() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + var arguments = new HashMap(); + arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, null); + arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); + cfg.setArguments(arguments); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Missing required coordinates argument: " + ENTITY_ID_LATITUDE_ARGUMENT_KEY + "!"); + } + + @Test + void validateShouldThrowWhenLongitudeArgIsMissing() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + var arguments = new HashMap(); + arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); + arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, null); + cfg.setArguments(arguments); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Missing required coordinates argument: " + ENTITY_ID_LONGITUDE_ARGUMENT_KEY + "!"); + } + + @Test + void validateShouldThrowWhenLatitudeReferenceKeyIsNull() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setArguments(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument(null), + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)) + ); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Missing or invalid reference entity key for argument: " + ENTITY_ID_LATITUDE_ARGUMENT_KEY); + } + + @Test + void validateShouldThrowWhenLongitudeReferenceKeyIsNull() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setArguments(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST), + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument(null)) + ); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Missing or invalid reference entity key for argument: " + ENTITY_ID_LONGITUDE_ARGUMENT_KEY); + } + + @Test + void validateShouldThrowWhenLatitudeReferenceKeyTypeIsNull() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setArguments(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument(new ReferencedEntityKey("latitude", null, null)), + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)) + ); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Missing or invalid reference entity key for argument: " + ENTITY_ID_LATITUDE_ARGUMENT_KEY); + } + + @Test + void validateShouldThrowWhenReferenceKeyTypeIsNull() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setArguments(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST), + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument(new ReferencedEntityKey("longitude", null, null))) + ); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Missing or invalid reference entity key for argument: " + ENTITY_ID_LONGITUDE_ARGUMENT_KEY); + } + + @Test + void validateShouldThrowWhenLatitudeArgHasWrongArgumentType() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setArguments(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.ATTRIBUTE), + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST) + )); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Argument '" + ENTITY_ID_LATITUDE_ARGUMENT_KEY + "' must be of type TS_LATEST!"); + } + + @Test + void validateShouldThrowWhenLongitudeArgHasWrongArgumentType() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setArguments(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST), + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.ATTRIBUTE) + )); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Argument '" + ENTITY_ID_LONGITUDE_ARGUMENT_KEY + "' must be of type TS_LATEST!"); + } + + @Test + void validateShouldThrowWhenLatitudeArgHasDynamicSource() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + + Argument latitudeArg = toArgument("latitude", ArgumentType.TS_LATEST); + var refDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); + latitudeArg.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration); + + Argument longitudeArg = toArgument("longitude", ArgumentType.TS_LATEST); + + cfg.setArguments(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArg, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArg)); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dynamic source is not allowed for '" + ENTITY_ID_LATITUDE_ARGUMENT_KEY + "' argument!"); + } + + @Test + void validateShouldThrowWhenLongitudeArgHasDynamicSource() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + + Argument latitudeArg = toArgument("latitude", ArgumentType.TS_LATEST); + Argument longitudeArg = toArgument("longitude", ArgumentType.TS_LATEST); + var refDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); + longitudeArg.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration); + + cfg.setArguments(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArg, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArg)); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Dynamic source is not allowed for '" + ENTITY_ID_LONGITUDE_ARGUMENT_KEY + "' argument!"); + } + + @Test + void validateShouldThrowWhenGeofencingArgumentsMissing() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setArguments(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST), + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST) + )); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Geofencing calculated field must contain at least one geofencing zone group defined!"); + } + + @Test + void validateShouldThrowWhenZoneGroupArgumentIsNull() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + var arguments = new HashMap(); + arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); + arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); + arguments.put("someZones", null); + + cfg.setArguments(arguments); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Geofencing calculated field must contain at least one geofencing zone group defined!"); + } + + @Test + void validateShouldThrowWhenZoneGroupArgumentHasInvalidArgumentType() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + var arguments = new HashMap(); + arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); + arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); + arguments.put("allowedZones", toArgument("allowedZone", ArgumentType.TS_LATEST)); + + cfg.setArguments(arguments); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Argument 'allowedZones' must be of type ATTRIBUTE!"); + } + + @Test + void validateShouldCallDynamicSourceConfigValidationWhenZoneGroupArgumentHasDynamicSourceConfiguration() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + var arguments = new HashMap(); + arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); + arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); + Argument allowedZonesArg = toArgument("allowedZone", ArgumentType.ATTRIBUTE); + var refDynamicSourceConfigurationMock = mock(RelationQueryDynamicSourceConfiguration.class); + allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); + arguments.put("allowedZones", allowedZonesArg); + + ZoneGroupConfiguration allowedZoneConfiguration = new ZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); + Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); + + cfg.setArguments(arguments); + cfg.setZoneGroupConfigurations(zoneGroupConfigurations); + + cfg.validate(); + + verify(refDynamicSourceConfigurationMock).validate(); + } + + @Test + void validateShouldThrowWhenZoneGroupConfigurationIsMissing() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + var arguments = new HashMap(); + arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); + arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); + Argument allowedZonesArg = toArgument("allowedZone", ArgumentType.ATTRIBUTE); + var refDynamicSourceConfigurationMock = mock(RelationQueryDynamicSourceConfiguration.class); + allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); + arguments.put("allowedZones", allowedZonesArg); + + cfg.setArguments(arguments); + cfg.setZoneGroupConfigurations(null); + cfg.setCreateRelationsWithMatchedZones(false); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Zone groups configuration should be specified!"); + } + + @Test + void validateShouldThrowWhenReportTelemetryPrefixDuplicate() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + var arguments = new HashMap(); + arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); + arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); + Argument allowedZonesArg = toArgument("allowedZone", ArgumentType.ATTRIBUTE); + Argument restrictedZonesArg = toArgument("restrictedZone", ArgumentType.ATTRIBUTE); + var refDynamicSourceConfigurationMock = mock(RelationQueryDynamicSourceConfiguration.class); + allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); + arguments.put("allowedZones", allowedZonesArg); + arguments.put("restrictedZones", restrictedZonesArg); + + ZoneGroupConfiguration allowedZoneConfiguration = new ZoneGroupConfiguration("theSamePrefixTest", Arrays.asList(GeofencingEvent.values())); + ZoneGroupConfiguration restrictedZoneConfiguration = new ZoneGroupConfiguration("theSamePrefixTest", Arrays.asList(GeofencingEvent.values())); + Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration, "restrictedZones", restrictedZoneConfiguration); + + cfg.setArguments(arguments); + cfg.setZoneGroupConfigurations(zoneGroupConfigurations); + cfg.setCreateRelationsWithMatchedZones(false); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Duplicate report telemetry prefix found: 'theSamePrefixTest'. Must be unique!"); + } + + @Test + void validateShouldThrowWhenZoneGroupArgumentConfigurationIsMissing() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + var arguments = new HashMap(); + arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); + arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); + Argument allowedZonesArg = toArgument("allowedZone", ArgumentType.ATTRIBUTE); + var refDynamicSourceConfigurationMock = mock(RelationQueryDynamicSourceConfiguration.class); + allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); + arguments.put("allowedZones", allowedZonesArg); + + ZoneGroupConfiguration allowedZoneConfiguration = new ZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); + Map zoneGroupConfigurations = Map.of("someOtherZones", allowedZoneConfiguration); + + cfg.setArguments(arguments); + cfg.setZoneGroupConfigurations(zoneGroupConfigurations); + cfg.setCreateRelationsWithMatchedZones(false); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Zone group configuration is not configured for 'allowedZones' argument!"); + } + + @Test + void validateShouldThrowWhenZoneGroupConfigurationReportEventsAreNotSpecified() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + var arguments = new HashMap(); + arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); + arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); + Argument allowedZonesArg = toArgument("allowedZone", ArgumentType.ATTRIBUTE); + var refDynamicSourceConfigurationMock = mock(RelationQueryDynamicSourceConfiguration.class); + allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); + arguments.put("allowedZones", allowedZonesArg); + + ZoneGroupConfiguration allowedZoneConfiguration = new ZoneGroupConfiguration("allowedZone", null); + Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); + + cfg.setArguments(arguments); + cfg.setZoneGroupConfigurations(zoneGroupConfigurations); + cfg.setCreateRelationsWithMatchedZones(false); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Zone group configuration report events must be specified for 'allowedZones' argument!"); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = " ") + void validateShouldThrowWhenZoneGroupConfigurationTelemetryPrefixIsBlankOrNull(String reportTelemetryPrefix) { + var cfg = new GeofencingCalculatedFieldConfiguration(); + var arguments = new HashMap(); + arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); + arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); + Argument allowedZonesArg = toArgument("allowedZone", ArgumentType.ATTRIBUTE); + var refDynamicSourceConfigurationMock = mock(RelationQueryDynamicSourceConfiguration.class); + allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); + arguments.put("allowedZones", allowedZonesArg); + + ZoneGroupConfiguration allowedZoneConfiguration = new ZoneGroupConfiguration(reportTelemetryPrefix, Arrays.asList(GeofencingEvent.values())); + Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); + + cfg.setArguments(arguments); + cfg.setZoneGroupConfigurations(zoneGroupConfigurations); + cfg.setCreateRelationsWithMatchedZones(false); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Report telemetry prefix should be specified for 'allowedZones' argument!"); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = " ") + void validateShouldThrowWhenHasBlankOrNullZoneRelationType(String zoneRelationType) { + var cfg = new GeofencingCalculatedFieldConfiguration(); + var arguments = new HashMap(); + arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); + arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); + Argument allowedZonesArg = toArgument("allowedZone", ArgumentType.ATTRIBUTE); + var refDynamicSourceConfigurationMock = mock(RelationQueryDynamicSourceConfiguration.class); + allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); + arguments.put("allowedZones", allowedZonesArg); + + ZoneGroupConfiguration allowedZoneConfiguration = new ZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); + Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); + + cfg.setArguments(arguments); + cfg.setZoneGroupConfigurations(zoneGroupConfigurations); + cfg.setCreateRelationsWithMatchedZones(true); + cfg.setZoneRelationType(zoneRelationType); + cfg.setZoneRelationDirection(EntitySearchDirection.TO); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Zone relation type must be specified to create relations with matched zones!"); + } + + @Test + void validateShouldThrowWhenNoZoneRelationDirectionSpecified() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + var arguments = new HashMap(); + arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); + arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); + Argument allowedZonesArg = toArgument("allowedZone", ArgumentType.ATTRIBUTE); + var refDynamicSourceConfigurationMock = mock(RelationQueryDynamicSourceConfiguration.class); + allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); + arguments.put("allowedZones", allowedZonesArg); + + ZoneGroupConfiguration allowedZoneConfiguration = new ZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); + Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); + + cfg.setArguments(arguments); + cfg.setZoneGroupConfigurations(zoneGroupConfigurations); + cfg.setCreateRelationsWithMatchedZones(true); + cfg.setZoneRelationType("SomeRelationType"); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Zone relation direction must be specified to create relations with matched zones!"); + } + + @Test + void scheduledUpdateDisabledWhenIntervalIsZero() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setScheduledUpdateIntervalSec(0); + assertThat(cfg.isScheduledUpdateEnabled()).isFalse(); + } + + @Test + void scheduledUpdateDisabledWhenIntervalIsGreaterThanZeroButArgumentsAreEmpty() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setArguments(Map.of()); + cfg.setScheduledUpdateIntervalSec(60); + assertThat(cfg.isScheduledUpdateEnabled()).isFalse(); + } + + @Test + void scheduledUpdateDisabledWhenIntervalIsGreaterThanZeroButDynamicArgumentsAreMissing() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setArguments(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST))); + cfg.setScheduledUpdateIntervalSec(60); + assertThat(cfg.isScheduledUpdateEnabled()).isFalse(); + } + + @Test + void scheduledUpdateEnabledWhenIntervalIsGreaterThanZeroAndDynamicArgumentsPresent() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + Argument someDynamicArgument = toArgument("someDynamicArgument", ArgumentType.ATTRIBUTE); + someDynamicArgument.setRefDynamicSourceConfiguration(new RelationQueryDynamicSourceConfiguration()); + cfg.setArguments(Map.of("someDynamicArugument", someDynamicArgument)); + cfg.setScheduledUpdateIntervalSec(60); + assertThat(cfg.isScheduledUpdateEnabled()).isTrue(); + } + + + private Argument toArgument(String key, ArgumentType type) { + var referencedEntityKey = new ReferencedEntityKey(key, type, null); + return toArgument(referencedEntityKey); + } + + private Argument toArgument(ReferencedEntityKey referencedEntityKey) { + Argument argument = new Argument(); + argument.setRefEntityKey(referencedEntityKey); + return argument; + } + +} From 44a9327a26d02fb3a9c61f9fff88c521d510fc04 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Wed, 13 Aug 2025 13:05:50 +0300 Subject: [PATCH 051/216] Replaced with parameterized tests --- ...ncingCalculatedFieldConfigurationTest.java | 192 +++++++++--------- 1 file changed, 96 insertions(+), 96 deletions(-) diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java index f4b4a66300..3d7974df11 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java @@ -18,6 +18,8 @@ package org.thingsboard.server.common.data.cf.configuration; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.junit.jupiter.MockitoExtension; @@ -27,8 +29,10 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection; import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -54,141 +58,116 @@ public class GeofencingCalculatedFieldConfigurationTest { .hasMessage("Geofencing calculated field arguments must be specified!"); } - @Test - void validateShouldThrowWhenLatitudeArgIsMissing() { - var cfg = new GeofencingCalculatedFieldConfiguration(); - var arguments = new HashMap(); - arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, null); - arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); - cfg.setArguments(arguments); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Missing required coordinates argument: " + ENTITY_ID_LATITUDE_ARGUMENT_KEY + "!"); - } - - @Test - void validateShouldThrowWhenLongitudeArgIsMissing() { + @ParameterizedTest + @MethodSource("missingCoordinateArgs") + void validateShouldThrowWhenCoordinateArgIsMissing(String missingKey, String presentKey) { var cfg = new GeofencingCalculatedFieldConfiguration(); var arguments = new HashMap(); - arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); - arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, null); + arguments.put(missingKey, null); + arguments.put(presentKey, toArgument(presentKey, ArgumentType.TS_LATEST)); cfg.setArguments(arguments); assertThatThrownBy(cfg::validate) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Missing required coordinates argument: " + ENTITY_ID_LONGITUDE_ARGUMENT_KEY + "!"); + .hasMessage("Missing required coordinates argument: " + missingKey + "!"); } - @Test - void validateShouldThrowWhenLatitudeReferenceKeyIsNull() { - var cfg = new GeofencingCalculatedFieldConfiguration(); - cfg.setArguments(Map.of( - ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument(null), - ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)) + private static Stream missingCoordinateArgs() { + return Stream.of( + Arguments.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY), + Arguments.of(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, ENTITY_ID_LATITUDE_ARGUMENT_KEY) ); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Missing or invalid reference entity key for argument: " + ENTITY_ID_LATITUDE_ARGUMENT_KEY); } - @Test - void validateShouldThrowWhenLongitudeReferenceKeyIsNull() { - var cfg = new GeofencingCalculatedFieldConfiguration(); - cfg.setArguments(Map.of( - ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST), - ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument(null)) - ); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Missing or invalid reference entity key for argument: " + ENTITY_ID_LONGITUDE_ARGUMENT_KEY); - } + @ParameterizedTest + @MethodSource("nullRefKeyCoordinateArgs") + void validateShouldThrowWhenReferenceKeyIsNullOrTypeNull( + String brokenKey, Argument brokenArg, String okKey) { - @Test - void validateShouldThrowWhenLatitudeReferenceKeyTypeIsNull() { var cfg = new GeofencingCalculatedFieldConfiguration(); cfg.setArguments(Map.of( - ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument(new ReferencedEntityKey("latitude", null, null)), - ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)) - ); + brokenKey, brokenArg, + okKey, toArgument(okKey, ArgumentType.TS_LATEST) + )); assertThatThrownBy(cfg::validate) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Missing or invalid reference entity key for argument: " + ENTITY_ID_LATITUDE_ARGUMENT_KEY); + .hasMessage("Missing or invalid reference entity key for argument: " + brokenKey); } - @Test - void validateShouldThrowWhenReferenceKeyTypeIsNull() { - var cfg = new GeofencingCalculatedFieldConfiguration(); - cfg.setArguments(Map.of( - ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST), - ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument(new ReferencedEntityKey("longitude", null, null))) + private static Stream nullRefKeyCoordinateArgs() { + return Stream.of( + // null ref key on latitude + Arguments.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, + toArgument(null), + ENTITY_ID_LONGITUDE_ARGUMENT_KEY + ), + // null ref key on longitude + Arguments.of( + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, + toArgument(null), + ENTITY_ID_LATITUDE_ARGUMENT_KEY + ), + // null type on latitude + Arguments.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, + toArgument(new ReferencedEntityKey("latitude", null, null)), + ENTITY_ID_LONGITUDE_ARGUMENT_KEY + ), + // null type on longitude + Arguments.of( + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, + toArgument(new ReferencedEntityKey("longitude", null, null)), + ENTITY_ID_LATITUDE_ARGUMENT_KEY + ) ); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Missing or invalid reference entity key for argument: " + ENTITY_ID_LONGITUDE_ARGUMENT_KEY); } - @Test - void validateShouldThrowWhenLatitudeArgHasWrongArgumentType() { + @ParameterizedTest + @MethodSource("wrongTypeCoordinateArgs") + void validateShouldThrowWhenCoordinateHasWrongType(String wrongKey, String okKey) { var cfg = new GeofencingCalculatedFieldConfiguration(); cfg.setArguments(Map.of( - ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.ATTRIBUTE), - ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST) + wrongKey, toArgument(wrongKey, ArgumentType.ATTRIBUTE), + okKey, toArgument(okKey, ArgumentType.TS_LATEST) )); assertThatThrownBy(cfg::validate) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Argument '" + ENTITY_ID_LATITUDE_ARGUMENT_KEY + "' must be of type TS_LATEST!"); + .hasMessage("Argument '" + wrongKey + "' must be of type TS_LATEST!"); } - @Test - void validateShouldThrowWhenLongitudeArgHasWrongArgumentType() { - var cfg = new GeofencingCalculatedFieldConfiguration(); - cfg.setArguments(Map.of( - ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST), - ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.ATTRIBUTE) - )); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Argument '" + ENTITY_ID_LONGITUDE_ARGUMENT_KEY + "' must be of type TS_LATEST!"); + private static Stream wrongTypeCoordinateArgs() { + return Stream.of( + Arguments.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY), + Arguments.of(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, ENTITY_ID_LATITUDE_ARGUMENT_KEY) + ); } - @Test - void validateShouldThrowWhenLatitudeArgHasDynamicSource() { + @ParameterizedTest + @MethodSource("dynamicCoordinateArgs") + void validateShouldThrowWhenCoordinateHasDynamicSource(String dynamicKey, String okKey) { var cfg = new GeofencingCalculatedFieldConfiguration(); - Argument latitudeArg = toArgument("latitude", ArgumentType.TS_LATEST); - var refDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); - latitudeArg.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration); - - Argument longitudeArg = toArgument("longitude", ArgumentType.TS_LATEST); + var dynamicArg = toArgument(dynamicKey, ArgumentType.TS_LATEST); + dynamicArg.setRefDynamicSourceConfiguration(new RelationQueryDynamicSourceConfiguration()); - cfg.setArguments(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArg, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArg)); + cfg.setArguments(Map.of( + dynamicKey, dynamicArg, + okKey, toArgument(okKey, ArgumentType.TS_LATEST) + )); assertThatThrownBy(cfg::validate) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Dynamic source is not allowed for '" + ENTITY_ID_LATITUDE_ARGUMENT_KEY + "' argument!"); + .hasMessage("Dynamic source is not allowed for '" + dynamicKey + "' argument!"); } - @Test - void validateShouldThrowWhenLongitudeArgHasDynamicSource() { - var cfg = new GeofencingCalculatedFieldConfiguration(); - - Argument latitudeArg = toArgument("latitude", ArgumentType.TS_LATEST); - Argument longitudeArg = toArgument("longitude", ArgumentType.TS_LATEST); - var refDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); - longitudeArg.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration); - - cfg.setArguments(Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArg, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArg)); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Dynamic source is not allowed for '" + ENTITY_ID_LONGITUDE_ARGUMENT_KEY + "' argument!"); + private static Stream dynamicCoordinateArgs() { + return Stream.of( + Arguments.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY), + Arguments.of(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, ENTITY_ID_LATITUDE_ARGUMENT_KEY) + ); } @Test @@ -457,13 +436,34 @@ public class GeofencingCalculatedFieldConfigurationTest { assertThat(cfg.isScheduledUpdateEnabled()).isTrue(); } + @Test + void validateShouldPassOnMinimalValidConfig() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + var args = new HashMap(); + args.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); + args.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); + Argument allowed = toArgument("allowed", ArgumentType.ATTRIBUTE); + var refDynamicSourceConfigurationMock = mock(RelationQueryDynamicSourceConfiguration.class); + allowed.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); + args.put("allowedZones", allowed); + cfg.setArguments(args); + + var zc = new ZoneGroupConfiguration("gf_allowed", Arrays.asList(GeofencingEvent.values())); + cfg.setZoneGroupConfigurations(Map.of("allowedZones", zc)); + + cfg.setCreateRelationsWithMatchedZones(true); + cfg.setZoneRelationType("Contains"); + cfg.setZoneRelationDirection(EntitySearchDirection.FROM); + + assertThatCode(cfg::validate).doesNotThrowAnyException(); + } private Argument toArgument(String key, ArgumentType type) { var referencedEntityKey = new ReferencedEntityKey(key, type, null); return toArgument(referencedEntityKey); } - private Argument toArgument(ReferencedEntityKey referencedEntityKey) { + private static Argument toArgument(ReferencedEntityKey referencedEntityKey) { Argument argument = new Argument(); argument.setRefEntityKey(referencedEntityKey); return argument; From a78e88906bfce8424a250cfcf23b3612d5d0bb07 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 13 Aug 2025 13:18:58 +0300 Subject: [PATCH 052/216] expose swagger doc expansion property --- .../org/thingsboard/server/config/SwaggerConfiguration.java | 4 +++- application/src/main/resources/thingsboard.yml | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java index 067a1e98c6..8154b4fd9e 100644 --- a/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/SwaggerConfiguration.java @@ -116,6 +116,8 @@ public class SwaggerConfiguration { private String appVersion; @Value("${swagger.group_name:thingsboard}") private String groupName; + @Value("${swagger.doc_expansion:none}") + private String docExpansion; @Bean public OpenAPI thingsboardApi() { @@ -172,7 +174,7 @@ public class SwaggerConfiguration { uiProperties.setDefaultModelExpandDepth(1); uiProperties.setDefaultModelRendering("example"); uiProperties.setDisplayRequestDuration(false); - uiProperties.setDocExpansion("list"); + uiProperties.setDocExpansion(docExpansion); uiProperties.setFilter("false"); uiProperties.setMaxDisplayedTags(null); uiProperties.setOperationsSorter("alpha"); diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 54c1442ea7..4ebb6881b0 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1534,6 +1534,8 @@ swagger: version: "${SWAGGER_VERSION:}" # The group name (definition) on the API doc UI page. group_name: "${SWAGGER_GROUP_NAME:thingsboard}" + # Control the initial display state of API operations and tags (none, list or full) + doc_expansion: "${SWAGGER_DOC_EXPANSION:none}" # Queue configuration parameters queue: From ad511e935539e41a897d1455804e0e974b5a025a Mon Sep 17 00:00:00 2001 From: dshvaika Date: Wed, 13 Aug 2025 13:19:02 +0300 Subject: [PATCH 053/216] Added test for RelationQueryDynamicSourceConfiguration class --- ...lationQueryDynamicSourceConfiguration.java | 3 +- ...onQueryDynamicSourceConfigurationTest.java | 202 ++++++++++++++++++ 2 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java index fdf8815591..fb36417a35 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java @@ -17,6 +17,7 @@ package org.thingsboard.server.common.data.cf.configuration; import lombok.Data; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; @@ -50,7 +51,7 @@ public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynami if (direction == null) { throw new IllegalArgumentException("Relation query dynamic source configuration direction must be specified!"); } - if (relationType == null) { + if (StringUtils.isBlank(relationType)) { throw new IllegalArgumentException("Relation query dynamic source configuration relation type must be specified!"); } } diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java new file mode 100644 index 0000000000..3ab45858ab --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java @@ -0,0 +1,202 @@ +/** + * 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.common.data.cf.configuration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationsSearchParameters; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class RelationQueryDynamicSourceConfigurationTest { + + @Mock + EntityId rootEntityId; + + @Mock + EntityRelation rel1; + @Mock + EntityRelation rel2; + + @Test + void typeShouldBeRelationQuery() { + var cfg = new RelationQueryDynamicSourceConfiguration(); + assertThat(cfg.getType()).isEqualTo(CFArgumentDynamicSourceType.RELATION_QUERY); + } + + @Test + void validateShouldThrowWhenMaxLevelGreaterThanTwo() { + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setMaxLevel(3); + cfg.setDirection(EntitySearchDirection.FROM); + cfg.setRelationType(EntityRelation.CONTAINS_TYPE); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Relation query dynamic source configuration max relation level can't be greater than 2!"); + } + + @Test + void validateShouldThrowWhenDirectionIsNull() { + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setMaxLevel(1); + cfg.setDirection(null); + cfg.setRelationType(EntityRelation.CONTAINS_TYPE); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Relation query dynamic source configuration direction must be specified!"); + } + + @ParameterizedTest + @ValueSource(strings = {" "}) + @NullAndEmptySource + void validateShouldThrowWhenRelationTypeIsNull(String relationType) { + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setMaxLevel(1); + cfg.setDirection(EntitySearchDirection.TO); + cfg.setRelationType(relationType); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Relation query dynamic source configuration relation type must be specified!"); + } + + @ParameterizedTest + @NullAndEmptySource + void isSimpleRelationTrueWhenLevelIsOneAndEntityTypesEmptyOrNull(List entityTypes) { + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setMaxLevel(1); + cfg.setEntityTypes(entityTypes); + + assertThat(cfg.isSimpleRelation()).isTrue(); + } + + @Test + void isSimpleRelationFalseWhenMaxLevelNotOne() { + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setMaxLevel(2); + cfg.setEntityTypes(null); + + assertThat(cfg.isSimpleRelation()).isFalse(); + } + + @Test + void isSimpleRelationFalseWhenEntityTypesProvided() { + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setMaxLevel(1); + cfg.setEntityTypes(List.of(EntityType.DEVICE)); + + assertThat(cfg.isSimpleRelation()).isFalse(); + } + + @ParameterizedTest + @NullAndEmptySource + void toEntityRelationsQueryShouldThrowForSimpleRelation(List entityTypes) { + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setMaxLevel(1); + cfg.setFetchLastLevelOnly(false); + cfg.setDirection(EntitySearchDirection.FROM); + cfg.setRelationType(EntityRelation.CONTAINS_TYPE); + cfg.setEntityTypes(entityTypes); + + assertThatThrownBy(() -> cfg.toEntityRelationsQuery(rootEntityId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Entity relations query can't be created for a simple relation!"); + } + + @Test + void toEntityRelationsQueryShouldBuildQueryForNonSimpleRelation() { + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setMaxLevel(2); + cfg.setFetchLastLevelOnly(true); + cfg.setDirection(EntitySearchDirection.TO); + cfg.setRelationType(EntityRelation.MANAGES_TYPE); + cfg.setEntityTypes(List.of(EntityType.DEVICE, EntityType.ASSET)); + + var query = cfg.toEntityRelationsQuery(rootEntityId); + + assertThat(query).isNotNull(); + RelationsSearchParameters params = query.getParameters(); + assertThat(params).isNotNull(); + assertThat(params.getRootId()).isEqualTo(rootEntityId.getId()); + assertThat(params.getDirection()).isEqualTo(EntitySearchDirection.TO); + assertThat(params.getMaxLevel()).isEqualTo(2); + assertThat(params.isFetchLastLevelOnly()).isTrue(); + + assertThat(query.getFilters()).hasSize(1); + assertThat(query.getFilters().get(0)).isInstanceOf(RelationEntityTypeFilter.class); + RelationEntityTypeFilter filter = query.getFilters().get(0); + assertThat(filter.getRelationType()).isEqualTo(EntityRelation.MANAGES_TYPE); + assertThat(filter.getEntityTypes()).containsExactly(EntityType.DEVICE, EntityType.ASSET); + } + + @Test + void resolveEntityIdsFromDirectionFROMReturnsToIds() { + when(rel1.getTo()).thenReturn(mock(EntityId.class)); + when(rel2.getTo()).thenReturn(mock(EntityId.class)); + + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setDirection(EntitySearchDirection.FROM); + + var out = cfg.resolveEntityIds(List.of(rel1, rel2)); + + assertThat(out).containsExactly(rel1.getTo(), rel2.getTo()); + } + + @Test + void resolveEntityIdsFromDirectionTOReturnsFromIds() { + when(rel1.getFrom()).thenReturn(mock(EntityId.class)); + when(rel2.getFrom()).thenReturn(mock(EntityId.class)); + + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setDirection(EntitySearchDirection.TO); + + var out = cfg.resolveEntityIds(List.of(rel1, rel2)); + + assertThat(out).containsExactly(rel1.getFrom(), rel2.getFrom()); + } + + @Test + void validateShouldPassForValidConfig() { + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setMaxLevel(2); + cfg.setFetchLastLevelOnly(false); + cfg.setDirection(EntitySearchDirection.FROM); + cfg.setRelationType(EntityRelation.CONTAINS_TYPE); + cfg.setEntityTypes(List.of(EntityType.DEVICE)); + + assertThatCode(cfg::validate).doesNotThrowAnyException(); + } + +} From bf8384807648b8d22e06d3ba031c89836c933a2c Mon Sep 17 00:00:00 2001 From: dshvaika Date: Wed, 13 Aug 2025 13:24:52 +0300 Subject: [PATCH 054/216] Removed maxAllowedScheduledUpdateIntervalInSecForCF from tenantProfileConfiguration --- .../tenant/profile/DefaultTenantProfileConfiguration.java | 2 -- .../server/dao/cf/BaseCalculatedFieldService.java | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index f35fca23f9..7c174b005b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -174,8 +174,6 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long maxArgumentsPerCF = 10; @Schema(example = "3600") private int minAllowedScheduledUpdateIntervalInSecForCF = 3600; - @Schema(example = "86400") - private int maxAllowedScheduledUpdateIntervalInSecForCF = 86400; @Builder.Default @Min(value = 1, message = "must be at least 1") @Schema(example = "1000") diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 40694050be..1dc1e16144 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -97,10 +97,10 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements if (!configuration.isScheduledUpdateEnabled()) { return; } - var defaultProfileConfiguration = tbTenantProfileCache.get(calculatedField.getTenantId()).getDefaultProfileConfiguration(); - int min = defaultProfileConfiguration.getMinAllowedScheduledUpdateIntervalInSecForCF(); - int max = defaultProfileConfiguration.getMaxAllowedScheduledUpdateIntervalInSecForCF(); - configuration.setScheduledUpdateIntervalSec(Math.max(min, Math.min(configuration.getScheduledUpdateIntervalSec(), max))); + int tenantProfileMinAllowedValue = tbTenantProfileCache.get(calculatedField.getTenantId()) + .getDefaultProfileConfiguration() + .getMinAllowedScheduledUpdateIntervalInSecForCF(); + configuration.setScheduledUpdateIntervalSec(Math.max(configuration.getScheduledUpdateIntervalSec(), tenantProfileMinAllowedValue)); } @Override From 43b07c242fb03df5b5732cd6acd3ee9ed8c81164 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Wed, 13 Aug 2025 14:43:59 +0300 Subject: [PATCH 055/216] Added service layer test with validation of scheduling config updates --- .../cf/CalculatedFieldIntegrationTest.java | 6 +- .../GeofencingCalculatedFieldStateTest.java | 6 +- .../CalculatedFieldConfiguration.java | 2 - ...eofencingCalculatedFieldConfiguration.java | 4 +- ... => GeofencingZoneGroupConfiguration.java} | 2 +- ...ncingCalculatedFieldConfigurationTest.java | 32 +-- .../service/CalculatedFieldServiceTest.java | 193 +++++++++++++++++- 7 files changed, 216 insertions(+), 29 deletions(-) rename common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/{ZoneGroupConfiguration.java => GeofencingZoneGroupConfiguration.java} (94%) diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index 8a8d5e389e..303515ca61 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -32,7 +32,7 @@ import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; -import org.thingsboard.server.common.data.cf.configuration.ZoneGroupConfiguration; +import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; @@ -701,8 +701,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes // Zone group reporting config List reportEvents = Arrays.stream(GeofencingEvent.values()).toList(); - ZoneGroupConfiguration allowedCfg = new ZoneGroupConfiguration("allowedZone", reportEvents); - ZoneGroupConfiguration restrictedCfg = new ZoneGroupConfiguration("restrictedZone", reportEvents); + GeofencingZoneGroupConfiguration allowedCfg = new GeofencingZoneGroupConfiguration("allowedZone", reportEvents); + GeofencingZoneGroupConfiguration restrictedCfg = new GeofencingZoneGroupConfiguration("restrictedZone", reportEvents); cfg.setZoneGroupConfigurations(Map.of( "allowedZones", allowedCfg, diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index 0d7a2971d0..cadb47c8f5 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -30,7 +30,7 @@ import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; -import org.thingsboard.server.common.data.cf.configuration.ZoneGroupConfiguration; +import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; @@ -337,8 +337,8 @@ public class GeofencingCalculatedFieldStateTest { config.setArguments(Map.of("latitude", argument1, "longitude", argument2, "allowedZones", argument3, "restrictedZones", argument4)); List reportEvents = Arrays.stream(GeofencingEvent.values()).toList(); - ZoneGroupConfiguration allowedZoneGroupConfiguration = new ZoneGroupConfiguration("allowedZone", reportEvents); - ZoneGroupConfiguration restrictedZoneGroupConfiguration = new ZoneGroupConfiguration("restrictedZone", reportEvents); + GeofencingZoneGroupConfiguration allowedZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", reportEvents); + GeofencingZoneGroupConfiguration restrictedZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("restrictedZone", reportEvents); config.setZoneGroupConfigurations(Map.of("allowedZones", allowedZoneGroupConfiguration, "restrictedZones", restrictedZoneGroupConfiguration)); config.setCreateRelationsWithMatchedZones(true); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java index 3459e11c0c..c7b5c6fcaf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java @@ -66,11 +66,9 @@ public interface CalculatedFieldConfiguration { return false; } - @JsonIgnore default void setScheduledUpdateIntervalSec(int scheduledUpdateIntervalSec) { } - @JsonIgnore default int getScheduledUpdateIntervalSec() { return 0; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java index b115f3e334..34b2820cd0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java @@ -44,7 +44,7 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC private boolean createRelationsWithMatchedZones; private String zoneRelationType; private EntitySearchDirection zoneRelationDirection; - private Map zoneGroupConfigurations; + private Map zoneGroupConfigurations; @Override public CalculatedFieldType getType() { @@ -91,7 +91,7 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC Set usedPrefixes = new HashSet<>(); zoneGroupsArguments.forEach((zoneGroupName, zoneGroupArgument) -> { - ZoneGroupConfiguration config = zoneGroupConfigurations.get(zoneGroupName); + GeofencingZoneGroupConfiguration config = zoneGroupConfigurations.get(zoneGroupName); if (config == null) { throw new IllegalArgumentException("Zone group configuration is not configured for '" + zoneGroupName + "' argument!"); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ZoneGroupConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingZoneGroupConfiguration.java similarity index 94% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ZoneGroupConfiguration.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingZoneGroupConfiguration.java index cc5eb70eea..c82151fc64 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ZoneGroupConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingZoneGroupConfiguration.java @@ -20,7 +20,7 @@ import lombok.Data; import java.util.List; @Data -public class ZoneGroupConfiguration { +public class GeofencingZoneGroupConfiguration { private final String reportTelemetryPrefix; private final List reportEvents; diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java index 3d7974df11..44e6365b2e 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java @@ -224,8 +224,8 @@ public class GeofencingCalculatedFieldConfigurationTest { allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); arguments.put("allowedZones", allowedZonesArg); - ZoneGroupConfiguration allowedZoneConfiguration = new ZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); - Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); + GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); + Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); cfg.setArguments(arguments); cfg.setZoneGroupConfigurations(zoneGroupConfigurations); @@ -268,9 +268,9 @@ public class GeofencingCalculatedFieldConfigurationTest { arguments.put("allowedZones", allowedZonesArg); arguments.put("restrictedZones", restrictedZonesArg); - ZoneGroupConfiguration allowedZoneConfiguration = new ZoneGroupConfiguration("theSamePrefixTest", Arrays.asList(GeofencingEvent.values())); - ZoneGroupConfiguration restrictedZoneConfiguration = new ZoneGroupConfiguration("theSamePrefixTest", Arrays.asList(GeofencingEvent.values())); - Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration, "restrictedZones", restrictedZoneConfiguration); + GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("theSamePrefixTest", Arrays.asList(GeofencingEvent.values())); + GeofencingZoneGroupConfiguration restrictedZoneConfiguration = new GeofencingZoneGroupConfiguration("theSamePrefixTest", Arrays.asList(GeofencingEvent.values())); + Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration, "restrictedZones", restrictedZoneConfiguration); cfg.setArguments(arguments); cfg.setZoneGroupConfigurations(zoneGroupConfigurations); @@ -292,8 +292,8 @@ public class GeofencingCalculatedFieldConfigurationTest { allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); arguments.put("allowedZones", allowedZonesArg); - ZoneGroupConfiguration allowedZoneConfiguration = new ZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); - Map zoneGroupConfigurations = Map.of("someOtherZones", allowedZoneConfiguration); + GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); + Map zoneGroupConfigurations = Map.of("someOtherZones", allowedZoneConfiguration); cfg.setArguments(arguments); cfg.setZoneGroupConfigurations(zoneGroupConfigurations); @@ -315,8 +315,8 @@ public class GeofencingCalculatedFieldConfigurationTest { allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); arguments.put("allowedZones", allowedZonesArg); - ZoneGroupConfiguration allowedZoneConfiguration = new ZoneGroupConfiguration("allowedZone", null); - Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); + GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", null); + Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); cfg.setArguments(arguments); cfg.setZoneGroupConfigurations(zoneGroupConfigurations); @@ -340,8 +340,8 @@ public class GeofencingCalculatedFieldConfigurationTest { allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); arguments.put("allowedZones", allowedZonesArg); - ZoneGroupConfiguration allowedZoneConfiguration = new ZoneGroupConfiguration(reportTelemetryPrefix, Arrays.asList(GeofencingEvent.values())); - Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); + GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration(reportTelemetryPrefix, Arrays.asList(GeofencingEvent.values())); + Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); cfg.setArguments(arguments); cfg.setZoneGroupConfigurations(zoneGroupConfigurations); @@ -365,8 +365,8 @@ public class GeofencingCalculatedFieldConfigurationTest { allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); arguments.put("allowedZones", allowedZonesArg); - ZoneGroupConfiguration allowedZoneConfiguration = new ZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); - Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); + GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); + Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); cfg.setArguments(arguments); cfg.setZoneGroupConfigurations(zoneGroupConfigurations); @@ -390,8 +390,8 @@ public class GeofencingCalculatedFieldConfigurationTest { allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); arguments.put("allowedZones", allowedZonesArg); - ZoneGroupConfiguration allowedZoneConfiguration = new ZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); - Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); + GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); + Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); cfg.setArguments(arguments); cfg.setZoneGroupConfigurations(zoneGroupConfigurations); @@ -448,7 +448,7 @@ public class GeofencingCalculatedFieldConfigurationTest { args.put("allowedZones", allowed); cfg.setArguments(args); - var zc = new ZoneGroupConfiguration("gf_allowed", Arrays.asList(GeofencingEvent.values())); + var zc = new GeofencingZoneGroupConfiguration("gf_allowed", Arrays.asList(GeofencingEvent.values())); cfg.setZoneGroupConfigurations(Map.of("allowedZones", zc)); cfg.setCreateRelationsWithMatchedZones(true); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index 2985aa7620..3c6a30ca5c 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java @@ -28,18 +28,24 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; +import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import java.util.Arrays; import java.util.Map; -import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -51,6 +57,8 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { private CalculatedFieldService calculatedFieldService; @Autowired private DeviceService deviceService; + @Autowired + private TbTenantProfileCache tbTenantProfileCache; private ListeningExecutorService executor; @@ -90,6 +98,187 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { calculatedFieldService.deleteCalculatedField(tenantId, savedCalculatedField.getId()); } + @Test + public void testSaveGeofencingCalculatedField_shouldNotChangeScheduledInterval() { + // Arrange a device + Device device = createTestDevice(); + + // Build a valid Geofencing configuration + GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); + + // Coordinates: TS_LATEST, no dynamic source + Argument lat = new Argument(); + lat.setRefEntityId(device.getId()); + lat.setRefEntityKey(new ReferencedEntityKey("latitude", ArgumentType.TS_LATEST, null)); + + Argument lon = new Argument(); + lon.setRefEntityId(device.getId()); + lon.setRefEntityKey(new ReferencedEntityKey("longitude", ArgumentType.TS_LATEST, null)); + + // Zone-group argument (ATTRIBUTE) — no DYNAMIC configuration, so no scheduling even if the scheduled interval is set + Argument allowed = new Argument(); + lat.setRefEntityId(device.getId()); + allowed.setRefEntityKey(new ReferencedEntityKey("allowed", ArgumentType.ATTRIBUTE, null)); + + cfg.setArguments(Map.of( + GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY, lat, + GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY, lon, + "allowed", allowed + )); + + // Matching zone-group configuration + var geofencingZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("gf_allowed", Arrays.asList(GeofencingEvent.values())); + cfg.setZoneGroupConfigurations(Map.of("allowed", geofencingZoneGroupConfiguration)); + + // Set a scheduled interval to some value + cfg.setScheduledUpdateIntervalSec(600); + + // Create & save Calculated Field + CalculatedField cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("GF clamp test"); + cf.setConfigurationVersion(0); + cf.setConfiguration(cfg); + + CalculatedField saved = calculatedFieldService.save(cf); + + // Assert: the interval is saved, but scheduling is not enabled + int savedInterval = saved.getConfiguration().getScheduledUpdateIntervalSec(); + boolean scheduledUpdateEnabled = saved.getConfiguration().isScheduledUpdateEnabled(); + + assertThat(savedInterval).isEqualTo(600); + assertThat(scheduledUpdateEnabled).isFalse(); + + calculatedFieldService.deleteCalculatedField(tenantId, saved.getId()); + } + + @Test + public void testSaveGeofencingCalculatedField_shouldClampScheduledIntervalToTenantMin() { + // Arrange a device + Device device = createTestDevice(); + + // Build a valid Geofencing configuration + GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); + + // Coordinates: TS_LATEST, no dynamic source + Argument lat = new Argument(); + lat.setRefEntityId(device.getId()); + lat.setRefEntityKey(new ReferencedEntityKey("latitude", ArgumentType.TS_LATEST, null)); + + Argument lon = new Argument(); + lon.setRefEntityId(device.getId()); + lon.setRefEntityKey(new ReferencedEntityKey("longitude", ArgumentType.TS_LATEST, null)); + + // Zone-group argument (ATTRIBUTE) — make it DYNAMIC so scheduling is enabled + Argument allowed = new Argument(); + allowed.setRefEntityKey(new ReferencedEntityKey("allowed", ArgumentType.ATTRIBUTE, null)); + var dynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); + dynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); + dynamicSourceConfiguration.setMaxLevel(1); + dynamicSourceConfiguration.setRelationType(EntityRelation.CONTAINS_TYPE); + allowed.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); + + cfg.setArguments(Map.of( + GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY, lat, + GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY, lon, + "allowed", allowed + )); + + // Matching zone-group configuration + var geofencingZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("gf_allowed", Arrays.asList(GeofencingEvent.values())); + cfg.setZoneGroupConfigurations(Map.of("allowed", geofencingZoneGroupConfiguration)); + + // Enable scheduling with an interval below tenant min + cfg.setScheduledUpdateIntervalSec(600); + + // Create & save Calculated Field + CalculatedField cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("GF clamp test"); + cf.setConfigurationVersion(0); + cf.setConfiguration(cfg); + + CalculatedField saved = calculatedFieldService.save(cf); + + // Assert: the interval is clamped up to tenant profile min + int savedInterval = saved.getConfiguration().getScheduledUpdateIntervalSec(); + + int min = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedScheduledUpdateIntervalInSecForCF(); + assertThat(savedInterval).isEqualTo(min); + + calculatedFieldService.deleteCalculatedField(tenantId, saved.getId()); + } + + @Test + public void testSaveGeofencingCalculatedField_shouldUseScheduledIntervalFromConfig() { + // Arrange a device + Device device = createTestDevice(); + + // Build a valid Geofencing configuration + GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); + + // Coordinates: TS_LATEST, no dynamic source + Argument lat = new Argument(); + lat.setRefEntityId(device.getId()); + lat.setRefEntityKey(new ReferencedEntityKey("latitude", ArgumentType.TS_LATEST, null)); + + Argument lon = new Argument(); + lon.setRefEntityId(device.getId()); + lon.setRefEntityKey(new ReferencedEntityKey("longitude", ArgumentType.TS_LATEST, null)); + + // Zone-group argument (ATTRIBUTE) — make it DYNAMIC so scheduling is enabled + Argument allowed = new Argument(); + allowed.setRefEntityKey(new ReferencedEntityKey("allowed", ArgumentType.ATTRIBUTE, null)); + var dynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); + dynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); + dynamicSourceConfiguration.setMaxLevel(1); + dynamicSourceConfiguration.setRelationType(EntityRelation.CONTAINS_TYPE); + allowed.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); + + cfg.setArguments(Map.of( + GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY, lat, + GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY, lon, + "allowed", allowed + )); + + // Matching zone-group configuration + var geofencingZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("gf_allowed", Arrays.asList(GeofencingEvent.values())); + cfg.setZoneGroupConfigurations(Map.of("allowed", geofencingZoneGroupConfiguration)); + + // Get tenant profile min. + int min = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedScheduledUpdateIntervalInSecForCF(); + + + // Enable scheduling with an interval greater than tenant min + int valueFromConfig = min + 100; + cfg.setScheduledUpdateIntervalSec(valueFromConfig); + + // Create & save Calculated Field + CalculatedField cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("GF no clamp test"); + cf.setConfigurationVersion(0); + cf.setConfiguration(cfg); + + CalculatedField saved = calculatedFieldService.save(cf); + + // Assert: the interval is clamped up to tenant profile min (or stays >= original if already >= min) + int savedInterval = saved.getConfiguration().getScheduledUpdateIntervalSec(); + assertThat(savedInterval).isEqualTo(valueFromConfig); + + calculatedFieldService.deleteCalculatedField(tenantId, saved.getId()); + } + @Test public void testSaveCalculatedFieldWithExistingName() { Device device = createTestDevice(); From ed70a1e690192db3180d467de73aaeda8e60acf6 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Wed, 13 Aug 2025 15:56:31 +0300 Subject: [PATCH 056/216] Added additional validation to be sure that the value cannot be set to 0 which means unlimited level --- .../RelationQueryDynamicSourceConfiguration.java | 3 +++ .../RelationQueryDynamicSourceConfigurationTest.java | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java index fb36417a35..e2a33228c6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java @@ -45,6 +45,9 @@ public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynami @Override public void validate() { + if (maxLevel < 1) { + throw new IllegalArgumentException("Relation query dynamic source configuration max relation level can't be less than 1!"); + } if (maxLevel > 2) { throw new IllegalArgumentException("Relation query dynamic source configuration max relation level can't be greater than 2!"); } diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java index 3ab45858ab..648a7c985e 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java @@ -54,6 +54,18 @@ public class RelationQueryDynamicSourceConfigurationTest { assertThat(cfg.getType()).isEqualTo(CFArgumentDynamicSourceType.RELATION_QUERY); } + @Test + void validateShouldThrowWhenMaxLevelLessThanOne() { + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setMaxLevel(0); + cfg.setDirection(EntitySearchDirection.FROM); + cfg.setRelationType(EntityRelation.CONTAINS_TYPE); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Relation query dynamic source configuration max relation level can't be less than 1!"); + } + @Test void validateShouldThrowWhenMaxLevelGreaterThanTwo() { var cfg = new RelationQueryDynamicSourceConfiguration(); From a4ac5e3a7faccd9cb326b938563b68d157148a19 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Wed, 13 Aug 2025 17:10:52 +0300 Subject: [PATCH 057/216] Added integration test with dynamic arguments refresh logic --- .../cf/CalculatedFieldIntegrationTest.java | 165 +++++++++++++++++- .../server/controller/AbstractWebTest.java | 26 +++ 2 files changed, 184 insertions(+), 7 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index 303515ca61..688096dcae 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -24,6 +24,8 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.EntityInfo; +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.cf.CalculatedField; @@ -618,26 +620,28 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes } @Test - public void testGeofencingCalculatedField_SingleZonePerGroup() throws Exception { + public void testGeofencingCalculatedField_withoutRelationsCreationAndDynamicRefresh() throws Exception { // --- Arrange entities --- Device device = createDevice("GF Device", "sn-geo-1"); // Allowed zone polygon (square) String allowedPolygon = """ - {"type":"POLYGON","polygonsDefinition":"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"} - """; + {"type":"POLYGON","polygonsDefinition":"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"} + """; // Restricted zone polygon (square) String restrictedPolygon = """ - {"type":"POLYGON","polygonsDefinition":"[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"} - """; + {"type":"POLYGON","polygonsDefinition":"[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"} + """; Asset allowedZoneAsset = createAsset("Allowed Zone", null); doPost("/api/plugins/telemetry/ASSET/" + allowedZoneAsset.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, - JacksonUtil.toJsonNode("{\"zone\":" + allowedPolygon + "}")).andExpect(status().isOk());; + JacksonUtil.toJsonNode("{\"zone\":" + allowedPolygon + "}")).andExpect(status().isOk()); + ; Asset restrictedZoneAsset = createAsset("Restricted Zone", null); doPost("/api/plugins/telemetry/ASSET/" + restrictedZoneAsset.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, - JacksonUtil.toJsonNode("{\"zone\":" + restrictedPolygon + "}")).andExpect(status().isOk());; + JacksonUtil.toJsonNode("{\"zone\":" + restrictedPolygon + "}")).andExpect(status().isOk()); + ; // Relations from device to zones EntityRelation deviceToAllowedZoneRelation = new EntityRelation(); @@ -748,6 +752,153 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); } + @Test + public void testGeofencingCalculatedField_DynamicRefresh_RebindsZoneArguments() throws Exception { + // --- Update min allowed scheduled update intervals for CFs --- + loginSysAdmin(); + EntityInfo tenantProfileEntityInfo = doGet("/api/tenantProfileInfo/default", EntityInfo.class); + assertThat(tenantProfileEntityInfo).isNotNull(); + TenantProfile foundTenantProfile = doGet("/api/tenantProfile/" + tenantProfileEntityInfo.getId().getId().toString(), TenantProfile.class); + assertThat(foundTenantProfile).isNotNull(); + assertThat(foundTenantProfile.getDefaultProfileConfiguration()).isNotNull(); + foundTenantProfile.getDefaultProfileConfiguration().setMinAllowedScheduledUpdateIntervalInSecForCF(TIMEOUT / 10); + TenantProfile savedTenantProfile = doPost("/api/tenantProfile", foundTenantProfile, TenantProfile.class); + assertThat(savedTenantProfile).isNotNull(); + assertThat(savedTenantProfile.getDefaultProfileConfiguration().getMinAllowedScheduledUpdateIntervalInSecForCF()).isEqualTo(TIMEOUT / 10); + loginTenantAdmin(); + + // --- Arrange entities --- + Device device = createDevice("GF Device dyn", "sn-geo-dyn-1"); + + // Allowed Zone A: covers initial point (ENTERED) + String allowedPolygonA = """ + {"type":"POLYGON","polygonsDefinition":"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"} + """; + + Asset allowedZoneA = createAsset("Allowed Zone A", null); + doPost("/api/plugins/telemetry/ASSET/" + allowedZoneA.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, + JacksonUtil.toJsonNode("{\"zone\":" + allowedPolygonA + "}")).andExpect(status().isOk()); + + // Relation from device to Allowed Zone A + EntityRelation relAllowedA = new EntityRelation(); + relAllowedA.setFrom(device.getId()); + relAllowedA.setTo(allowedZoneA.getId()); + relAllowedA.setType("AllowedZone"); + doPost("/api/relation", relAllowedA).andExpect(status().isOk()); + + // Initial device coordinates: INSIDE Zone A + doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", + JacksonUtil.toJsonNode("{\"latitude\":50.4730,\"longitude\":30.5050}")).andExpect(status().isOk()); + + // --- Build CF: GEOFENCING with dynamic 'allowedZones' and short scheduled refresh --- + CalculatedField cf = new CalculatedField(); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("Geofencing CF (dynamic refresh)"); + cf.setDebugSettings(DebugSettings.off()); + + GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); + + // Coordinates (TS_LATEST) + Argument lat = new Argument(); + lat.setRefEntityKey(new ReferencedEntityKey("latitude", ArgumentType.TS_LATEST, null)); + Argument lon = new Argument(); + lon.setRefEntityKey(new ReferencedEntityKey("longitude", ArgumentType.TS_LATEST, null)); + + // Dynamic group 'allowedZones' resolved by relations (FROM device -> assets of type AllowedZone) + Argument allowedZones = new Argument(); + var dyn = new RelationQueryDynamicSourceConfiguration(); + dyn.setDirection(EntitySearchDirection.FROM); + dyn.setRelationType("AllowedZone"); + dyn.setMaxLevel(1); + dyn.setFetchLastLevelOnly(true); + allowedZones.setRefEntityKey(new ReferencedEntityKey("zone", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); + allowedZones.setRefDynamicSourceConfiguration(dyn); + + cfg.setArguments(Map.of( + GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY, lat, + GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY, lon, + "allowedZones", allowedZones + )); + + // Report all events for the group + List reportEvents = Arrays.stream(GeofencingEvent.values()).toList(); + GeofencingZoneGroupConfiguration allowedCfg = new GeofencingZoneGroupConfiguration("allowedZone", reportEvents); + cfg.setZoneGroupConfigurations(Map.of("allowedZones", allowedCfg)); + + // Server attributes output + Output out = new Output(); + out.setType(OutputType.ATTRIBUTES); + out.setScope(AttributeScope.SERVER_SCOPE); + cfg.setOutput(out); + + // Enable scheduled refresh with a 6-second interval + cfg.setScheduledUpdateIntervalSec(6); + + cf.setConfiguration(cfg); + CalculatedField savedCalculatedField = doPost("/api/calculatedField", cf, CalculatedField.class); + assertThat(savedCalculatedField).isNotNull(); + assertThat(savedCalculatedField.getConfiguration().isScheduledUpdateEnabled()).isTrue(); + + // --- Assert initial evaluation (ENTERED) --- + await().alias("initial geofencing evaluation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = getServerAttributes(device.getId(), "allowedZoneEvent"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(1); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZoneEvent", "ENTERED"); + }); + + // --- Move device OUTSIDE Zone A (expect LEFT) --- + doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", + JacksonUtil.toJsonNode("{\"latitude\":50.4760,\"longitude\":30.5110}")).andExpect(status().isOk()); + + await().alias("outside zone A (LEFT)") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = getServerAttributes(device.getId(), "allowedZoneEvent"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(1); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZoneEvent", "LEFT"); + }); + + // --- Create Allowed Zone B covering the CURRENT location --- + String allowedPolygonB = """ + {"type":"POLYGON","polygonsDefinition":"[[50.475500, 30.510500], [50.475500, 30.511500], [50.476500, 30.511500], [50.476500, 30.510500]]"} + """; + + Asset allowedZoneB = createAsset("Allowed Zone B", null); + doPost("/api/plugins/telemetry/ASSET/" + allowedZoneB.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, + JacksonUtil.toJsonNode("{\"zone\":" + allowedPolygonB + "}")).andExpect(status().isOk()); + + // Add a new relation + EntityRelation relAllowedB = new EntityRelation(); + relAllowedB.setFrom(device.getId()); + relAllowedB.setTo(allowedZoneB.getId()); + relAllowedB.setType("AllowedZone"); + doPost("/api/relation", relAllowedB).andExpect(status().isOk()); + + awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsDirty(device.getId(), savedCalculatedField.getId()); + + // --- Same coordinates as before, but now we expect ENTERED since a new zone is registered --- + doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", + JacksonUtil.toJsonNode("{\"latitude\":50.4760,\"longitude\":30.5110}")).andExpect(status().isOk()); + + // --- Assert dynamic refresh picks up new relation and flips event back to ENTERED on the next telemetry update --- + await().alias("dynamic refresh rebinds allowedZones") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(1, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = getServerAttributes(device.getId(), "allowedZoneEvent"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(1); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZoneEvent", "ENTERED"); + }); + } + private ObjectNode getLatestTelemetry(EntityId entityId, String... keys) throws Exception { return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/timeseries?keys=" + String.join(",", keys), ObjectNode.class); } diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index b93ff0f623..fd01581e36 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -68,7 +68,10 @@ import org.thingsboard.rule.engine.api.MailService; import org.thingsboard.server.actors.DefaultTbActorSystem; import org.thingsboard.server.actors.TbActorId; import org.thingsboard.server.actors.TbActorMailbox; +import org.thingsboard.server.actors.TbCalculatedFieldEntityActorId; import org.thingsboard.server.actors.TbEntityActorId; +import org.thingsboard.server.actors.calculatedField.CalculatedFieldEntityActor; +import org.thingsboard.server.actors.calculatedField.CalculatedFieldEntityMessageProcessor; import org.thingsboard.server.actors.device.DeviceActor; import org.thingsboard.server.actors.device.DeviceActorMessageProcessor; import org.thingsboard.server.actors.device.SessionInfo; @@ -99,6 +102,7 @@ import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadCo import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; @@ -150,6 +154,7 @@ import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.memory.InMemoryStorage; import org.thingsboard.server.service.cf.CfRocksDb; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest; @@ -1099,6 +1104,17 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { }); } + protected void awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsDirty(EntityId entityId, CalculatedFieldId cfId) { + CalculatedFieldEntityMessageProcessor processor = getCalculatedFieldEntityMessageProcessor(entityId); + Map statesMap = (Map) ReflectionTestUtils.getField(processor, "states"); + Awaitility.await("CF state for entity actor marked as dirty").atMost(5, TimeUnit.SECONDS).until(() -> { + CalculatedFieldState calculatedFieldState = statesMap.get(cfId); + boolean stateDirty = calculatedFieldState != null && calculatedFieldState.isDirty(); + log.warn("entityId {}, cfId {}, state dirty == {}", entityId, cfId, stateDirty); + return stateDirty; + }); + } + protected static String getMapName(FeatureType featureType) { switch (featureType) { case ATTRIBUTES: @@ -1120,6 +1136,16 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return (DeviceActorMessageProcessor) ReflectionTestUtils.getField(actor, "processor"); } + protected CalculatedFieldEntityMessageProcessor getCalculatedFieldEntityMessageProcessor(EntityId entityId) { + DefaultTbActorSystem actorSystem = (DefaultTbActorSystem) ReflectionTestUtils.getField(actorService, "system"); + ConcurrentMap actors = (ConcurrentMap) ReflectionTestUtils.getField(actorSystem, "actors"); + Awaitility.await("CF entity actor was created").atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> actors.containsKey(new TbCalculatedFieldEntityActorId(entityId))); + TbActorMailbox actorMailbox = actors.get(new TbCalculatedFieldEntityActorId(entityId)); + CalculatedFieldEntityActor actor = (CalculatedFieldEntityActor) ReflectionTestUtils.getField(actorMailbox, "actor"); + return (CalculatedFieldEntityMessageProcessor) ReflectionTestUtils.getField(actor, "processor"); + } + protected void updateDefaultTenantProfileConfig(Consumer updater) throws ThingsboardException { updateDefaultTenantProfile(tenantProfile -> { TenantProfileData profileData = tenantProfile.getProfileData(); From faf842f9986541d270529281b022a43011795038 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Wed, 13 Aug 2025 19:03:14 +0300 Subject: [PATCH 058/216] Updated toTbelCfArg implementation for GeofencingArgumentEntry --- .../service/cf/ctx/state/GeofencingArgumentEntry.java | 2 +- application/src/main/resources/logback.xml | 1 + .../RelationQueryDynamicSourceConfiguration.java | 2 ++ .../script/api/tbel/TbelCfTsGeofencingArg.java | 11 +++++++++-- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java index 4b88419fbb..509bb46c60 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java @@ -72,7 +72,7 @@ public class GeofencingArgumentEntry implements ArgumentEntry { @Override public TbelCfArg toTbelCfArg() { - return new TbelCfTsGeofencingArg(); + return new TbelCfTsGeofencingArg(zoneStates); } private Map toZones(Map entityIdKvEntryMap) { diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml index f5a8d47df1..8e1a49faef 100644 --- a/application/src/main/resources/logback.xml +++ b/application/src/main/resources/logback.xml @@ -58,6 +58,7 @@ + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java index e2a33228c6..c34199a9d7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.cf.configuration; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; @@ -59,6 +60,7 @@ public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynami } } + @JsonIgnore public boolean isSimpleRelation() { return maxLevel == 1 && CollectionsUtil.isEmpty(entityTypes); } diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsGeofencingArg.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsGeofencingArg.java index 46ac553a76..f1e8ec16db 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsGeofencingArg.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbelCfTsGeofencingArg.java @@ -15,11 +15,18 @@ */ package org.thingsboard.script.api.tbel; -// TODO: should I add any specific logic for this? +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data public class TbelCfTsGeofencingArg implements TbelCfArg { - public TbelCfTsGeofencingArg() { + private final Object value; + @JsonCreator + public TbelCfTsGeofencingArg(@JsonProperty("value") Object value) { + this.value = value; } @Override From 1a28a30ce691b34a32622dfe1416f68901668609 Mon Sep 17 00:00:00 2001 From: wangxin Date: Fri, 15 Aug 2025 10:01:01 +0800 Subject: [PATCH 059/216] fix: Fixed the issue that textSearch is invalid when querying calculated field --- .../server/dao/sql/cf/CalculatedFieldRepository.java | 6 +++++- .../server/dao/sql/cf/JpaCalculatedFieldDao.java | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java index 8ccdb88db0..7755ef036b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/CalculatedFieldRepository.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.sql.cf; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.dao.model.sql.CalculatedFieldEntity; @@ -36,7 +37,10 @@ public interface CalculatedFieldRepository extends JpaRepository findAllByTenantId(UUID tenantId, Pageable pageable); - Page findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId, Pageable pageable); + @Query("SELECT cf FROM CalculatedFieldEntity cf WHERE cf.tenantId = :tenantId " + + "AND cf.entityId = :entityId " + + "AND (:textSearch IS NULL OR ilike(cf.name, CONCAT('%', :textSearch, '%')) = true)") + Page findAllByTenantIdAndEntityId(UUID tenantId, UUID entityId, String textSearch, Pageable pageable); List findAllByTenantId(UUID tenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java index 2632b0237b..385839dded 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/cf/JpaCalculatedFieldDao.java @@ -85,7 +85,7 @@ public class JpaCalculatedFieldDao extends JpaAbstractDao findAllByEntityId(TenantId tenantId, EntityId entityId, PageLink pageLink) { log.debug("Try to find calculated fields by entityId[{}] and pageLink [{}]", entityId, pageLink); - return DaoUtil.toPageData(calculatedFieldRepository.findAllByTenantIdAndEntityId(tenantId.getId(), entityId.getId(), DaoUtil.toPageable(pageLink))); + return DaoUtil.toPageData(calculatedFieldRepository.findAllByTenantIdAndEntityId(tenantId.getId(), entityId.getId(), pageLink.getTextSearch(), DaoUtil.toPageable(pageLink))); } @Override From 0c03abe5e6ca2045e3e8b531477b2cc7eed5c07a Mon Sep 17 00:00:00 2001 From: dshvaika Date: Fri, 15 Aug 2025 12:43:31 +0300 Subject: [PATCH 060/216] Added tenant profile upgrade script & Added argument test & removed outdated todo items --- .../main/data/upgrade/basic/schema_update.sql | 21 +++++++++- .../cf/ctx/state/GeofencingZoneState.java | 3 +- .../GeofencingCalculatedFieldStateTest.java | 4 -- .../GeofencingValueArgumentEntryTest.java | 2 - .../data/cf/configuration/Argument.java | 1 - .../data/cf/configuration/ArgumentTest.java | 38 +++++++++++++++++++ 6 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index add832ea6e..f0da7dcb47 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -43,4 +43,23 @@ DROP INDEX IF EXISTS idx_widgets_bundle_external_id; -- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT END -ALTER TABLE mobile_app ADD COLUMN IF NOT EXISTS title varchar(255); \ No newline at end of file +-- ADD NEW COLUMN TITLE TO MOBILE APP START + +ALTER TABLE mobile_app ADD COLUMN IF NOT EXISTS title varchar(255); + +-- ADD NEW COLUMN TITLE TO MOBILE APP END + +-- UPDATE TENANT PROFILE CONFIGURATION START + +UPDATE tenant_profile +SET profile_data = jsonb_set( + profile_data, + '{configuration}', + (profile_data -> 'configuration') || '{ + "minAllowedScheduledUpdateIntervalInSecForCF": 3600 + }'::jsonb, + false + ) +WHERE (profile_data -> 'configuration' -> 'minAllowedScheduledUpdateIntervalInSecForCF') IS NULL; + +-- UPDATE TENANT PROFILE CONFIGURATION END diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java index 12493152dc..761245cb73 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java @@ -71,8 +71,7 @@ public class GeofencingZoneState { this.ts = newZoneState.getTs(); this.version = newVersion; this.perimeterDefinition = newZoneState.getPerimeterDefinition(); - // TODO: should we reinitialize state if zone changed? - // this.inside = null; + this.inside = null; return true; } return false; diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index cadb47c8f5..fe9444461e 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -160,7 +160,6 @@ public class GeofencingCalculatedFieldStateTest { assertThat(state.getArguments()).isEqualTo(newArgs); } - // TODO: write opposite test for this. See TODO in the GeofencingZoneState class. @Test void testUpdateStateWhenUpdateExistingGeofencingValueArgumentEntryWithTheSameValue() { state.arguments = new HashMap<>(Map.of("allowedZones", geofencingAllowedZoneArgEntry)); @@ -345,9 +344,6 @@ public class GeofencingCalculatedFieldStateTest { config.setZoneRelationType("CurrentZone"); config.setZoneRelationDirection(EntitySearchDirection.TO); - // TODO: Does CF possible to save with null? - config.setExpression("latitude + longitude"); - Output output = new Output(); output.setType(OutputType.TIME_SERIES); config.setOutput(output); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java index 4491716b19..aad9c4e29c 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java @@ -184,6 +184,4 @@ public class GeofencingValueArgumentEntryTest { assertThat(geofencingArgumentEntry.isEmpty()).isTrue(); } - // TODO: should we test to TBEL logic? - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java index 3b8ec3308a..52935c3411 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/Argument.java @@ -26,7 +26,6 @@ public class Argument { @Nullable private EntityId refEntityId; - // TODO: add upgrade in PE version -> CFArgumentDynamicSourceType to CFArgumentDynamicSourceConfiguration private CfArgumentDynamicSourceConfiguration refDynamicSourceConfiguration; private ReferencedEntityKey refEntityKey; private String defaultValue; diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java new file mode 100644 index 0000000000..3039e36a05 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java @@ -0,0 +1,38 @@ +/** + * 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.common.data.cf.configuration; + + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ArgumentTest { + + @Test + void validateShouldReturnFalseIfDynamicSourceConfigurationIsNull() { + var argument = new Argument(); + assertThat(argument.hasDynamicSource()).isFalse(); + } + + @Test + void validateShouldReturnTrueIfDynamicSourceConfigurationIsNotNull() { + var argument = new Argument(); + argument.setRefDynamicSourceConfiguration(new RelationQueryDynamicSourceConfiguration()); + assertThat(argument.hasDynamicSource()).isTrue(); + } + +} \ No newline at end of file From 012930a2a9177a80cb5c668a567bcec230065575 Mon Sep 17 00:00:00 2001 From: Mia <43924767+371518473@users.noreply.github.com> Date: Sat, 16 Aug 2025 10:50:53 +0800 Subject: [PATCH 061/216] Update rpc_debug_terminal.json Fix the extra ']' in the help --- .../data/json/system/widget_types/rpc_debug_terminal.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/application/src/main/data/json/system/widget_types/rpc_debug_terminal.json b/application/src/main/data/json/system/widget_types/rpc_debug_terminal.json index e65e9a1204..40399f034b 100644 --- a/application/src/main/data/json/system/widget_types/rpc_debug_terminal.json +++ b/application/src/main/data/json/system/widget_types/rpc_debug_terminal.json @@ -11,7 +11,7 @@ "resources": [], "templateHtml": "
", "templateCss": ".cmd .cursor.blink {\n -webkit-animation-name: terminal-underline;\n -moz-animation-name: terminal-underline;\n -ms-animation-name: terminal-underline;\n animation-name: terminal-underline;\n}\n.terminal .inverted, .cmd .inverted {\n border-bottom-color: #aaa;\n}\n\n", - "controllerScript": "var requestTimeout = 500;\nvar requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetEntityName && subscription.targetEntityName.length) {\n deviceName = subscription.targetEntityName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' [params body]]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestPersistent, persistentPollingInterval, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}", + "controllerScript": "var requestTimeout = 500;\nvar requestPersistent = false;\nvar persistentPollingInterval = 5000;\n\nself.onInit = function() {\n var subscription = self.ctx.defaultSubscription;\n var rpcEnabled = subscription.rpcEnabled;\n var deviceName = 'Simulated';\n var prompt;\n if (subscription.targetEntityName && subscription.targetEntityName.length) {\n deviceName = subscription.targetEntityName;\n }\n if (self.ctx.settings.requestTimeout) {\n requestTimeout = self.ctx.settings.requestTimeout;\n }\n if (self.ctx.settings.requestPersistent) {\n requestPersistent = self.ctx.settings.requestPersistent;\n }\n if (self.ctx.settings.persistentPollingInterval) {\n persistentPollingInterval = self.ctx.settings.persistentPollingInterval;\n }\n var greetings = 'Welcome to ThingsBoard RPC debug terminal.\\n\\n';\n if (!rpcEnabled) {\n greetings += 'Target device is not set!\\n\\n';\n prompt = '';\n } else {\n greetings += 'Current target device for RPC commands: [[b;#fff;]' + deviceName + ']\\n\\n';\n greetings += 'Please type [[b;#fff;]\\'help\\'] to see usage.\\n';\n prompt = '[[b;#8bc34a;]' + deviceName +']> ';\n }\n \n var terminal = $('#device-terminal', self.ctx.$container).terminal(\n function(command) {\n if (command !== '') {\n try {\n var localCommand = command.trim();\n var requestUUID = uuidv4();\n if (localCommand === 'help') {\n printUsage(this);\n } else {\n var spaceIndex = localCommand.indexOf(' ');\n if (spaceIndex === -1 && !localCommand.length) {\n this.error(\"Wrong number of arguments!\");\n this.echo(' ');\n } else {\n var params;\n if (spaceIndex === -1) {\n spaceIndex = localCommand.length;\n }\n var name = localCommand.substr(0, spaceIndex);\n var args = localCommand.substr(spaceIndex + 1);\n if (args.length) {\n try {\n params = JSON.parse(args);\n } catch (e) {\n params = args;\n }\n }\n performRpc(this, name, params, requestUUID);\n }\n }\n } catch(e) {\n this.error(new String(e));\n }\n } else {\n this.echo('');\n }\n }, {\n greetings: greetings,\n prompt: prompt,\n enabled: rpcEnabled\n });\n \n if (!rpcEnabled) {\n terminal.error('No RPC target detected!').pause();\n }\n}\n\n\nfunction printUsage(terminal) {\n var commandsListText = '\\n[[b;#fff;]Usage:]\\n';\n commandsListText += ' [params body]\\n\\n';\n commandsListText += '[[b;#fff;]Example 1:]\\n'; \n commandsListText += ' myRemoteMethod1 myText\\n\\n'; \n commandsListText += '[[b;#fff;]Example 2:]\\n'; \n commandsListText += ' myOtherRemoteMethod \"{\\\\\"key1\\\\\": 2, \\\\\"key2\\\\\": \\\\\"myVal\\\\\"}\"\\n'; \n terminal.echo(new String(commandsListText));\n}\n\n\nfunction performRpc(terminal, method, params, requestUUID) {\n terminal.pause();\n self.ctx.controlApi.sendTwoWayCommand(method, params, requestTimeout, requestPersistent, persistentPollingInterval, requestUUID).subscribe(\n function success(responseBody) {\n terminal.echo(JSON.stringify(responseBody));\n terminal.echo(' ');\n terminal.resume();\n },\n function fail() {\n var errorText = self.ctx.defaultSubscription.rpcErrorText;\n terminal.error(errorText);\n terminal.echo(' ');\n terminal.resume();\n }\n );\n}\n\n\nfunction uuidv4() {\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {\n var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);\n return v.toString(16);\n });\n}\n\n \nself.onDestroy = function() {\n self.ctx.controlApi.completedCommand();\n}", "settingsSchema": "", "dataKeySettingsSchema": "{}\n", "settingsDirective": "tb-rpc-terminal-widget-settings", @@ -43,4 +43,4 @@ "public": true } ] -} \ No newline at end of file +} From cc3ecfc0272994b46669d2606e828cc224c68245 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Mon, 18 Aug 2025 13:23:59 +0300 Subject: [PATCH 062/216] Added reporting strategies instead of single zone group event --- .../state/GeofencingCalculatedFieldState.java | 156 ++++++++---------- .../cf/ctx/state/GeofencingEvalResult.java | 23 +++ .../cf/ctx/state/GeofencingZoneState.java | 41 +++-- .../server/utils/CalculatedFieldUtils.java | 5 +- .../cf/CalculatedFieldIntegrationTest.java | 68 ++++---- .../GeofencingCalculatedFieldStateTest.java | 9 +- .../cf/ctx/state/GeofencingZoneStateTest.java | 25 +-- .../utils/CalculatedFieldUtilsTest.java | 7 +- ...eofencingCalculatedFieldConfiguration.java | 25 +-- .../cf/configuration/GeofencingEvent.java | 15 +- ...ion.java => GeofencingPresenceStatus.java} | 11 +- .../GeofencingReportStrategy.java | 24 +++ .../GeofencingTransitionEvent.java | 20 +++ ...ncingCalculatedFieldConfigurationTest.java | 110 ++---------- .../service/CalculatedFieldServiceTest.java | 13 +- 15 files changed, 256 insertions(+), 296 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingEvalResult.java rename common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/{GeofencingZoneGroupConfiguration.java => GeofencingPresenceStatus.java} (77%) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingReportStrategy.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingTransitionEvent.java diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java index 9e598db69a..b6b8d89928 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java @@ -25,7 +25,8 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.geo.Coordinates; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; +import org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy; +import org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.service.cf.CalculatedFieldResult; @@ -34,15 +35,14 @@ import org.thingsboard.server.utils.CalculatedFieldUtils; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Optional; -import java.util.Set; import java.util.stream.Collectors; import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.INSIDE; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.OUTSIDE; @Data @AllArgsConstructor @@ -129,54 +129,60 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { return calculateWithoutRelations(ctx, entityCoordinates, configuration); } + @Override + public boolean isReady() { + return arguments.keySet().containsAll(requiredArguments) && + arguments.values().stream().noneMatch(ArgumentEntry::isEmpty); + } + + @Override + public void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize) { + if (!sizeExceedsLimit && maxStateSize > 0 && CalculatedFieldUtils.toProto(ctxId, this).getSerializedSize() > maxStateSize) { + arguments.clear(); + sizeExceedsLimit = true; + } + } + + private ListenableFuture calculateWithRelations( EntityId entityId, CalculatedFieldCtx ctx, Coordinates entityCoordinates, GeofencingCalculatedFieldConfiguration configuration) { - var geofencingZoneGroupConfigurations = configuration.getZoneGroupConfigurations(); - - Map zoneEventMap = new HashMap<>(); + var zoneGroupReportStrategies = configuration.getZoneGroupReportStrategies(); ObjectNode resultNode = JacksonUtil.newObjectNode(); + List> relationFutures = new ArrayList<>(); getGeofencingArguments().forEach((argumentKey, argumentEntry) -> { - var zoneGroupConfig = geofencingZoneGroupConfigurations.get(argumentKey); - Set groupEvents = new HashSet<>(); + GeofencingReportStrategy geofencingReportStrategy = zoneGroupReportStrategies.get(argumentKey); + List zoneResults = new ArrayList<>(); argumentEntry.getZoneStates().forEach((zoneId, zoneState) -> { - GeofencingEvent event = zoneState.evaluate(entityCoordinates); - zoneEventMap.put(zoneId, event); - groupEvents.add(event); + GeofencingEvalResult eval = zoneState.evaluate(entityCoordinates); + zoneResults.add(eval); + + GeofencingTransitionEvent transitionEvent = eval.transition(); + if (transitionEvent == null) { + return; + } + EntityRelation relation = toRelation(zoneId, entityId, configuration); + ListenableFuture f = switch (transitionEvent) { + case ENTERED -> ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), relation); + case LEFT -> ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), relation); + }; + relationFutures.add(f); }); - - aggregateZoneGroupEvent(groupEvents) - .filter(zoneGroupConfig.getReportEvents()::contains) - .ifPresent(geofencingGroupEvent -> - resultNode.put(zoneGroupConfig.getReportTelemetryPrefix() + "Event", geofencingGroupEvent.name())); + updateResultNode(argumentKey, zoneResults, geofencingReportStrategy, resultNode); }); var result = calculationResult(ctx, resultNode); - - List> relationFutures = zoneEventMap.entrySet().stream() - .filter(entry -> entry.getValue().isTransitionEvent()) - .map(entry -> { - EntityRelation relation = toRelation(entry.getKey(), entityId, configuration); - return switch (entry.getValue()) { - case ENTERED -> ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), relation); - case LEFT -> ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), relation); - default -> throw new IllegalStateException("Unexpected transition event: " + entry.getValue()); - }; - }) - .toList(); - if (relationFutures.isEmpty()) { return Futures.immediateFuture(result); } - - return Futures.whenAllComplete(relationFutures).call(() -> - new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode), - MoreExecutors.directExecutor()); + return Futures.whenAllComplete(relationFutures) + .call(() -> new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode), + MoreExecutors.directExecutor()); } private ListenableFuture calculateWithoutRelations( @@ -184,19 +190,17 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { Coordinates entityCoordinates, GeofencingCalculatedFieldConfiguration configuration) { - var geofencingZoneGroupConfigurations = configuration.getZoneGroupConfigurations(); + var zoneGroupReportStrategies = configuration.getZoneGroupReportStrategies(); ObjectNode resultNode = JacksonUtil.newObjectNode(); getGeofencingArguments().forEach((argumentKey, argumentEntry) -> { - var zoneGroupConfig = geofencingZoneGroupConfigurations.get(argumentKey); - Set groupEvents = argumentEntry.getZoneStates().values().stream() + var geofencingReportStrategy = zoneGroupReportStrategies.get(argumentKey); + + List zoneResults = argumentEntry.getZoneStates().values().stream() .map(zs -> zs.evaluate(entityCoordinates)) - .collect(Collectors.toSet()); - aggregateZoneGroupEvent(groupEvents) - .filter(zoneGroupConfig.getReportEvents()::contains) - .ifPresent(e -> resultNode.put( - zoneGroupConfig.getReportTelemetryPrefix() + "Event", - e.name())); + .toList(); + + updateResultNode(argumentKey, zoneResults, geofencingReportStrategy, resultNode); }); return Futures.immediateFuture(calculationResult(ctx, resultNode)); } @@ -205,20 +209,6 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { return new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode); } - @Override - public boolean isReady() { - return arguments.keySet().containsAll(requiredArguments) && - arguments.values().stream().noneMatch(ArgumentEntry::isEmpty); - } - - @Override - public void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize) { - if (!sizeExceedsLimit && maxStateSize > 0 && CalculatedFieldUtils.toProto(ctxId, this).getSerializedSize() > maxStateSize) { - arguments.clear(); - sizeExceedsLimit = true; - } - } - private Map getGeofencingArguments() { return arguments.entrySet() .stream() @@ -226,37 +216,37 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { .collect(Collectors.toMap(Map.Entry::getKey, entry -> (GeofencingArgumentEntry) entry.getValue())); } - private Optional aggregateZoneGroupEvent(Set zoneEvents) { - boolean hasEntered = false; - boolean hasLeft = false; - boolean hasInside = false; - boolean hasOutside = false; - - for (GeofencingEvent event : zoneEvents) { - if (event == null) { - continue; - } - switch (event) { - case ENTERED -> hasEntered = true; - case LEFT -> hasLeft = true; - case INSIDE -> hasInside = true; - case OUTSIDE -> hasOutside = true; + private void updateResultNode(String argumentKey, List zoneResults, GeofencingReportStrategy geofencingReportStrategy, ObjectNode resultNode) { + GeofencingEvalResult aggregationResult = aggregateZoneGroup(zoneResults); + final String eventKey = argumentKey + "Event"; + final String statusKey = argumentKey + "Status"; + switch (geofencingReportStrategy) { + case REPORT_TRANSITION_EVENTS_ONLY -> addTransitionEventIfExists(resultNode, aggregationResult, eventKey); + case REPORT_PRESENCE_STATUS_ONLY -> resultNode.put(statusKey, aggregationResult.status().name()); + case REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS -> { + addTransitionEventIfExists(resultNode, aggregationResult, eventKey); + resultNode.put(statusKey, aggregationResult.status().name()); } } + } - if (hasOutside && !hasInside && !hasEntered && !hasLeft) { - return Optional.of(GeofencingEvent.OUTSIDE); - } - if (hasLeft && !hasEntered && !hasInside) { - return Optional.of(GeofencingEvent.LEFT); - } - if (hasEntered && !hasLeft && !hasInside) { - return Optional.of(GeofencingEvent.ENTERED); + private void addTransitionEventIfExists(ObjectNode resultNode, GeofencingEvalResult aggregationResult, String eventKey) { + if (aggregationResult.transition() != null) { + resultNode.put(eventKey, aggregationResult.transition().name()); } - if (hasInside || hasEntered) { - return Optional.of(GeofencingEvent.INSIDE); + } + + private GeofencingEvalResult aggregateZoneGroup(List zoneResults) { + boolean nowInside = zoneResults.stream().anyMatch(r -> INSIDE.equals(r.status())); + boolean prevInside = zoneResults.stream() + .anyMatch(r -> GeofencingTransitionEvent.LEFT.equals(r.transition()) || r.transition() == null && r.status() == INSIDE); + GeofencingTransitionEvent transition = null; + if (!prevInside && nowInside) { + transition = GeofencingTransitionEvent.ENTERED; + } else if (prevInside && !nowInside) { + transition = GeofencingTransitionEvent.LEFT; } - return Optional.empty(); + return new GeofencingEvalResult(transition, nowInside ? INSIDE : OUTSIDE); } private EntityRelation toRelation(EntityId zoneId, EntityId entityId, GeofencingCalculatedFieldConfiguration configuration) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingEvalResult.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingEvalResult.java new file mode 100644 index 0000000000..65b862b4f5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingEvalResult.java @@ -0,0 +1,23 @@ +/** + * 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.service.cf.ctx.state; + +import jakarta.annotation.Nullable; +import org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus; +import org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent; + +public record GeofencingEvalResult(@Nullable GeofencingTransitionEvent transition, + GeofencingPresenceStatus status) {} \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java index 761245cb73..ad8de7454e 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java @@ -20,7 +20,8 @@ import lombok.EqualsAndHashCode; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.geo.Coordinates; import org.thingsboard.common.util.geo.PerimeterDefinition; -import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; +import org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus; +import org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.kv.AttributeKvEntry; @@ -30,6 +31,9 @@ import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneProto; import java.util.UUID; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.INSIDE; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.OUTSIDE; + @Data public class GeofencingZoneState { @@ -40,7 +44,7 @@ public class GeofencingZoneState { private PerimeterDefinition perimeterDefinition; @EqualsAndHashCode.Exclude - private Boolean inside; + private GeofencingPresenceStatus lastPresence; public GeofencingZoneState(EntityId zoneId, KvEntry entry) { this.zoneId = zoneId; @@ -58,7 +62,7 @@ public class GeofencingZoneState { this.version = proto.getVersion(); this.perimeterDefinition = JacksonUtil.fromString(proto.getPerimeterDefinition(), PerimeterDefinition.class); if (proto.hasInside()) { - this.inside = proto.getInside(); + this.lastPresence = proto.getInside() ? INSIDE : OUTSIDE; } } @@ -71,26 +75,35 @@ public class GeofencingZoneState { this.ts = newZoneState.getTs(); this.version = newVersion; this.perimeterDefinition = newZoneState.getPerimeterDefinition(); - this.inside = null; + this.lastPresence = null; return true; } return false; } - public GeofencingEvent evaluate(Coordinates entityCoordinates) { - boolean inside = perimeterDefinition.checkMatches(entityCoordinates); - // Initial evaluation — no prior state - if (this.inside == null) { - this.inside = inside; - return inside ? GeofencingEvent.ENTERED : GeofencingEvent.OUTSIDE; + public GeofencingEvalResult evaluate(Coordinates entityCoordinates) { + boolean nowInside = perimeterDefinition.checkMatches(entityCoordinates); + + GeofencingPresenceStatus status = nowInside ? INSIDE : OUTSIDE; + + // first evaluation + if (this.lastPresence == null) { + this.lastPresence = status; + GeofencingTransitionEvent transition = null; + if (status == GeofencingPresenceStatus.INSIDE) { + transition = GeofencingTransitionEvent.ENTERED; + } + return new GeofencingEvalResult(transition, status); } // State changed - if (this.inside != inside) { - this.inside = inside; - return inside ? GeofencingEvent.ENTERED : GeofencingEvent.LEFT; + if (this.lastPresence != status) { + this.lastPresence = status; + GeofencingTransitionEvent transition = (status == GeofencingPresenceStatus.INSIDE) ? + GeofencingTransitionEvent.ENTERED : GeofencingTransitionEvent.LEFT; + return new GeofencingEvalResult(transition, status); } // State unchanged - return inside ? GeofencingEvent.INSIDE : GeofencingEvent.OUTSIDE; + return new GeofencingEvalResult(null, status); } private EntityId toZoneId(GeofencingZoneProto proto) { diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java index 7658409662..e4240ef104 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java @@ -18,6 +18,7 @@ package org.thingsboard.server.utils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; @@ -138,8 +139,8 @@ public class CalculatedFieldUtils { .setTs(zoneState.getTs()) .setVersion(zoneState.getVersion()) .setPerimeterDefinition(JacksonUtil.toString(zoneState.getPerimeterDefinition())); - if (zoneState.getInside() != null) { - builder.setInside(zoneState.getInside()); + if (zoneState.getLastPresence() != null) { + builder.setInside(zoneState.getLastPresence().equals(GeofencingPresenceStatus.INSIDE)); } return builder.build(); } diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index 688096dcae..c2ff2342ab 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -33,8 +33,6 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; -import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; @@ -49,15 +47,14 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.controller.CalculatedFieldControllerTest; import org.thingsboard.server.dao.service.DaoSqlTest; -import java.util.Arrays; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; @DaoSqlTest public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTest { @@ -702,15 +699,9 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes "restrictedZones", restrictedZones )); - // Zone group reporting config - List reportEvents = Arrays.stream(GeofencingEvent.values()).toList(); - - GeofencingZoneGroupConfiguration allowedCfg = new GeofencingZoneGroupConfiguration("allowedZone", reportEvents); - GeofencingZoneGroupConfiguration restrictedCfg = new GeofencingZoneGroupConfiguration("restrictedZone", reportEvents); - - cfg.setZoneGroupConfigurations(Map.of( - "allowedZones", allowedCfg, - "restrictedZones", restrictedCfg + cfg.setZoneGroupReportStrategies(Map.of( + "allowedZones", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, + "restrictedZones", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS )); // Output to server attributes @@ -728,14 +719,16 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes .atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { - ArrayNode attrs = getServerAttributes(device.getId(), "allowedZoneEvent", "restrictedZoneEvent"); - assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); + ArrayNode attrs = getServerAttributes(device.getId(), + "allowedZonesEvent", "allowedZonesStatus", "restrictedZonesStatus"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(3); Map m = kv(attrs); - assertThat(m).containsEntry("allowedZoneEvent", "ENTERED") - .containsEntry("restrictedZoneEvent", "OUTSIDE"); + assertThat(m).containsEntry("allowedZonesEvent", "ENTERED") + .containsEntry("allowedZonesStatus", "INSIDE") + .containsEntry("restrictedZonesStatus", "OUTSIDE"); }); - // --- Move device into Restricted zone (and outside Allowed) --- + // --- Move the device into Restricted zone (and outside Allowed) --- doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", JacksonUtil.toJsonNode("{\"latitude\":50.4760,\"longitude\":30.5110}")); @@ -744,11 +737,15 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes .atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { - ArrayNode attrs = getServerAttributes(device.getId(), "allowedZoneEvent", "restrictedZoneEvent"); - assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); + ArrayNode attrs = getServerAttributes(device.getId(), + "allowedZonesEvent", "allowedZonesStatus", + "restrictedZonesEvent", "restrictedZonesStatus"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(4); Map m = kv(attrs); - assertThat(m).containsEntry("allowedZoneEvent", "LEFT") - .containsEntry("restrictedZoneEvent", "ENTERED"); + assertThat(m).containsEntry("allowedZonesEvent", "LEFT") + .containsEntry("restrictedZonesEvent", "ENTERED") + .containsEntry("allowedZonesStatus", "OUTSIDE") + .containsEntry("restrictedZonesStatus", "INSIDE"); }); } @@ -821,10 +818,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes "allowedZones", allowedZones )); - // Report all events for the group - List reportEvents = Arrays.stream(GeofencingEvent.values()).toList(); - GeofencingZoneGroupConfiguration allowedCfg = new GeofencingZoneGroupConfiguration("allowedZone", reportEvents); - cfg.setZoneGroupConfigurations(Map.of("allowedZones", allowedCfg)); + // Report all for the group + cfg.setZoneGroupReportStrategies(Map.of("allowedZones", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS)); // Server attributes output Output out = new Output(); @@ -845,10 +840,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes .atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { - ArrayNode attrs = getServerAttributes(device.getId(), "allowedZoneEvent"); - assertThat(attrs).isNotNull().isNotEmpty().hasSize(1); + ArrayNode attrs = getServerAttributes(device.getId(), "allowedZonesEvent", "allowedZonesStatus"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); Map m = kv(attrs); - assertThat(m).containsEntry("allowedZoneEvent", "ENTERED"); + assertThat(m).containsEntry("allowedZonesEvent", "ENTERED") + .containsEntry("allowedZonesStatus", "INSIDE"); }); // --- Move device OUTSIDE Zone A (expect LEFT) --- @@ -859,10 +855,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes .atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) .untilAsserted(() -> { - ArrayNode attrs = getServerAttributes(device.getId(), "allowedZoneEvent"); - assertThat(attrs).isNotNull().isNotEmpty().hasSize(1); + ArrayNode attrs = getServerAttributes(device.getId(), "allowedZonesEvent", "allowedZonesStatus"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); Map m = kv(attrs); - assertThat(m).containsEntry("allowedZoneEvent", "LEFT"); + assertThat(m).containsEntry("allowedZonesEvent", "LEFT") + .containsEntry("allowedZonesStatus", "OUTSIDE"); }); // --- Create Allowed Zone B covering the CURRENT location --- @@ -892,10 +889,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes .atMost(TIMEOUT, TimeUnit.SECONDS) .pollInterval(1, TimeUnit.SECONDS) .untilAsserted(() -> { - ArrayNode attrs = getServerAttributes(device.getId(), "allowedZoneEvent"); - assertThat(attrs).isNotNull().isNotEmpty().hasSize(1); + ArrayNode attrs = getServerAttributes(device.getId(), "allowedZonesEvent", "allowedZonesStatus"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); Map m = kv(attrs); - assertThat(m).containsEntry("allowedZoneEvent", "ENTERED"); + assertThat(m).containsEntry("allowedZonesEvent", "ENTERED") + .containsEntry("allowedZonesStatus", "INSIDE"); }); } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index fe9444461e..c6be855c91 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -29,8 +29,6 @@ import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; -import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; @@ -47,7 +45,6 @@ import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.service.cf.CalculatedFieldResult; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -63,6 +60,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; @ExtendWith(MockitoExtension.class) public class GeofencingCalculatedFieldStateTest { @@ -335,10 +333,7 @@ public class GeofencingCalculatedFieldStateTest { config.setArguments(Map.of("latitude", argument1, "longitude", argument2, "allowedZones", argument3, "restrictedZones", argument4)); - List reportEvents = Arrays.stream(GeofencingEvent.values()).toList(); - GeofencingZoneGroupConfiguration allowedZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", reportEvents); - GeofencingZoneGroupConfiguration restrictedZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("restrictedZone", reportEvents); - config.setZoneGroupConfigurations(Map.of("allowedZones", allowedZoneGroupConfiguration, "restrictedZones", restrictedZoneGroupConfiguration)); + config.setZoneGroupReportStrategies(Map.of("allowedZones", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, "restrictedZones", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS)); config.setCreateRelationsWithMatchedZones(true); config.setZoneRelationType("CurrentZone"); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java index e31e62deef..e5f95b97c5 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java @@ -21,11 +21,14 @@ import org.thingsboard.common.util.geo.Coordinates; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry; -import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.INSIDE; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.OUTSIDE; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent.ENTERED; +import static org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent.LEFT; public class GeofencingZoneStateTest { @@ -45,18 +48,18 @@ public class GeofencingZoneStateTest { void evaluate_initialInside_thenInsideAgain() { var inside = new Coordinates(50.4730, 30.5050); // first evaluation: no prior state -> ENTERED - assertThat(state.evaluate(inside)).isEqualTo(GeofencingEvent.ENTERED); + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(ENTERED, INSIDE)); // same position again -> INSIDE (steady state) - assertThat(state.evaluate(inside)).isEqualTo(GeofencingEvent.INSIDE); + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE)); } @Test void evaluate_initialOutside_thenOutsideAgain() { var outside = new Coordinates(50.4760, 30.5110); // first evaluation: no prior state -> OUTSIDE - assertThat(state.evaluate(outside)).isEqualTo(GeofencingEvent.OUTSIDE); + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE)); // same position again -> OUTSIDE (steady state) - assertThat(state.evaluate(outside)).isEqualTo(GeofencingEvent.OUTSIDE); + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE)); } @Test @@ -64,11 +67,11 @@ public class GeofencingZoneStateTest { var inside = new Coordinates(50.4730, 30.5050); var outside = new Coordinates(50.4760, 30.5110); // enter - assertThat(state.evaluate(inside)).isEqualTo(GeofencingEvent.ENTERED); + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(ENTERED, INSIDE)); // leave -> LEFT - assertThat(state.evaluate(outside)).isEqualTo(GeofencingEvent.LEFT); + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(LEFT, OUTSIDE)); // still outside -> OUTSIDE - assertThat(state.evaluate(outside)).isEqualTo(GeofencingEvent.OUTSIDE); + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE)); } @Test @@ -76,11 +79,11 @@ public class GeofencingZoneStateTest { var outside = new Coordinates(50.4760, 30.5110); var inside = new Coordinates(50.4730, 30.5050); // start outside - assertThat(state.evaluate(outside)).isEqualTo(GeofencingEvent.OUTSIDE); + assertThat(state.evaluate(outside)).isEqualTo(new GeofencingEvalResult(null, OUTSIDE)); // cross boundary -> ENTERED - assertThat(state.evaluate(inside)).isEqualTo(GeofencingEvent.ENTERED); + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(ENTERED, INSIDE)); // remain inside -> INSIDE - assertThat(state.evaluate(inside)).isEqualTo(GeofencingEvent.INSIDE); + assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE)); } } diff --git a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java index 3d51420f2e..9cae7f59d6 100644 --- a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java @@ -18,6 +18,7 @@ package org.thingsboard.server.utils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.DeviceId; @@ -76,7 +77,7 @@ class CalculatedFieldUtilsTest { BaseAttributeKvEntry zone2PerimeterAttribute = new BaseAttributeKvEntry(zone2, System.currentTimeMillis(), 0L); GeofencingZoneState s1 = new GeofencingZoneState(z1, zone1PerimeterAttribute); - s1.setInside(true); + s1.setLastPresence(GeofencingPresenceStatus.INSIDE); GeofencingZoneState s2 = new GeofencingZoneState(z2, zone2PerimeterAttribute); zoneStates.put(z1, s1); @@ -101,8 +102,8 @@ class CalculatedFieldUtilsTest { assertThat(fromProtoArgument).isInstanceOf(GeofencingArgumentEntry.class); GeofencingArgumentEntry fromProtoGeoArgument = (GeofencingArgumentEntry) fromProtoArgument; assertThat(fromProtoGeoArgument.getZoneStates()).hasSize(2); - assertThat(fromProtoGeoArgument.getZoneStates().get(z1).getInside()).isTrue(); - assertThat(fromProtoGeoArgument.getZoneStates().get(z2).getInside()).isNull(); + assertThat(fromProtoGeoArgument.getZoneStates().get(z1).getLastPresence()).isEqualTo(GeofencingPresenceStatus.INSIDE); + assertThat(fromProtoGeoArgument.getZoneStates().get(z2).getLastPresence()).isNull(); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java index 34b2820cd0..2edd5cc07a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java @@ -20,9 +20,7 @@ import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.relation.EntitySearchDirection; -import org.thingsboard.server.common.data.util.CollectionsUtil; -import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -44,7 +42,7 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC private boolean createRelationsWithMatchedZones; private String zoneRelationType; private EntitySearchDirection zoneRelationDirection; - private Map zoneGroupConfigurations; + private Map zoneGroupReportStrategies; @Override public CalculatedFieldType getType() { @@ -56,7 +54,6 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC return scheduledUpdateIntervalSec > 0 && arguments.values().stream().anyMatch(Argument::hasDynamicSource); } - // TODO: update validate method in PE version. @Override public void validate() { if (arguments == null) { @@ -85,25 +82,13 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC } private void validateZoneGroupConfigurations(Map zoneGroupsArguments) { - if (zoneGroupConfigurations == null || zoneGroupConfigurations.isEmpty()) { + if (zoneGroupReportStrategies == null || zoneGroupReportStrategies.isEmpty()) { throw new IllegalArgumentException("Zone groups configuration should be specified!"); } - Set usedPrefixes = new HashSet<>(); - zoneGroupsArguments.forEach((zoneGroupName, zoneGroupArgument) -> { - GeofencingZoneGroupConfiguration config = zoneGroupConfigurations.get(zoneGroupName); - if (config == null) { - throw new IllegalArgumentException("Zone group configuration is not configured for '" + zoneGroupName + "' argument!"); - } - if (CollectionsUtil.isEmpty(config.getReportEvents())) { - throw new IllegalArgumentException("Zone group configuration report events must be specified for '" + zoneGroupName + "' argument!"); - } - String prefix = config.getReportTelemetryPrefix(); - if (StringUtils.isBlank(prefix)) { - throw new IllegalArgumentException("Report telemetry prefix should be specified for '" + zoneGroupName + "' argument!"); - } - if (!usedPrefixes.add(prefix)) { - throw new IllegalArgumentException("Duplicate report telemetry prefix found: '" + prefix + "'. Must be unique!"); + GeofencingReportStrategy geofencingReportStrategy = zoneGroupReportStrategies.get(zoneGroupName); + if (geofencingReportStrategy == null) { + throw new IllegalArgumentException("Zone group report strategy is not configured for '" + zoneGroupName + "' argument!"); } }); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java index d770520daa..ca3b91baec 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java @@ -15,16 +15,5 @@ */ package org.thingsboard.server.common.data.cf.configuration; -import lombok.Getter; - -@Getter -public enum GeofencingEvent { - - ENTERED(true), LEFT(true), INSIDE(false), OUTSIDE(false); - - private final boolean transitionEvent; - - GeofencingEvent(boolean transitionEvent) { - this.transitionEvent = transitionEvent; - } -} +public sealed interface GeofencingEvent + permits GeofencingTransitionEvent, GeofencingPresenceStatus { } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingZoneGroupConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingPresenceStatus.java similarity index 77% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingZoneGroupConfiguration.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingPresenceStatus.java index c82151fc64..3e88744132 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingZoneGroupConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingPresenceStatus.java @@ -15,14 +15,11 @@ */ package org.thingsboard.server.common.data.cf.configuration; -import lombok.Data; +import lombok.Getter; -import java.util.List; +@Getter +public enum GeofencingPresenceStatus implements GeofencingEvent { -@Data -public class GeofencingZoneGroupConfiguration { - - private final String reportTelemetryPrefix; - private final List reportEvents; + INSIDE, OUTSIDE; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingReportStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingReportStrategy.java new file mode 100644 index 0000000000..774e725650 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingReportStrategy.java @@ -0,0 +1,24 @@ +/** + * 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.common.data.cf.configuration; + +public enum GeofencingReportStrategy { + + REPORT_TRANSITION_EVENTS_ONLY, + REPORT_PRESENCE_STATUS_ONLY, + REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingTransitionEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingTransitionEvent.java new file mode 100644 index 0000000000..edd747587e --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingTransitionEvent.java @@ -0,0 +1,20 @@ +/** + * 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.common.data.cf.configuration; + +public enum GeofencingTransitionEvent implements GeofencingEvent { + ENTERED, LEFT +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java index 44e6365b2e..23e4dbefca 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java @@ -26,7 +26,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.relation.EntitySearchDirection; -import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; @@ -224,11 +223,11 @@ public class GeofencingCalculatedFieldConfigurationTest { allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); arguments.put("allowedZones", allowedZonesArg); - GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); - Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); + Map zoneGroupReportStrategies = + Map.of("allowedZones", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS); cfg.setArguments(arguments); - cfg.setZoneGroupConfigurations(zoneGroupConfigurations); + cfg.setZoneGroupReportStrategies(zoneGroupReportStrategies); cfg.validate(); @@ -247,7 +246,7 @@ public class GeofencingCalculatedFieldConfigurationTest { arguments.put("allowedZones", allowedZonesArg); cfg.setArguments(arguments); - cfg.setZoneGroupConfigurations(null); + cfg.setZoneGroupReportStrategies(null); cfg.setCreateRelationsWithMatchedZones(false); assertThatThrownBy(cfg::validate) @@ -256,100 +255,26 @@ public class GeofencingCalculatedFieldConfigurationTest { } @Test - void validateShouldThrowWhenReportTelemetryPrefixDuplicate() { + void validateShouldThrowWhenZoneGroupArgumentReportStrategyIsMissing() { var cfg = new GeofencingCalculatedFieldConfiguration(); var arguments = new HashMap(); arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); Argument allowedZonesArg = toArgument("allowedZone", ArgumentType.ATTRIBUTE); - Argument restrictedZonesArg = toArgument("restrictedZone", ArgumentType.ATTRIBUTE); var refDynamicSourceConfigurationMock = mock(RelationQueryDynamicSourceConfiguration.class); allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); arguments.put("allowedZones", allowedZonesArg); - arguments.put("restrictedZones", restrictedZonesArg); - GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("theSamePrefixTest", Arrays.asList(GeofencingEvent.values())); - GeofencingZoneGroupConfiguration restrictedZoneConfiguration = new GeofencingZoneGroupConfiguration("theSamePrefixTest", Arrays.asList(GeofencingEvent.values())); - Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration, "restrictedZones", restrictedZoneConfiguration); + Map zoneGroupReportStrategies = + Map.of("someOtherZones", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS); cfg.setArguments(arguments); - cfg.setZoneGroupConfigurations(zoneGroupConfigurations); + cfg.setZoneGroupReportStrategies(zoneGroupReportStrategies); cfg.setCreateRelationsWithMatchedZones(false); assertThatThrownBy(cfg::validate) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Duplicate report telemetry prefix found: 'theSamePrefixTest'. Must be unique!"); - } - - @Test - void validateShouldThrowWhenZoneGroupArgumentConfigurationIsMissing() { - var cfg = new GeofencingCalculatedFieldConfiguration(); - var arguments = new HashMap(); - arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); - arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); - Argument allowedZonesArg = toArgument("allowedZone", ArgumentType.ATTRIBUTE); - var refDynamicSourceConfigurationMock = mock(RelationQueryDynamicSourceConfiguration.class); - allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); - arguments.put("allowedZones", allowedZonesArg); - - GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); - Map zoneGroupConfigurations = Map.of("someOtherZones", allowedZoneConfiguration); - - cfg.setArguments(arguments); - cfg.setZoneGroupConfigurations(zoneGroupConfigurations); - cfg.setCreateRelationsWithMatchedZones(false); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Zone group configuration is not configured for 'allowedZones' argument!"); - } - - @Test - void validateShouldThrowWhenZoneGroupConfigurationReportEventsAreNotSpecified() { - var cfg = new GeofencingCalculatedFieldConfiguration(); - var arguments = new HashMap(); - arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); - arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); - Argument allowedZonesArg = toArgument("allowedZone", ArgumentType.ATTRIBUTE); - var refDynamicSourceConfigurationMock = mock(RelationQueryDynamicSourceConfiguration.class); - allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); - arguments.put("allowedZones", allowedZonesArg); - - GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", null); - Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); - - cfg.setArguments(arguments); - cfg.setZoneGroupConfigurations(zoneGroupConfigurations); - cfg.setCreateRelationsWithMatchedZones(false); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Zone group configuration report events must be specified for 'allowedZones' argument!"); - } - - @ParameterizedTest - @NullAndEmptySource - @ValueSource(strings = " ") - void validateShouldThrowWhenZoneGroupConfigurationTelemetryPrefixIsBlankOrNull(String reportTelemetryPrefix) { - var cfg = new GeofencingCalculatedFieldConfiguration(); - var arguments = new HashMap(); - arguments.put(ENTITY_ID_LATITUDE_ARGUMENT_KEY, toArgument("latitude", ArgumentType.TS_LATEST)); - arguments.put(ENTITY_ID_LONGITUDE_ARGUMENT_KEY, toArgument("longitude", ArgumentType.TS_LATEST)); - Argument allowedZonesArg = toArgument("allowedZone", ArgumentType.ATTRIBUTE); - var refDynamicSourceConfigurationMock = mock(RelationQueryDynamicSourceConfiguration.class); - allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); - arguments.put("allowedZones", allowedZonesArg); - - GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration(reportTelemetryPrefix, Arrays.asList(GeofencingEvent.values())); - Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); - - cfg.setArguments(arguments); - cfg.setZoneGroupConfigurations(zoneGroupConfigurations); - cfg.setCreateRelationsWithMatchedZones(false); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Report telemetry prefix should be specified for 'allowedZones' argument!"); + .hasMessage("Zone group report strategy is not configured for 'allowedZones' argument!"); } @ParameterizedTest @@ -365,11 +290,11 @@ public class GeofencingCalculatedFieldConfigurationTest { allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); arguments.put("allowedZones", allowedZonesArg); - GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); - Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); + Map zoneGroupReportStrategies = + Map.of("allowedZones", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS); cfg.setArguments(arguments); - cfg.setZoneGroupConfigurations(zoneGroupConfigurations); + cfg.setZoneGroupReportStrategies(zoneGroupReportStrategies); cfg.setCreateRelationsWithMatchedZones(true); cfg.setZoneRelationType(zoneRelationType); cfg.setZoneRelationDirection(EntitySearchDirection.TO); @@ -390,11 +315,11 @@ public class GeofencingCalculatedFieldConfigurationTest { allowedZonesArg.setRefDynamicSourceConfiguration(refDynamicSourceConfigurationMock); arguments.put("allowedZones", allowedZonesArg); - GeofencingZoneGroupConfiguration allowedZoneConfiguration = new GeofencingZoneGroupConfiguration("allowedZone", Arrays.asList(GeofencingEvent.values())); - Map zoneGroupConfigurations = Map.of("allowedZones", allowedZoneConfiguration); + Map zoneGroupReportStrategies = + Map.of("allowedZones", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS); cfg.setArguments(arguments); - cfg.setZoneGroupConfigurations(zoneGroupConfigurations); + cfg.setZoneGroupReportStrategies(zoneGroupReportStrategies); cfg.setCreateRelationsWithMatchedZones(true); cfg.setZoneRelationType("SomeRelationType"); @@ -448,8 +373,9 @@ public class GeofencingCalculatedFieldConfigurationTest { args.put("allowedZones", allowed); cfg.setArguments(args); - var zc = new GeofencingZoneGroupConfiguration("gf_allowed", Arrays.asList(GeofencingEvent.values())); - cfg.setZoneGroupConfigurations(Map.of("allowedZones", zc)); + Map zoneGroupReportStrategies = + Map.of("allowedZones", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS); + cfg.setZoneGroupReportStrategies(zoneGroupReportStrategies); cfg.setCreateRelationsWithMatchedZones(true); cfg.setZoneRelationType("Contains"); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index 3c6a30ca5c..81dbe7b799 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java @@ -29,8 +29,7 @@ import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.GeofencingEvent; -import org.thingsboard.server.common.data.cf.configuration.GeofencingZoneGroupConfiguration; +import org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; @@ -44,7 +43,6 @@ import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; -import java.util.Arrays; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; @@ -127,8 +125,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { )); // Matching zone-group configuration - var geofencingZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("gf_allowed", Arrays.asList(GeofencingEvent.values())); - cfg.setZoneGroupConfigurations(Map.of("allowed", geofencingZoneGroupConfiguration)); + cfg.setZoneGroupReportStrategies(Map.of("allowed", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS)); // Set a scheduled interval to some value cfg.setScheduledUpdateIntervalSec(600); @@ -187,8 +184,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { )); // Matching zone-group configuration - var geofencingZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("gf_allowed", Arrays.asList(GeofencingEvent.values())); - cfg.setZoneGroupConfigurations(Map.of("allowed", geofencingZoneGroupConfiguration)); + cfg.setZoneGroupReportStrategies(Map.of("allowed", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS)); // Enable scheduling with an interval below tenant min cfg.setScheduledUpdateIntervalSec(600); @@ -248,8 +244,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { )); // Matching zone-group configuration - var geofencingZoneGroupConfiguration = new GeofencingZoneGroupConfiguration("gf_allowed", Arrays.asList(GeofencingEvent.values())); - cfg.setZoneGroupConfigurations(Map.of("allowed", geofencingZoneGroupConfiguration)); + cfg.setZoneGroupReportStrategies(Map.of("allowed", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS)); // Get tenant profile min. int min = tbTenantProfileCache.get(tenantId) From 29934d08bd14c218c4259553eb85a376bff32ab3 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Mon, 18 Aug 2025 16:06:05 +0300 Subject: [PATCH 063/216] Added custom serializer/deserializer logic for perimeter definitions --- .../cf/CalculatedFieldIntegrationTest.java | 16 ++---- .../GeofencingCalculatedFieldStateTest.java | 18 ++++--- .../GeofencingValueArgumentEntryTest.java | 21 +++----- .../cf/ctx/state/GeofencingZoneStateTest.java | 4 +- .../utils/CalculatedFieldUtilsTest.java | 4 +- ...eofencingCalculatedFieldConfiguration.java | 2 +- ...ncingCalculatedFieldConfigurationTest.java | 2 +- .../util/geo/CirclePerimeterDefinition.java | 12 ++--- .../common/util/geo/PerimeterDefinition.java | 13 ++--- .../geo/PerimeterDefinitionDeserializer.java | 48 +++++++++++++++++ .../geo/PerimeterDefinitionSerializer.java | 49 +++++++++++++++++ .../util/geo/PolygonPerimeterDefinition.java | 4 +- .../PerimeterDefinitionDeserializerTest.java | 50 ++++++++++++++++++ .../PerimeterDefinitionSerializerTest.java | 52 +++++++++++++++++++ 14 files changed, 236 insertions(+), 59 deletions(-) create mode 100644 common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializer.java create mode 100644 common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializer.java create mode 100644 common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializerTest.java create mode 100644 common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializerTest.java diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index c2ff2342ab..b7f32f2506 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -622,13 +622,9 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes Device device = createDevice("GF Device", "sn-geo-1"); // Allowed zone polygon (square) - String allowedPolygon = """ - {"type":"POLYGON","polygonsDefinition":"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"} - """; + String allowedPolygon = "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"; // Restricted zone polygon (square) - String restrictedPolygon = """ - {"type":"POLYGON","polygonsDefinition":"[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"} - """; + String restrictedPolygon = "[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"; Asset allowedZoneAsset = createAsset("Allowed Zone", null); doPost("/api/plugins/telemetry/ASSET/" + allowedZoneAsset.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, @@ -768,9 +764,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes Device device = createDevice("GF Device dyn", "sn-geo-dyn-1"); // Allowed Zone A: covers initial point (ENTERED) - String allowedPolygonA = """ - {"type":"POLYGON","polygonsDefinition":"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"} - """; + String allowedPolygonA = "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"; Asset allowedZoneA = createAsset("Allowed Zone A", null); doPost("/api/plugins/telemetry/ASSET/" + allowedZoneA.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, @@ -863,9 +857,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); // --- Create Allowed Zone B covering the CURRENT location --- - String allowedPolygonB = """ - {"type":"POLYGON","polygonsDefinition":"[[50.475500, 30.510500], [50.475500, 30.511500], [50.476500, 30.511500], [50.476500, 30.510500]]"} - """; + String allowedPolygonB = "[[50.475500, 30.510500], [50.475500, 30.511500], [50.476500, 30.511500], [50.476500, 30.510500]]"; Asset allowedZoneB = createAsset("Allowed Zone B", null); doPost("/api/plugins/telemetry/ASSET/" + allowedZoneB.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index c6be855c91..3adc26f242 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -73,13 +73,11 @@ public class GeofencingCalculatedFieldStateTest { private final SingleValueArgumentEntry latitudeArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 10, new DoubleDataEntry("latitude", 50.4730), 145L); private final SingleValueArgumentEntry longitudeArgEntry = new SingleValueArgumentEntry(System.currentTimeMillis() - 6, new DoubleDataEntry("longitude", 30.5050), 165L); - private final JsonDataEntry allowedZoneDataEntry = new JsonDataEntry("zone", """ - {"type":"POLYGON","polygonsDefinition":"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"}"""); + private final JsonDataEntry allowedZoneDataEntry = new JsonDataEntry("zone", "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"); private final BaseAttributeKvEntry allowedZoneAttributeKvEntry = new BaseAttributeKvEntry(allowedZoneDataEntry, System.currentTimeMillis(), 0L); private final GeofencingArgumentEntry geofencingAllowedZoneArgEntry = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, allowedZoneAttributeKvEntry)); - private final JsonDataEntry restrictedZoneDataEntry = new JsonDataEntry("zone", """ - {"type":"POLYGON","polygonsDefinition":"[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"}"""); + private final JsonDataEntry restrictedZoneDataEntry = new JsonDataEntry("zone", "[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"); private final BaseAttributeKvEntry restrictedZoneAttributeKvEntry = new BaseAttributeKvEntry(restrictedZoneDataEntry, System.currentTimeMillis(), 0L); private final GeofencingArgumentEntry geofencingRestrictedZoneArgEntry = new GeofencingArgumentEntry(Map.of(ZONE_2_ID, restrictedZoneAttributeKvEntry)); @@ -219,6 +217,7 @@ public class GeofencingCalculatedFieldStateTest { assertThat(state.isReady()).isFalse(); } + // TODO: test different reporting strategies @Test void testPerformCalculation() throws ExecutionException, InterruptedException { state.arguments = new HashMap<>(Map.of( @@ -241,8 +240,9 @@ public class GeofencingCalculatedFieldStateTest { assertThat(result.getScope()).isEqualTo(output.getScope()); assertThat(result.getResult()).isEqualTo( JacksonUtil.newObjectNode() - .put("allowedZoneEvent", "ENTERED") - .put("restrictedZoneEvent", "OUTSIDE") + .put("allowedZonesEvent", "ENTERED") + .put("allowedZonesStatus", "INSIDE") + .put("restrictedZonesStatus", "OUTSIDE") ); SingleValueArgumentEntry newLatitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 146L); @@ -258,8 +258,10 @@ public class GeofencingCalculatedFieldStateTest { assertThat(result2.getScope()).isEqualTo(output.getScope()); assertThat(result2.getResult()).isEqualTo( JacksonUtil.newObjectNode() - .put("allowedZoneEvent", "LEFT") - .put("restrictedZoneEvent", "ENTERED") + .put("allowedZonesEvent", "LEFT") + .put("allowedZonesStatus", "OUTSIDE") + .put("restrictedZonesEvent", "ENTERED") + .put("restrictedZonesStatus", "INSIDE") ); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java index aad9c4e29c..87ef9bf0a1 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java @@ -36,12 +36,10 @@ public class GeofencingValueArgumentEntryTest { private final AssetId ZONE_1_ID = new AssetId(UUID.fromString("c0e3031c-7df1-45e4-9590-cfd621a4d714")); private final AssetId ZONE_2_ID = new AssetId(UUID.fromString("e7da6200-2096-4038-a343-ade9ea4fa3e4")); - private final JsonDataEntry allowedZoneDataEntry = new JsonDataEntry("zone", """ - {"type":"POLYGON","polygonsDefinition":"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"}"""); + private final JsonDataEntry allowedZoneDataEntry = new JsonDataEntry("zone", "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"); private final BaseAttributeKvEntry allowedZoneAttributeKvEntry = new BaseAttributeKvEntry(allowedZoneDataEntry, 363L, 155L); - private final JsonDataEntry restrictedZoneDataEntry = new JsonDataEntry("zone", """ - {"type":"POLYGON","polygonsDefinition":"[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"}"""); + private final JsonDataEntry restrictedZoneDataEntry = new JsonDataEntry("zone", "[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"); private final BaseAttributeKvEntry restrictedZoneAttributeKvEntry = new BaseAttributeKvEntry(restrictedZoneDataEntry, 363L, 155L); private GeofencingArgumentEntry entry; @@ -72,8 +70,7 @@ public class GeofencingValueArgumentEntryTest { @Test void testUpdateEntryWithTheSameTs() { - BaseAttributeKvEntry differentValueSameTs = new BaseAttributeKvEntry(new JsonDataEntry("zone", """ - {"type":"POLYGON","polygonsDefinition":"[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"}"""), 363L, 156L); + BaseAttributeKvEntry differentValueSameTs = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 363L, 156L); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueSameTs, ZONE_2_ID, restrictedZoneAttributeKvEntry)); assertThat(entry.updateEntry(updated)).isFalse(); } @@ -81,8 +78,7 @@ public class GeofencingValueArgumentEntryTest { @Test @SuppressWarnings("unchecked") void testUpdateEntryWhenNewVersionIsNull() { - BaseAttributeKvEntry differentValueNewVersionIsNull = new BaseAttributeKvEntry(new JsonDataEntry("zone", """ - {"type":"POLYGON","polygonsDefinition":"[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"}"""), 364L, null); + BaseAttributeKvEntry differentValueNewVersionIsNull = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, null); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsNull, ZONE_2_ID, restrictedZoneAttributeKvEntry)); assertThat(entry.updateEntry(updated)).isTrue(); @@ -104,8 +100,7 @@ public class GeofencingValueArgumentEntryTest { @Test @SuppressWarnings("unchecked") void testUpdateEntryWhenNewVersionIsGreaterThanCurrent() { - BaseAttributeKvEntry differentValueNewVersionIsSet = new BaseAttributeKvEntry(new JsonDataEntry("zone", """ - {"type":"POLYGON","polygonsDefinition":"[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"}"""), 364L, 156L); + BaseAttributeKvEntry differentValueNewVersionIsSet = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, 156L); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsSet, ZONE_2_ID, restrictedZoneAttributeKvEntry)); assertThat(entry.updateEntry(updated)).isTrue(); @@ -126,8 +121,7 @@ public class GeofencingValueArgumentEntryTest { @Test void testUpdateEntryWhenNewVersionIsLessThanCurrent() { - BaseAttributeKvEntry differentValueNewVersionIsSet = new BaseAttributeKvEntry(new JsonDataEntry("zone", """ - {"type":"POLYGON","polygonsDefinition":"[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"}"""), 364L, 154L); + BaseAttributeKvEntry differentValueNewVersionIsSet = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, 154L); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, differentValueNewVersionIsSet, ZONE_2_ID, restrictedZoneAttributeKvEntry)); assertThat(entry.updateEntry(updated)).isFalse(); @@ -152,8 +146,7 @@ public class GeofencingValueArgumentEntryTest { @Test void testUpdateEntryWithNewZone() { final AssetId NEW_ZONE_ID = new AssetId(UUID.fromString("a3eacf1a-6af3-4e9f-87c4-502bb25c7dc3")); - BaseAttributeKvEntry newZone = new BaseAttributeKvEntry(new JsonDataEntry("zone", """ - {"type":"POLYGON","polygonsDefinition":"[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"}"""), 364L, 156L); + BaseAttributeKvEntry newZone = new BaseAttributeKvEntry(new JsonDataEntry("zone", "[[50.472001, 30.504001], [50.472001, 30.506001], [50.474001, 30.506001], [50.474001, 30.504001]]"), 364L, 156L); var updated = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, allowedZoneAttributeKvEntry, ZONE_2_ID, restrictedZoneAttributeKvEntry, NEW_ZONE_ID, newZone)); assertThat(entry.updateEntry(updated)).isTrue(); } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java index e5f95b97c5..3c26f2bb02 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java @@ -38,9 +38,7 @@ public class GeofencingZoneStateTest { @BeforeEach void setUp() { - String POLYGON = """ - {"type":"POLYGON","polygonsDefinition":"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"} - """; + String POLYGON = "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"; state = new GeofencingZoneState(ZONE_ID, new BaseAttributeKvEntry(new JsonDataEntry("zone", POLYGON), 100L, 1L)); } diff --git a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java index 9cae7f59d6..ac6d45ad47 100644 --- a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java @@ -70,8 +70,8 @@ class CalculatedFieldUtilsTest { AssetId z1 = new AssetId(zoneId1); AssetId z2 = new AssetId(zoneId2); - JsonDataEntry zone1 = new JsonDataEntry("zone", "{\"type\":\"POLYGON\",\"polygonsDefinition\":\"[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]\"}"); - JsonDataEntry zone2 = new JsonDataEntry("zone", "{\"type\":\"POLYGON\",\"polygonsDefinition\":\"[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]\"}"); + JsonDataEntry zone1 = new JsonDataEntry("zone", "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"); + JsonDataEntry zone2 = new JsonDataEntry("zone", "[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"); BaseAttributeKvEntry zone1PerimeterAttribute = new BaseAttributeKvEntry(zone1, System.currentTimeMillis(), 0L); BaseAttributeKvEntry zone2PerimeterAttribute = new BaseAttributeKvEntry(zone2, System.currentTimeMillis(), 0L); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java index 2edd5cc07a..4f71ef749e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java @@ -83,7 +83,7 @@ public class GeofencingCalculatedFieldConfiguration extends BaseCalculatedFieldC private void validateZoneGroupConfigurations(Map zoneGroupsArguments) { if (zoneGroupReportStrategies == null || zoneGroupReportStrategies.isEmpty()) { - throw new IllegalArgumentException("Zone groups configuration should be specified!"); + throw new IllegalArgumentException("Zone groups reporting strategies should be specified!"); } zoneGroupsArguments.forEach((zoneGroupName, zoneGroupArgument) -> { GeofencingReportStrategy geofencingReportStrategy = zoneGroupReportStrategies.get(zoneGroupName); diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java index 23e4dbefca..ebdd915c45 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java @@ -251,7 +251,7 @@ public class GeofencingCalculatedFieldConfigurationTest { assertThatThrownBy(cfg::validate) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Zone groups configuration should be specified!"); + .hasMessage("Zone groups reporting strategies should be specified!"); } @Test diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/CirclePerimeterDefinition.java b/common/util/src/main/java/org/thingsboard/common/util/geo/CirclePerimeterDefinition.java index 33035b016e..4d5390a27a 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/geo/CirclePerimeterDefinition.java +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/CirclePerimeterDefinition.java @@ -20,10 +20,9 @@ import lombok.Data; @Data public class CirclePerimeterDefinition implements PerimeterDefinition { - private Double centerLatitude; - private Double centerLongitude; - private Double range; - private RangeUnit rangeUnit; + private final Double latitude; + private final Double longitude; + private final Double radius; @Override public PerimeterType getType() { @@ -32,9 +31,8 @@ public class CirclePerimeterDefinition implements PerimeterDefinition { @Override public boolean checkMatches(Coordinates entityCoordinates) { - Coordinates perimeterCoordinates = new Coordinates(centerLatitude, centerLongitude); - return range > GeoUtil.distance(entityCoordinates, perimeterCoordinates, rangeUnit); + Coordinates perimeterCoordinates = new Coordinates(latitude, longitude); + return radius > GeoUtil.distance(entityCoordinates, perimeterCoordinates, RangeUnit.METER); } - } diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinition.java b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinition.java index 4cba6f9c8a..7c7f2cd1a3 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinition.java +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinition.java @@ -17,19 +17,14 @@ package org.thingsboard.common.util.geo; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import java.io.Serializable; -@JsonTypeInfo( - use = JsonTypeInfo.Id.NAME, - include = JsonTypeInfo.As.PROPERTY, - property = "type") -@JsonSubTypes({ - @JsonSubTypes.Type(value = PolygonPerimeterDefinition.class, name = "POLYGON"), - @JsonSubTypes.Type(value = CirclePerimeterDefinition.class, name = "CIRCLE")}) @JsonIgnoreProperties(ignoreUnknown = true) +@JsonDeserialize(using = PerimeterDefinitionDeserializer.class) +@JsonSerialize(using = PerimeterDefinitionSerializer.class) public interface PerimeterDefinition extends Serializable { @JsonIgnore diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializer.java b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializer.java new file mode 100644 index 0000000000..e60e314fe0 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializer.java @@ -0,0 +1,48 @@ +/** + * 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.common.util.geo; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; + +public class PerimeterDefinitionDeserializer extends JsonDeserializer { + + @Override + public PerimeterDefinition deserialize(JsonParser p, DeserializationContext ctx) throws IOException { + ObjectCodec codec = p.getCodec(); + JsonNode node = codec.readTree(p); + + if (node.isObject()) { + double latitude = node.get("latitude").asDouble(); + double longitude = node.get("longitude").asDouble(); + double radius = node.get("radius").asDouble(); + return new CirclePerimeterDefinition(latitude, longitude, radius); + } + if (node.isArray()) { + ObjectMapper mapper = (ObjectMapper) p.getCodec(); + String polygonStrDefinition = mapper.writeValueAsString(node); + return new PolygonPerimeterDefinition(polygonStrDefinition); + } + throw new IOException("Failed to deserialize PerimeterDefinition from node: " + node); + } + +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializer.java b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializer.java new file mode 100644 index 0000000000..386a7e67ff --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializer.java @@ -0,0 +1,49 @@ +/** + * 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.common.util.geo; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import org.thingsboard.server.common.data.StringUtils; + +import java.io.IOException; + +public class PerimeterDefinitionSerializer extends JsonSerializer { + + @Override + public void serialize(PerimeterDefinition value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value instanceof CirclePerimeterDefinition c) { + gen.writeStartObject(); + gen.writeNumberField("latitude", c.getLatitude()); + gen.writeNumberField("longitude", c.getLongitude()); + gen.writeNumberField("radius", c.getRadius()); + gen.writeEndObject(); + return; + } + if (value instanceof PolygonPerimeterDefinition p) { + String raw = p.getPolygonDefinition(); + if (StringUtils.isBlank(raw)) { + throw new IOException("Failed to serialize PolygonPerimeterDefinition with blank: " + value); + } + ObjectMapper mapper = (ObjectMapper) gen.getCodec(); + gen.writeTree(mapper.readTree(raw)); + return; + } + throw new IOException("Failed to serialize PerimeterDefinition from value: " + value); + } +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/PolygonPerimeterDefinition.java b/common/util/src/main/java/org/thingsboard/common/util/geo/PolygonPerimeterDefinition.java index b2259b5b07..2d8ca6ef56 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/geo/PolygonPerimeterDefinition.java +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/PolygonPerimeterDefinition.java @@ -20,7 +20,7 @@ import lombok.Data; @Data public class PolygonPerimeterDefinition implements PerimeterDefinition { - private String polygonsDefinition; + private final String polygonDefinition; @Override public PerimeterType getType() { @@ -29,7 +29,7 @@ public class PolygonPerimeterDefinition implements PerimeterDefinition { @Override public boolean checkMatches(Coordinates entityCoordinates) { - return GeoUtil.contains(polygonsDefinition, entityCoordinates); + return GeoUtil.contains(polygonDefinition, entityCoordinates); } } diff --git a/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializerTest.java b/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializerTest.java new file mode 100644 index 0000000000..5c00847861 --- /dev/null +++ b/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionDeserializerTest.java @@ -0,0 +1,50 @@ +/** + * 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.common.util.geo; + +import org.junit.jupiter.api.Test; +import org.thingsboard.common.util.JacksonUtil; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PerimeterDefinitionDeserializerTest { + + @Test + void shouldDeserializeCircle() { + String json = """ + {"latitude":50.45,"longitude":30.52,"radius":100.0}"""; + + PerimeterDefinition def = JacksonUtil.fromString(json, PerimeterDefinition.class); + + assertThat(def).isNotNull().isInstanceOf(CirclePerimeterDefinition.class); + + CirclePerimeterDefinition circle = (CirclePerimeterDefinition) def; + assertThat(circle.getLatitude()).isEqualTo(50.45); + assertThat(circle.getLongitude()).isEqualTo(30.52); + assertThat(circle.getRadius()).isEqualTo(100.0); + } + + @Test + void shouldDeserializePolygon() { + String json = "[[50.45,30.52],[50.46,30.53],[50.44,30.54]]"; + + PerimeterDefinition def = JacksonUtil.fromString(json, PerimeterDefinition.class); + + assertThat(def).isInstanceOf(PolygonPerimeterDefinition.class); + PolygonPerimeterDefinition poly = (PolygonPerimeterDefinition) def; + assertThat(poly.getPolygonDefinition()).isEqualTo(json); + } +} diff --git a/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializerTest.java b/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializerTest.java new file mode 100644 index 0000000000..d316d1c398 --- /dev/null +++ b/common/util/src/test/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializerTest.java @@ -0,0 +1,52 @@ +/** + * 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.common.util.geo; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.Test; +import org.thingsboard.common.util.JacksonUtil; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PerimeterDefinitionSerializerTest { + + @Test + void shouldSerializeCircle() { + PerimeterDefinition circle = new CirclePerimeterDefinition(50.45, 30.52, 120.0); + + String json = JacksonUtil.writeValueAsString(circle); + + JsonNode actual = JacksonUtil.toJsonNode(json); + assertThat(actual.get("latitude").asDouble()).isEqualTo(50.45); + assertThat(actual.get("longitude").asDouble()).isEqualTo(30.52); + assertThat(actual.get("radius").asDouble()).isEqualTo(120.0); + } + + @Test + void shouldSerializePolygon() throws Exception { + String rawArray = "[[50.45,30.52],[50.46,30.53],[50.44,30.54]]"; + PerimeterDefinition polygon = new PolygonPerimeterDefinition(rawArray); + + String json = JacksonUtil.writeValueAsString(polygon); + + JsonNode actual = JacksonUtil.toJsonNode(json); + JsonNode expected = JacksonUtil.toJsonNode(rawArray); + assertThat(actual).isEqualTo(expected); + assertThat(actual.isArray()).isTrue(); + assertThat(actual.size()).isEqualTo(3); + } + +} From e5d0733a3dce3e885eead29fa2dce1e5708af3d3 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Mon, 18 Aug 2025 18:04:39 +0300 Subject: [PATCH 064/216] Alarm Details field as JSON --- .../widget/lib/alarm/alarms-table-widget.component.ts | 3 +++ ui-ngx/src/app/shared/models/alarm.models.ts | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts index 3972ceecb0..968ad06a14 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.ts @@ -851,6 +851,9 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, content = this.defaultContent(key, contentInfo, value); } if (isDefined(content)) { + if (typeof content === 'object') { + content = JSON.stringify(content); + } content = this.utils.customTranslation(content, content); switch (typeof content) { case 'string': diff --git a/ui-ngx/src/app/shared/models/alarm.models.ts b/ui-ngx/src/app/shared/models/alarm.models.ts index 61407e1248..bcf0d7b75d 100644 --- a/ui-ngx/src/app/shared/models/alarm.models.ts +++ b/ui-ngx/src/app/shared/models/alarm.models.ts @@ -271,6 +271,11 @@ export const alarmFields: {[fieldName: string]: AlarmField} = { keyName: 'assignee', value: 'assignee', name: 'alarm.assignee' + }, + details: { + keyName: 'details', + value: 'details', + name: 'alarm.details' } }; From 1421f9cc9f373f83a02803646abc69a085a4f7a0 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Tue, 19 Aug 2025 11:43:19 +0300 Subject: [PATCH 065/216] Resolved TODOs, refactoring: make GeofencingCalculatedFieldState extends Base state class --- .../ctx/state/BaseCalculatedFieldState.java | 2 +- .../state/GeofencingCalculatedFieldState.java | 35 +--- .../cf/ctx/state/GeofencingEvalResult.java | 2 +- .../ctx/state/ScriptCalculatedFieldState.java | 6 +- .../ctx/state/SimpleCalculatedFieldState.java | 2 + application/src/main/resources/logback.xml | 2 +- .../GeofencingCalculatedFieldStateTest.java | 153 +++++++++++++++++- .../cf/ctx/state/GeofencingZoneStateTest.java | 80 +++++++++ .../data/cf/configuration/ArgumentTest.java | 2 +- 9 files changed, 243 insertions(+), 41 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index eb87d375c5..9a1d06cf24 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -93,7 +93,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { } } - protected abstract void validateNewEntry(ArgumentEntry newEntry); + protected void validateNewEntry(ArgumentEntry newEntry) {} private void updateLastUpdateTimestamp(ArgumentEntry entry) { long newTs = this.latestTimestamp; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java index b6b8d89928..80a0534cdf 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java @@ -19,8 +19,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import lombok.AllArgsConstructor; import lombok.Data; +import lombok.EqualsAndHashCode; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.geo.Coordinates; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @@ -30,8 +30,6 @@ import org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionE import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.service.cf.CalculatedFieldResult; -import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; -import org.thingsboard.server.utils.CalculatedFieldUtils; import java.util.ArrayList; import java.util.HashMap; @@ -45,24 +43,18 @@ import static org.thingsboard.server.common.data.cf.configuration.GeofencingPres import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.OUTSIDE; @Data -@AllArgsConstructor -public class GeofencingCalculatedFieldState implements CalculatedFieldState { - - private List requiredArguments; - Map arguments; - private boolean sizeExceedsLimit; - - private long latestTimestamp = -1; +@EqualsAndHashCode(callSuper = true) +public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { private boolean dirty; public GeofencingCalculatedFieldState() { - this(new ArrayList<>(), new HashMap<>(), false, -1, false); + super(new ArrayList<>(), new HashMap<>(), false, -1); + this.dirty = false; } public GeofencingCalculatedFieldState(List argNames) { - this.requiredArguments = argNames; - this.arguments = new HashMap<>(); + super(argNames); } @Override @@ -129,21 +121,6 @@ public class GeofencingCalculatedFieldState implements CalculatedFieldState { return calculateWithoutRelations(ctx, entityCoordinates, configuration); } - @Override - public boolean isReady() { - return arguments.keySet().containsAll(requiredArguments) && - arguments.values().stream().noneMatch(ArgumentEntry::isEmpty); - } - - @Override - public void checkStateSize(CalculatedFieldEntityCtxId ctxId, long maxStateSize) { - if (!sizeExceedsLimit && maxStateSize > 0 && CalculatedFieldUtils.toProto(ctxId, this).getSerializedSize() > maxStateSize) { - arguments.clear(); - sizeExceedsLimit = true; - } - } - - private ListenableFuture calculateWithRelations( EntityId entityId, CalculatedFieldCtx ctx, diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingEvalResult.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingEvalResult.java index 65b862b4f5..dff9a4d9ea 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingEvalResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingEvalResult.java @@ -20,4 +20,4 @@ import org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceSta import org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent; public record GeofencingEvalResult(@Nullable GeofencingTransitionEvent transition, - GeofencingPresenceStatus status) {} \ No newline at end of file + GeofencingPresenceStatus status) {} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java index e1f1305c48..fe7dfa04d0 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ScriptCalculatedFieldState.java @@ -20,6 +20,7 @@ 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.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.thingsboard.script.api.tbel.TbelCfArg; @@ -38,6 +39,7 @@ import java.util.Map; @Data @Slf4j @NoArgsConstructor +@EqualsAndHashCode(callSuper = true) public class ScriptCalculatedFieldState extends BaseCalculatedFieldState { public ScriptCalculatedFieldState(List requiredArguments) { @@ -49,10 +51,6 @@ public class ScriptCalculatedFieldState extends BaseCalculatedFieldState { return CalculatedFieldType.SCRIPT; } - @Override - protected void validateNewEntry(ArgumentEntry newEntry) { - } - @Override public ListenableFuture performCalculation(EntityId entityId, CalculatedFieldCtx ctx) { Map arguments = new LinkedHashMap<>(); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java index 76839a3cbc..80b650fc7c 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.script.api.tbel.TbUtils; @@ -34,6 +35,7 @@ import java.util.Map; @Data @NoArgsConstructor +@EqualsAndHashCode(callSuper = true) public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { public SimpleCalculatedFieldState(List requiredArguments) { diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml index 8e1a49faef..28a8b9fcdc 100644 --- a/application/src/main/resources/logback.xml +++ b/application/src/main/resources/logback.xml @@ -57,7 +57,7 @@ - + diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index 3adc26f242..73320b3aca 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; @@ -217,7 +218,6 @@ public class GeofencingCalculatedFieldStateTest { assertThat(state.isReady()).isFalse(); } - // TODO: test different reporting strategies @Test void testPerformCalculation() throws ExecutionException, InterruptedException { state.arguments = new HashMap<>(Map.of( @@ -264,6 +264,147 @@ public class GeofencingCalculatedFieldStateTest { .put("restrictedZonesStatus", "INSIDE") ); + // Check relations are created and deleted correctly for both iterations. + ArgumentCaptor saveCaptor = ArgumentCaptor.forClass(EntityRelation.class); + verify(relationService, times(2)).saveRelationAsync(eq(ctx.getTenantId()), saveCaptor.capture()); + List saveValues = saveCaptor.getAllValues(); + assertThat(saveValues).hasSize(2); + + EntityRelation relationFromFirstIteration = saveValues.get(0); + assertThat(relationFromFirstIteration.getTo()).isEqualTo(ctx.getEntityId()); + assertThat(relationFromFirstIteration.getFrom()).isEqualTo(ZONE_1_ID); + assertThat(relationFromFirstIteration.getType()).isEqualTo(configuration.getZoneRelationType()); + + EntityRelation relationFromSecondIteration = saveValues.get(1); + assertThat(relationFromSecondIteration.getTo()).isEqualTo(ctx.getEntityId()); + assertThat(relationFromSecondIteration.getFrom()).isEqualTo(ZONE_2_ID); + assertThat(relationFromSecondIteration.getType()).isEqualTo(configuration.getZoneRelationType()); + + ArgumentCaptor deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class); + verify(relationService).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); + EntityRelation leftRelation = deleteCaptor.getValue(); + assertThat(leftRelation.getFrom()).isEqualTo(ZONE_1_ID); + assertThat(leftRelation.getTo()).isEqualTo(ctx.getEntityId()); + } + + @Test + void testPerformCalculationWithOnlyTransitionEventsReportingStrategy() throws ExecutionException, InterruptedException { + state.arguments = new HashMap<>(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, + "allowedZones", geofencingAllowedZoneArgEntry, + "restrictedZones", geofencingRestrictedZoneArgEntry + )); + + Output output = ctx.getOutput(); + + var calculatedFieldConfig = getCalculatedFieldConfig(GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_ONLY); + + ctx.setCalculatedField(getCalculatedField(calculatedFieldConfig)); + ctx.init(); + + var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); + + when(relationService.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); + when(relationService.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); + + CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); + + assertThat(result).isNotNull(); + assertThat(result.getType()).isEqualTo(output.getType()); + assertThat(result.getScope()).isEqualTo(output.getScope()); + assertThat(result.getResult()).isEqualTo( + JacksonUtil.newObjectNode().put("allowedZonesEvent", "ENTERED") + ); + + SingleValueArgumentEntry newLatitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 146L); + SingleValueArgumentEntry newLongitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", 30.5110), 166L); + + // move the device to new coordinates → leaves allowed, enters restricted + state.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude)); + + CalculatedFieldResult result2 = state.performCalculation(ctx.getEntityId(), ctx).get(); + + assertThat(result2).isNotNull(); + assertThat(result2.getType()).isEqualTo(output.getType()); + assertThat(result2.getScope()).isEqualTo(output.getScope()); + assertThat(result2.getResult()).isEqualTo( + JacksonUtil.newObjectNode() + .put("allowedZonesEvent", "LEFT") + .put("restrictedZonesEvent", "ENTERED") + ); + + // Check relations are created and deleted correctly for both iterations. + ArgumentCaptor saveCaptor = ArgumentCaptor.forClass(EntityRelation.class); + verify(relationService, times(2)).saveRelationAsync(eq(ctx.getTenantId()), saveCaptor.capture()); + List saveValues = saveCaptor.getAllValues(); + assertThat(saveValues).hasSize(2); + + EntityRelation relationFromFirstIteration = saveValues.get(0); + assertThat(relationFromFirstIteration.getTo()).isEqualTo(ctx.getEntityId()); + assertThat(relationFromFirstIteration.getFrom()).isEqualTo(ZONE_1_ID); + assertThat(relationFromFirstIteration.getType()).isEqualTo(configuration.getZoneRelationType()); + + EntityRelation relationFromSecondIteration = saveValues.get(1); + assertThat(relationFromSecondIteration.getTo()).isEqualTo(ctx.getEntityId()); + assertThat(relationFromSecondIteration.getFrom()).isEqualTo(ZONE_2_ID); + assertThat(relationFromSecondIteration.getType()).isEqualTo(configuration.getZoneRelationType()); + + ArgumentCaptor deleteCaptor = ArgumentCaptor.forClass(EntityRelation.class); + verify(relationService).deleteRelationAsync(eq(ctx.getTenantId()), deleteCaptor.capture()); + EntityRelation leftRelation = deleteCaptor.getValue(); + assertThat(leftRelation.getFrom()).isEqualTo(ZONE_1_ID); + assertThat(leftRelation.getTo()).isEqualTo(ctx.getEntityId()); + } + + @Test + void testPerformCalculationWithOnlyPresenceStatusReportingStrategy() throws ExecutionException, InterruptedException { + state.arguments = new HashMap<>(Map.of( + ENTITY_ID_LATITUDE_ARGUMENT_KEY, latitudeArgEntry, + ENTITY_ID_LONGITUDE_ARGUMENT_KEY, longitudeArgEntry, + "allowedZones", geofencingAllowedZoneArgEntry, + "restrictedZones", geofencingRestrictedZoneArgEntry + )); + + Output output = ctx.getOutput(); + + var calculatedFieldConfig = getCalculatedFieldConfig(GeofencingReportStrategy.REPORT_PRESENCE_STATUS_ONLY); + + ctx.setCalculatedField(getCalculatedField(calculatedFieldConfig)); + ctx.init(); + + var configuration = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); + + when(relationService.saveRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); + when(relationService.deleteRelationAsync(any(), any())).thenReturn(Futures.immediateFuture(true)); + + CalculatedFieldResult result = state.performCalculation(ctx.getEntityId(), ctx).get(); + + assertThat(result).isNotNull(); + assertThat(result.getType()).isEqualTo(output.getType()); + assertThat(result.getScope()).isEqualTo(output.getScope()); + assertThat(result.getResult()).isEqualTo( + JacksonUtil.newObjectNode() + .put("allowedZonesStatus", "INSIDE") + .put("restrictedZonesStatus", "OUTSIDE") + ); + + SingleValueArgumentEntry newLatitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("latitude", 50.4760), 146L); + SingleValueArgumentEntry newLongitude = new SingleValueArgumentEntry(System.currentTimeMillis(), new DoubleDataEntry("longitude", 30.5110), 166L); + + // move the device to new coordinates → leaves allowed, enters restricted + state.updateState(ctx, Map.of(ENTITY_ID_LATITUDE_ARGUMENT_KEY, newLatitude, ENTITY_ID_LONGITUDE_ARGUMENT_KEY, newLongitude)); + + CalculatedFieldResult result2 = state.performCalculation(ctx.getEntityId(), ctx).get(); + + assertThat(result2).isNotNull(); + assertThat(result2.getType()).isEqualTo(output.getType()); + assertThat(result2.getScope()).isEqualTo(output.getScope()); + assertThat(result2.getResult()).isEqualTo( + JacksonUtil.newObjectNode() + .put("allowedZonesStatus", "OUTSIDE") + .put("restrictedZonesStatus", "INSIDE") + ); // Check relations are created and deleted correctly for both iterations. ArgumentCaptor saveCaptor = ArgumentCaptor.forClass(EntityRelation.class); @@ -289,18 +430,22 @@ public class GeofencingCalculatedFieldStateTest { } private CalculatedField getCalculatedField() { + return getCalculatedField(getCalculatedFieldConfig(REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS)); + } + + private CalculatedField getCalculatedField(CalculatedFieldConfiguration configuration) { CalculatedField calculatedField = new CalculatedField(); calculatedField.setTenantId(TENANT_ID); calculatedField.setEntityId(DEVICE_ID); calculatedField.setType(CalculatedFieldType.GEOFENCING); calculatedField.setName("Test Geofencing Calculated Field"); calculatedField.setConfigurationVersion(1); - calculatedField.setConfiguration(getCalculatedFieldConfig()); + calculatedField.setConfiguration(configuration); calculatedField.setVersion(1L); return calculatedField; } - private CalculatedFieldConfiguration getCalculatedFieldConfig() { + private CalculatedFieldConfiguration getCalculatedFieldConfig(GeofencingReportStrategy reportStrategy) { var config = new GeofencingCalculatedFieldConfiguration(); Argument argument1 = new Argument(); @@ -335,7 +480,7 @@ public class GeofencingCalculatedFieldStateTest { config.setArguments(Map.of("latitude", argument1, "longitude", argument2, "allowedZones", argument3, "restrictedZones", argument4)); - config.setZoneGroupReportStrategies(Map.of("allowedZones", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, "restrictedZones", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS)); + config.setZoneGroupReportStrategies(Map.of("allowedZones", reportStrategy, "restrictedZones", reportStrategy)); config.setCreateRelationsWithMatchedZones(true); config.setZoneRelationType("CurrentZone"); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java index 3c26f2bb02..3a6d0fa30d 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java @@ -84,4 +84,84 @@ public class GeofencingZoneStateTest { assertThat(state.evaluate(inside)).isEqualTo(new GeofencingEvalResult(null, INSIDE)); } + @Test + void update_withNewerVersion_updatesState_andResetsPresence() { + // arrange: establish a prior presence to ensure it’s reset on update + var inside = new Coordinates(50.4730, 30.5050); + assertThat(state.evaluate(inside)).isNotNull(); // sets lastPresence internally + + String NEW_POLYGON = "[[50.470000, 30.502000], [50.470000, 30.503000], [50.471000, 30.503000], [50.471000, 30.502000]]"; + GeofencingZoneState newer = new GeofencingZoneState( + ZONE_ID, + new BaseAttributeKvEntry(new JsonDataEntry("zone", NEW_POLYGON), 200L, 2L) + ); + + // act + boolean changed = state.update(newer); + + // assert + assertThat(changed).isTrue(); + assertThat(state.getTs()).isEqualTo(200L); + assertThat(state.getVersion()).isEqualTo(2L); + assertThat(state.getPerimeterDefinition()).isNotNull(); + assertThat(state.getLastPresence()).isNull(); // must be reset on successful update + } + + @Test + void update_withEqualVersion_doesNothing() { + // arrange: same version (1L) but different ts/polygon should still be ignored + String SOME_POLYGON = "[[50.472500, 30.504500], [50.472500, 30.505500], [50.473500, 30.505500], [50.473500, 30.504500]]"; + GeofencingZoneState sameVersion = new GeofencingZoneState( + ZONE_ID, + new BaseAttributeKvEntry(new JsonDataEntry("zone", SOME_POLYGON), 300L, 1L) + ); + + // act + boolean changed = state.update(sameVersion); + + // assert: nothing changes + assertThat(changed).isFalse(); + assertThat(state.getTs()).isEqualTo(100L); + assertThat(state.getVersion()).isEqualTo(1L); + } + + @Test + void update_withNullNewVersion_alwaysApplies_andCopiesNull() { + // arrange: the implementation updates if newVersion == null + String OTHER_POLYGON = "[[50.471000, 30.506000], [50.471000, 30.507000], [50.472000, 30.507000], [50.472000, 30.506000]]"; + GeofencingZoneState nullVersion = new GeofencingZoneState( + ZONE_ID, + new BaseAttributeKvEntry(new JsonDataEntry("zone", OTHER_POLYGON), 400L, null) + ); + + // act + boolean changed = state.update(nullVersion); + + // assert: applied and version copied as null + assertThat(changed).isTrue(); + assertThat(state.getTs()).isEqualTo(400L); + assertThat(state.getVersion()).isNull(); + assertThat(state.getLastPresence()).isNull(); + } + + @Test + void update_withNewVersionWhenExistingIsNull_alwaysApplies_andCopiesNew() { + // arrange: the implementation updates if newVersion == null + String OTHER_POLYGON = "[[50.471000, 30.506000], [50.471000, 30.507000], [50.472000, 30.507000], [50.472000, 30.506000]]"; + GeofencingZoneState newVersion = new GeofencingZoneState( + ZONE_ID, + new BaseAttributeKvEntry(new JsonDataEntry("zone", OTHER_POLYGON), 400L, 2L) + ); + state.setVersion(null); + + // act + boolean changed = state.update(newVersion); + + // assert: applied and version copied as null + assertThat(changed).isTrue(); + assertThat(state.getTs()).isEqualTo(400L); + assertThat(state.getVersion()).isEqualTo(2); + assertThat(state.getLastPresence()).isNull(); + } + } diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java index 3039e36a05..fd59317649 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java @@ -35,4 +35,4 @@ public class ArgumentTest { assertThat(argument.hasDynamicSource()).isTrue(); } -} \ No newline at end of file +} From 86f7eb8da322512f2e65e4ae87a8b8b5c55171f0 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 19 Aug 2025 13:20:06 +0300 Subject: [PATCH 066/216] UI: Fixed blink action buttons in table widgets when API calls --- .../widget/lib/alarm/alarms-table-widget.component.html | 8 ++++---- .../lib/entity/entities-table-widget.component.html | 3 +-- .../widget/lib/timeseries-table-widget.component.html | 3 +-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html index acd4fc43be..0131eb2947 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarm/alarms-table-widget.component.html @@ -45,14 +45,14 @@ - + + + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.scss new file mode 100644 index 0000000000..f54357b60f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.scss @@ -0,0 +1,58 @@ +/** + * 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. + */ +@import '../../../../../../../../scss/constants'; + +.tb-form-table-row.tb-api-usage-data-key-row { + + .tb-source-field { + flex: 1 1 50%; + display: flex; + gap: 12px; + .tb-label-field { + flex: 1; + } + } + + .tb-data-key-field { + flex: 1 1 25%; + min-width: 0; + } + + .tb-remove-button { + width: 40px; + min-width: 40px; + } + + @media #{$mat-lt-lg} { + .tb-source-field { + flex-direction: column; + flex: 1 1 30%; + } + .tb-data-key-field{ + flex: 1 1 35%; + } + } + @media screen and (min-width: 450px) and (max-width: 599px) { + .tb-source-field { + flex-direction: row; + } + } + @media #{$mat-xs} { + .tb-data-key-field { + display: none; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.ts new file mode 100644 index 0000000000..adbf7ad180 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.ts @@ -0,0 +1,151 @@ +/// +/// 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. +/// + +import { + ChangeDetectorRef, + Component, + DestroyRef, + EventEmitter, + forwardRef, + Input, + OnInit, + Output, + ViewEncapsulation +} from '@angular/core'; +import { + ControlValueAccessor, + NG_VALUE_ACCESSOR, + UntypedFormBuilder, + UntypedFormGroup, + Validators +} from '@angular/forms'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { DataKey, DatasourceType, widgetType } from '@shared/models/widget.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { + ApiUsageDataKeysSettings, + ApiUsageSettingsContext +} from "@home/components/widget/lib/settings/cards/api-usage-settings.component.models"; + +@Component({ + selector: 'tb-api-usage-data-key-row', + templateUrl: './api-usage-data-key-row.component.html', + styleUrls: ['./api-usage-data-key-row.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ApiUsageDataKeyRowComponent), + multi: true + } + ], + encapsulation: ViewEncapsulation.None +}) +export class ApiUsageDataKeyRowComponent implements ControlValueAccessor, OnInit { + + DatasourceType = DatasourceType; + DataKeyType = DataKeyType; + + widgetType = widgetType; + + @Input() + disabled: boolean; + + @Input() + dsEntityAliasId: string; + + @Input() + context: ApiUsageSettingsContext; + + @Output() + dataKeyRemoved = new EventEmitter(); + + dataKeyFormGroup: UntypedFormGroup; + + modelValue: ApiUsageDataKeysSettings; + + private propagateChange = (_val: any) => {}; + + constructor(private fb: UntypedFormBuilder, + private cd: ChangeDetectorRef, + private destroyRef: DestroyRef) { + } + + ngOnInit() { + this.dataKeyFormGroup = this.fb.group({ + label: [null, [Validators.required]], + state: [null, []], + status: [null, [Validators.required]], + maxLimit: [null, [Validators.required]], + current: [null, [Validators.required]] + }); + this.dataKeyFormGroup.valueChanges.pipe( + takeUntilDestroyed(this.destroyRef) + ).subscribe( + () => this.updateModel() + ); + } + + registerOnChange(fn: any): void { + this.propagateChange = fn; + } + + registerOnTouched(_fn: any): void { + } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + if (isDisabled) { + this.dataKeyFormGroup.disable({emitEvent: false}); + } else { + this.dataKeyFormGroup.enable({emitEvent: false}); + this.updateValidators(); + } + } + + writeValue(value: ApiUsageDataKeysSettings): void { + this.modelValue = value; + this.dataKeyFormGroup.patchValue( + { + label: value?.label, + state: value?.state, + status: value?.status, + maxLimit: value?.maxLimit, + current: value?.current + }, {emitEvent: false} + ); + this.updateValidators(); + this.cd.markForCheck(); + } + + editKey(keyType: 'status' | 'maxLimit' | 'current') { + const targetDataKey: DataKey = this.dataKeyFormGroup.get(keyType).value; + this.context.editKey(targetDataKey, this.dsEntityAliasId).subscribe( + (updatedDataKey) => { + if (updatedDataKey) { + this.dataKeyFormGroup.get(keyType).patchValue(updatedDataKey); + } + } + ); + } + + private updateValidators() { + } + + private updateModel() { + this.modelValue = {...this.modelValue, ...this.dataKeyFormGroup.value}; + this.propagateChange(this.modelValue); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-settings.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-settings.component.models.ts new file mode 100644 index 0000000000..7af2711e8f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-settings.component.models.ts @@ -0,0 +1,106 @@ +/// +/// 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. +/// + +import { IAliasController } from '@core/api/widget-api.models'; +import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; +import { DataKey, Widget, widgetType } from '@shared/models/widget.models'; +import { Observable } from "rxjs"; +import { BackgroundSettings, BackgroundType } from "@shared/models/widget-settings.models"; +import { DataKeyType } from "@shared/models/telemetry/telemetry.models"; +import { materialColors } from "@shared/models/material.models"; + +export interface ApiUsageSettingsContext { + aliasController: IAliasController; + callbacks: WidgetConfigCallbacks; + widget: Widget; + editKey: (key: DataKey, entityAliasId: string, WidgetType?: widgetType) => Observable; + generateDataKey: (key: DataKey) => DataKey; +} + + +export interface ApiUsageWidgetSettings { + dsEntityAliasId: string; + dataKeys: ApiUsageDataKeysSettings[]; + targetDashboardState: string; + background: BackgroundSettings; + padding: string; +} + +export interface ApiUsageDataKeysSettings { + label: string; + state: string; + status: DataKey; + maxLimit: DataKey; + current: DataKey; +} + +const generateDataKey = (label: string, status: string, maxLimit: string, current: string) => { + return { + label, + state: '', + status: { + name: status, + label: status, + type: DataKeyType.timeseries, + funcBody: undefined, + settings: {}, + color: materialColors[0].value + }, + maxLimit: { + name: maxLimit, + label: maxLimit, + type: DataKeyType.timeseries, + funcBody: undefined, + settings: {}, + color: materialColors[0].value + }, + current: { + name: current, + label: current, + type: DataKeyType.timeseries, + funcBody: undefined, + settings: {}, + color: materialColors[0].value + } + } +} + +export const apiUsageDefaultSettings: ApiUsageWidgetSettings = { + dsEntityAliasId: '', + dataKeys: [ + generateDataKey('{i18n:api-usage.transport-messages}', 'transportApiState', 'transportMsgLimit', 'transportMsgCount'), + generateDataKey('{i18n:api-usage.transport-data-points}', 'transportApiState', 'transportDataPointsLimit', 'transportDataPointsCount'), + generateDataKey('{i18n:api-usage.rule-engine-executions}', 'ruleEngineApiState', 'ruleEngineExecutionLimit', 'ruleEngineExecutionCount'), + generateDataKey('{i18n:api-usage.javascript-function-executions}', 'jsExecutionApiState', 'jsExecutionLimit', 'jsExecutionCount'), + generateDataKey('{i18n:api-usage.tbel-function-executions}', 'tbelExecutionApiState', 'tbelExecutionLimit', 'tbelExecutionCount'), + generateDataKey('{i18n:api-usage.data-points-storage-days}', 'dbApiState', 'storageDataPointsLimit', 'storageDataPointsCount'), + generateDataKey('{i18n:api-usage.alarms-created}', 'alarmApiState', 'createdAlarmsLimit', 'createdAlarmsCount'), + generateDataKey('{i18n:api-usage.emails}', 'emailApiState', 'emailLimit', 'emailCount'), + generateDataKey('{i18n:api-usage.sms}', 'notificationApiState', 'smsLimit', 'smsCount'), + ], + targetDashboardState: 'default', + background: { + type: BackgroundType.color, + color: '#fff', + overlay: { + enabled: false, + color: 'rgba(255,255,255,0.72)', + blur: 3 + } + }, + padding: '0' +}; + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.html new file mode 100644 index 0000000000..babb9e4da2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.html @@ -0,0 +1,96 @@ + + +
+
widget-config.datasource
+ + + +
+
+
+
widgets.api-usage.label
+
widgets.api-usage.state-name
+
widgets.api-usage.status
+
widgets.api-usage.limit
+
widgets.api-usage.current-number
+
+
+
+
+
+ + +
+ +
+
+
+
+
+
+ +
+
+ + {{ 'widgets.api-usage.no-key' | translate }} + + + + widgets.api-usage.target-dashboard-state + + +
+ +
+
widget-config.card-appearance
+
+
{{ 'widgets.background.background' | translate }}
+ + +
+
+
{{ 'widget-config.card-padding' | translate }}
+ + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.scss new file mode 100644 index 0000000000..9543abb44b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.scss @@ -0,0 +1,62 @@ +/** + * 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. + */ +@import '../../../../../../../../scss/constants'; + +.tb-map-data-layers { + .tb-form-table-header-cell { + &.tb-source-header { + flex: 1 1 50%; + } + &.tb-x-pos-header { + flex: 1 1 25%; + } + &.tb-y-pos-header { + flex: 1 1 25%; + } + &.tb-key-header { + flex: 1 1 50%; + } + &.tb-actions-header { + width: 80px; + min-width: 80px; + } + @media #{$mat-lt-lg} { + &.tb-source-header { + flex: 1 1 30%; + } + &.tb-x-pos-header, &.tb-y-pos-header { + flex: 1 1 35%; + } + &.tb-key-header { + flex: 1 1 70%; + } + } + @media #{$mat-xs} { + &.tb-x-pos-header, &.tb-y-pos-header { + display: none; + } + &.tb-key-header { + display: none; + } + } + } + + .tb-form-table-body { + tb-api-usage-data-key-row { + overflow: hidden; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts new file mode 100644 index 0000000000..b71b406bfa --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts @@ -0,0 +1,193 @@ +/// +/// 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. +/// + +import { Component, forwardRef } from '@angular/core'; +import { + DataKey, + DataKeyConfigMode, + WidgetSettings, + WidgetSettingsComponent, + widgetType +} from '@shared/models/widget.models'; +import { + AbstractControl, + NG_VALUE_ACCESSOR, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormGroup, + ValidationErrors, + ValidatorFn +} from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { + ApiUsageDataKeysSettings, + apiUsageDefaultSettings, + ApiUsageSettingsContext +} from "@home/components/widget/lib/settings/cards/api-usage-settings.component.models"; +import { deepClone } from "@core/utils"; +import { Observable } from "rxjs"; +import { + DataKeyConfigDialogComponent, + DataKeyConfigDialogData +} from "@home/components/widget/lib/settings/common/key/data-key-config-dialog.component"; +import { MatDialog } from "@angular/material/dialog"; +import { CdkDragDrop } from "@angular/cdk/drag-drop"; + +@Component({ + selector: 'tb-api-usage-widget-settings', + templateUrl: './api-usage-widget-settings.component.html', + styleUrls: ['./../widget-settings.scss', 'api-usage-widget-settings.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ApiUsageWidgetSettingsComponent), + multi: true + } + ], +}) +export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent { + + apiUsageWidgetSettingsForm: UntypedFormGroup; + + context: ApiUsageSettingsContext; + + constructor(protected store: Store, + private dialog: MatDialog, + private fb: UntypedFormBuilder) { + super(store); + } + + ngOnInit() { + this.context = { + aliasController: this.aliasController, + callbacks: this.callbacks, + widget: this.widget, + editKey: this.editKey.bind(this), + generateDataKey: this.generateDataKey.bind(this) + }; + } + + dataKeysFormArray(): UntypedFormArray { + return this.apiUsageWidgetSettingsForm.get('dataKeys') as UntypedFormArray; + } + + trackByDataKey(index: number, dataKeyControl: AbstractControl): any { + return dataKeyControl; + } + + get dragEnabled(): boolean { + return this.dataKeysFormArray().controls.length > 1; + } + + layerDrop(event: CdkDragDrop) { + const layer = this.dataKeysFormArray().at(event.previousIndex); + this.dataKeysFormArray().removeAt(event.previousIndex); + this.dataKeysFormArray().insert(event.currentIndex, layer); + } + + removeDataKey(index: number) { + (this.apiUsageWidgetSettingsForm.get('dataKeys') as UntypedFormArray).removeAt(index); + } + + addDataKey() { + const dataKey = { + label: '', + state: '', + status: null, + maxLimit: null, + current: null + }; + const dataKeysArray = this.apiUsageWidgetSettingsForm.get('dataKeys') as UntypedFormArray; + const dataKeyControl = this.fb.control(dataKey, [this.mapDataKeyValidator()]); + dataKeysArray.push(dataKeyControl); + } + + protected settingsForm(): UntypedFormGroup { + return this.apiUsageWidgetSettingsForm; + } + + protected defaultSettings(): WidgetSettings { + return apiUsageDefaultSettings; + } + + protected onSettingsSet(settings: WidgetSettings) { + this.apiUsageWidgetSettingsForm = this.fb.group({ + dsEntityAliasId: [settings?.dsEntityAliasId], + dataKeys: this.prepareDataKeysFormArray(settings?.dataKeys), + targetDashboardState: [settings?.targetDashboardState], + background: [settings?.background, []], + padding: [settings.padding, []] + }); + } + + private prepareDataKeysFormArray(dataKeys: ApiUsageDataKeysSettings[]): UntypedFormArray { + const dataKeysControls: Array = []; + if (dataKeys) { + dataKeys.forEach((dataLayer) => { + dataKeysControls.push(this.fb.control(dataLayer, [this.mapDataKeyValidator()])); + }); + } + return this.fb.array(dataKeysControls); + } + + protected validatorTriggers(): string[] { + return []; + } + + protected updateValidators() { + } + + mapDataKeyValidator = (): ValidatorFn => { + return (control: AbstractControl): ValidationErrors | null => { + const value: ApiUsageDataKeysSettings = control.value; + if (!value?.label || !value?.current || !value?.maxLimit || !value?.status) { + return { + dataKey: true + } + } + return null; + }; + }; + + private editKey(key: DataKey, entityAliasId: string, _widgetType = widgetType.latest): Observable { + return this.dialog.open(DataKeyConfigDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + dataKey: deepClone(key), + dataKeyConfigMode: DataKeyConfigMode.general, + aliasController: this.aliasController, + widgetType: _widgetType, + entityAliasId, + showPostProcessing: true, + callbacks: this.callbacks, + hideDataKeyColor: true, + hideDataKeyDecimals: true, + hideDataKeyUnits: true, + widget: this.widget, + dashboard: null, + dataKeySettingsForm: null, + dataKeySettingsDirective: null + } + }).afterClosed(); + } + + private generateDataKey(key: DataKey): DataKey { + return this.callbacks.generateDataKey(key.name, key.type, null, false, null); + } +} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts index fbf9b2eec2..9e4de41841 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/widget-settings.module.ts @@ -375,6 +375,12 @@ import { ValueStepperWidgetSettingsComponent } from '@home/components/widget/lib/settings/control/value-stepper-widget-settings.component'; import { MapWidgetSettingsComponent } from '@home/components/widget/lib/settings/map/map-widget-settings.component'; +import { + ApiUsageWidgetSettingsComponent +} from "@home/components/widget/lib/settings/cards/api-usage-widget-settings.component"; +import { + ApiUsageDataKeyRowComponent +} from "@home/components/widget/lib/settings/cards/api-usage-data-key-row.component"; @NgModule({ declarations: [ @@ -508,7 +514,9 @@ import { MapWidgetSettingsComponent } from '@home/components/widget/lib/settings LabelValueCardWidgetSettingsComponent, UnreadNotificationWidgetSettingsComponent, ScadaSymbolWidgetSettingsComponent, - MapWidgetSettingsComponent + MapWidgetSettingsComponent, + ApiUsageWidgetSettingsComponent, + ApiUsageDataKeyRowComponent ], imports: [ CommonModule, @@ -647,7 +655,8 @@ import { MapWidgetSettingsComponent } from '@home/components/widget/lib/settings LabelValueCardWidgetSettingsComponent, UnreadNotificationWidgetSettingsComponent, ScadaSymbolWidgetSettingsComponent, - MapWidgetSettingsComponent + MapWidgetSettingsComponent, + ApiUsageWidgetSettingsComponent ] }) export class WidgetSettingsModule { diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts index cdc829a96f..fab51613d4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-components.module.ts @@ -94,6 +94,7 @@ import { SelectMapEntityPanelComponent } from '@home/components/widget/lib/maps/panels/select-map-entity-panel.component'; import { MapTimelinePanelComponent } from '@home/components/widget/lib/maps/panels/map-timeline-panel.component'; +import { ApiUsageWidgetComponent } from "@home/components/widget/lib/cards/api-usage-widget.component"; @NgModule({ declarations: [ @@ -151,7 +152,8 @@ import { MapTimelinePanelComponent } from '@home/components/widget/lib/maps/pane ScadaSymbolWidgetComponent, SelectMapEntityPanelComponent, MapTimelinePanelComponent, - MapWidgetComponent + MapWidgetComponent, + ApiUsageWidgetComponent ], imports: [ CommonModule, @@ -214,7 +216,8 @@ import { MapTimelinePanelComponent } from '@home/components/widget/lib/maps/pane UnreadNotificationWidgetComponent, NotificationTypeFilterPanelComponent, ScadaSymbolWidgetComponent, - MapWidgetComponent + MapWidgetComponent, + ApiUsageWidgetComponent ], providers: [ {provide: WIDGET_COMPONENTS_MODULE_TOKEN, useValue: WidgetComponentsModule}, diff --git a/ui-ngx/src/assets/dashboard/api_usage.json b/ui-ngx/src/assets/dashboard/api_usage.json index 9738e9ac0f..77dc18c11e 100644 --- a/ui-ngx/src/assets/dashboard/api_usage.json +++ b/ui-ngx/src/assets/dashboard/api_usage.json @@ -79,7 +79,6 @@ } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, "selectedTab": 0, @@ -121,779 +120,4138 @@ "widgetCss": "", "pageSize": 1024, "noDataDisplayMessage": "", - "configMode": "basic" + "configMode": "basic", + "borderRadius": "4px" }, "id": "a669cf86-e715-efa4-dd9a-b839abf499e9", "typeFullFqn": "system.cards.timeseries_table" }, - "aab68ab5-8e40-8694-c55c-8eb1c89b88fb": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, + "fa938580-33db-f1b3-fafc-bc3e3784ad57": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, "config": { "datasources": [ { "type": "entity", - "name": null, - "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", - "filterId": null, + "entityAliasId": "2e4c97b0-257a-a1b9-690c-141d9bf2ec6f", "dataKeys": [ { - "name": "transportMsgLimit", + "name": "successfulMsgs", "type": "timeseries", - "label": "limit", + "label": "{i18n:api-usage.successful}", "color": "#4caf50", - "settings": {}, - "_hash": 0.5463603803546802, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "transportMsgCount", - "type": "timeseries", - "label": "count", - "color": "#f44336", - "settings": {}, - "_hash": 0.5564241862015964, + "settings": { + "yAxisId": "default", + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "line", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2.5, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "pointShape": "circle", + "pointSize": 12, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + } + }, + "_hash": 0.15490750967648736, + "aggregationType": null, "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;\n", - "aggregationType": "NONE" + "usePostProcessing": null, + "postFuncBody": null }, { - "name": "transportDataPointsLimit", + "name": "failedMsgs", "type": "timeseries", - "label": "pointsLimit", - "color": "#9c27b0", - "settings": {}, - "_hash": 0.22082255831864894, + "label": "{i18n:api-usage.permanent-failures}", + "color": "#ef5350", + "settings": { + "yAxisId": "default", + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "line", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2.5, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "pointShape": "circle", + "pointSize": 12, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + } + }, + "_hash": 0.4186621166514697, + "aggregationType": null, "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" + "usePostProcessing": null, + "postFuncBody": null }, { - "name": "transportDataPointsCount", + "name": "tmpFailed", "type": "timeseries", - "label": "pointsCount", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.6340356364819146, + "label": "{i18n:api-usage.processing-failures}", + "color": "#ffc107", + "settings": { + "yAxisId": "default", + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "line", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2.5, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "pointShape": "circle", + "pointSize": 12, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + } + }, + "_hash": 0.49891007198715376, + "aggregationType": null, "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + }, + "latestDataKeys": [ { - "name": "transportApiState", - "type": "timeseries", - "label": "title", - "color": "#3f51b5", + "name": "queueName", + "type": "entityField", + "label": "Queue name", + "color": "#ffc107", "settings": {}, - "_hash": 0.6894070537030252, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.transport}\";" + "_hash": 0.7021721434431745 }, { - "name": "transportApiState", - "type": "timeseries", - "label": "apiStatus", - "color": "#3f51b5", + "name": "serviceId", + "type": "entityField", + "label": "Service Id", + "color": "#607d8b", "settings": {}, - "_hash": 0.430957831457494, - "aggregationType": "NONE", - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value ? value.toLowerCase() : 'enabled';" - }, - { - "name": "transportApiState", - "type": "timeseries", - "label": "unit", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.662147926074595, - "aggregationType": "NONE", - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return '{i18n:api-usage.messages}';" - }, - { - "name": "transportApiState", - "type": "timeseries", - "label": "pointsUnit", - "color": "#3f51b5", - "settings": {}, - "_hash": 0.44620898738917947, - "aggregationType": "NONE", - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return '{i18n:api-usage.data-points}';" + "_hash": 0.5924381120750077 } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - } + ] } ], "timewindow": { - "displayValue": "", + "hideAggregation": false, + "hideAggInterval": false, "selectedTab": 0, "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1708518962586, - "endTimeMs": 1708605362586 - }, - "quickInterval": "CURRENT_DAY" + "timewindowMs": 3600000, + "interval": 1000 }, "aggregation": { - "type": "AVG", - "limit": 25000 + "type": "NONE", + "limit": 10000 } }, - "showTitle": false, - "backgroundColor": "#fff", + "showTitle": true, + "backgroundColor": "#FFFFFF", "color": "rgba(0, 0, 0, 0.87)", "padding": "0px", "settings": { - "useMarkdownTextFunction": true, - "markdownTextPattern": "", - "markdownTextFunction": "function toShortNumber(number) {\n const rounder = Math.pow(10, 1);\n const powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n ];\n let key = '';\n for (const power of powers) {\n const reduced = number / power.value;\n for (const power of powers) {\n let reduced = number / power.value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n number = reduced;\n key = power.key;\n break;\n }\n }\n }\n \n return number + key;\n}\n\nfunction calculateBarValues(count, limit) {\n let apiUsageBar = '0%';\n let apiUsagePercent = '';\n let apiUsageValue = `${toShortNumber(count)} / ∞`;\n if (Number.isFinite(limit) && limit > 0) {\n var percent = Math.min(100, ((count / limit) * 100));\n apiUsageBar = `${percent}%`\n apiUsagePercent = `${percent.toFixed(2)}%`;\n apiUsageValue = `${toShortNumber(count)} / ${toShortNumber(limit)}`;\n }\n \n return [apiUsageBar, apiUsagePercent, apiUsageValue]\n}\n\nconst [apiUsageBar, apiUsagePercent, apiUsageValue] = calculateBarValues(data[0].count, data[0].limit);\nconst [apiUsageBar2, apiUsagePercent2, apiUsageValue2] = calculateBarValues(data[0].pointsCount, data[0].pointsLimit);\n\n\nreturn `
` +\n '
' +\n '
' +\n '
' +\n '
' +\n `${data[0].title}` +\n '
' +\n `
${data[0].apiStatus.toUpperCase()}
` +\n '
' +\n '
' +\n '
' +\n '
' +\n `
${data[0].unit}
` +\n '
' +\n `
` +\n '
' +\n '
' +\n `
${apiUsagePercent}
` +\n '
' +\n `
${apiUsageValue}
` +\n '
' +\n '
' +\n '
' +\n '
' +\n '
' +\n `
${data[0].pointsUnit}
` +\n '
' +\n `
` +\n '
' +\n '
' +\n `
${apiUsagePercent2}
` +\n '
' +\n `
${apiUsageValue2}
` +\n '
' +\n '
' + \n '
' +\n '
' +\n '
' +\n '
' +\n '' +\n '
'+\n '' +\n '
' +\n '
'\n", - "applyDefaultMarkdownStyle": false, - "markdownCss": "\n" - }, - "title": "Transport", - "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "dropShadow": true, - "enableFullscreen": false, - "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, - "widgetCss": "", - "pageSize": 1024, - "noDataDisplayMessage": "", - "actions": { - "elementClick": [ - { - "name": "transport_details", - "icon": "insert_chart", - "useShowWidgetActionFunction": null, - "showWidgetActionFunction": "return true;", - "type": "openDashboardState", - "targetDashboardStateId": "transport", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "openInSeparateDialog": false, - "openInPopover": false, - "id": "a60e09be-1bea-dfc3-6abb-f87e73256899" - } - ] - } - }, - "row": 0, - "col": 0, - "id": "aab68ab5-8e40-8694-c55c-8eb1c89b88fb" - }, - "a84fa70a-ddfa-3b24-9aa4-cf9ce91f919a": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, - "config": { - "datasources": [ - { - "type": "entity", - "name": null, - "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", - "filterId": null, - "dataKeys": [ - { - "name": "ruleEngineApiState", - "type": "timeseries", - "label": "apiStatus", - "color": "#2196f3", - "settings": {}, - "_hash": 0.8830669138660703, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value ? value : 'enabled';", - "aggregationType": "NONE" - }, - { - "name": "ruleEngineExecutionLimit", - "type": "timeseries", - "label": "limit", - "color": "#4caf50", - "settings": {}, - "_hash": 0.5463603803546802, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "ruleEngineExecutionCount", - "type": "timeseries", - "label": "count", - "color": "#f44336", - "settings": {}, - "_hash": 0.5564241862015964, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" }, - { - "name": "ruleEngineApiState", - "type": "timeseries", - "label": "title", - "color": "#9c27b0", - "settings": {}, - "_hash": 0.3551317421302518, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.rule-engine}\";" + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" }, - { - "name": "ruleEngineApiState", - "type": "timeseries", - "label": "unit", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.5100381746798048, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.executions}\";" - } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null } - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1708518962586, - "endTimeMs": 1708605362586 + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, - "showTitle": false, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "0px", - "settings": { - "useMarkdownTextFunction": true, - "markdownTextPattern": "", - "markdownTextFunction": "function toShortNumber(number) {\n const rounder = Math.pow(10, 1);\n const powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n ];\n let key = '';\n for (const power of powers) {\n let reduced = number / power.value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n number = reduced;\n key = power.key;\n break;\n }\n }\n \n return number + key;\n}\n\nfunction calculateBarValues(count, limit) {\n let apiUsageBar = '0%';\n let apiUsagePercent = '';\n let apiUsageValue = `${toShortNumber(count)} / ∞`;\n if (Number.isFinite(limit) && limit > 0) {\n var percent = Math.min(100, ((count / limit) * 100));\n apiUsageBar = `${percent}%`\n apiUsagePercent = `${percent.toFixed(2)}%`;\n apiUsageValue = `${toShortNumber(count)} / ${toShortNumber(limit)}`;\n }\n \n return [apiUsageBar, apiUsagePercent, apiUsageValue]\n}\n\nconst [apiUsageBar, apiUsagePercent, apiUsageValue] = calculateBarValues(data[0].count, data[0].limit);\n\n\nreturn `
` +\n '
' +\n '
' +\n '
' +\n '
${title}
' +\n `
${data[0].apiStatus.toUpperCase()}
` +\n '
' +\n '
' +\n `
${data[0].unit}
` +\n '
' +\n `
` +\n '
' +\n '
' +\n `
${apiUsagePercent}
` +\n '
' +\n `
${apiUsageValue}
` +\n '
' +\n '
' +\n '
' +\n '
' +\n '' +\n '
'+\n '' +\n '' +\n '
' +\n '
'\n", - "applyDefaultMarkdownStyle": false, - "markdownCss": "\n" - }, - "title": "Rule Engine execution", - "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "dropShadow": true, - "enableFullscreen": false, - "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, - "widgetCss": "", - "pageSize": 1024, - "noDataDisplayMessage": "", - "actions": { - "elementClick": [ - { - "name": "rule_engine_execution_details", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "rule_engine_execution", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "id": "3c30248f-0cd8-fb97-a917-bc1e09984a79" + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" }, - { - "name": "rule_engine_statistics_details", - "icon": "show_chart", - "type": "openDashboardState", - "targetDashboardStateId": "rule_engine_statistics", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "id": "04e4565a-9e24-23df-f376-f2ec70a8165f" - } - ] - } + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": false, + "relativeWidth": 2, + "absoluteWidth": 1800000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": true, + "showMax": true, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipHideZeroValues": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 300, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "padding": "12px", + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)" + }, + "title": "{i18n:api-usage.queue-stats}", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": {}, + "showTitleIcon": false, + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 16, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "24px" + }, + "titleColor": "rgba(0, 0, 0, 0.87)", + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "units": "", + "decimals": null, + "noDataDisplayMessage": "", + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "4px", + "iconSize": "0px" }, "row": 0, "col": 0, - "id": "a84fa70a-ddfa-3b24-9aa4-cf9ce91f919a" + "id": "fa938580-33db-f1b3-fafc-bc3e3784ad57" }, - "d70d26d4-e22d-4ca9-9ea7-f9c87c093321": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, + "2ee89893-4e38-5331-95b7-3fd4f310c5a7": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, "config": { "datasources": [ { "type": "entity", - "name": null, - "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", - "filterId": null, + "entityAliasId": "2e4c97b0-257a-a1b9-690c-141d9bf2ec6f", "dataKeys": [ { - "name": "jsExecutionApiState", - "type": "timeseries", - "label": "jsApiState", - "color": "#2196f3", - "settings": {}, - "_hash": 0.8830669138660703, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value ? value : 'ENABLED';", - "aggregationType": "NONE" - }, - { - "name": "jsExecutionLimit", + "name": "timeoutMsgs", "type": "timeseries", - "label": "jsLimit", + "label": "{i18n:api-usage.permanent-timeouts}", "color": "#4caf50", - "settings": {}, - "_hash": 0.5463603803546802, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "jsExecutionCount", - "type": "timeseries", - "label": "jsCount", - "color": "#f44336", - "settings": {}, - "_hash": 0.5564241862015964, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "jsExecutionApiState", - "type": "timeseries", - "label": "title", - "color": "#9c27b0", - "settings": {}, - "_hash": 0.7673280949238444, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.scripts}\";", - "aggregationType": "NONE" - }, - { - "name": "jsExecutionApiState", - "type": "timeseries", - "label": "jsUnit", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.7926918686567068, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.javascript}\";", - "aggregationType": "NONE" - }, - { - "name": "tbelExecutionApiState", - "type": "timeseries", - "label": "tbelApiState", - "color": "#3f51b5", - "settings": {}, - "_hash": 0.2002981454581909, - "aggregationType": "NONE", - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value ? value : 'ENABLED';" - }, - { - "name": "tbelExecutionLimit", - "type": "timeseries", - "label": "tbelLimit", - "color": "#ffeb3b", - "settings": {}, - "_hash": 0.5039854873031677, - "aggregationType": "NONE", - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;" - }, - { - "name": "tbelExecutionCount", - "type": "timeseries", - "label": "tbelCount", - "color": "#e91e63", - "settings": {}, - "_hash": 0.9506731992087107, - "aggregationType": "NONE", + "settings": { + "yAxisId": "default", + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "line", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2.5, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "pointShape": "circle", + "pointSize": 12, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + } + }, + "_hash": 0.565222981550328, + "aggregationType": null, "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;" + "usePostProcessing": null, + "postFuncBody": null }, { - "name": "tbelExecutionApiState", + "name": "tmpTimeout", "type": "timeseries", - "label": "tbelUnit", - "color": "#ffeb3b", - "settings": {}, - "_hash": 0.3673530683177082, - "aggregationType": "NONE", + "label": "{i18n:api-usage.processing-timeouts}", + "color": "#9c27b0", + "settings": { + "yAxisId": "default", + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "line", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2.5, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "pointShape": "circle", + "pointSize": 12, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + } + }, + "_hash": 0.2679547062508352, + "aggregationType": null, "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.tbel}\";" + "usePostProcessing": null, + "postFuncBody": null } ], "alarmFilterConfig": { "statusList": [ "ACTIVE" ] + }, + "latestDataKeys": [ + { + "name": "queueName", + "type": "entityField", + "label": "Queue name", + "color": "#f44336", + "settings": {}, + "_hash": 0.7066844328378095 + }, + { + "name": "serviceId", + "type": "entityField", + "label": "Service Id", + "color": "#ffc107", + "settings": {}, + "_hash": 0.1371570237026627 + } + ] + } + ], + "timewindow": { + "hideAggregation": false, + "hideAggInterval": false, + "selectedTab": 0, + "realtime": { + "timewindowMs": 3600000, + "interval": 1000 + }, + "aggregation": { + "type": "NONE", + "limit": 10000 + } + }, + "showTitle": true, + "backgroundColor": "#FFFFFF", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null + } + }, + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": false, + "relativeWidth": 2, + "absoluteWidth": 1800000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": true, + "showMax": true, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipHideZeroValues": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 300, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "padding": "12px", + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)" + }, + "title": "{i18n:api-usage.processing-failures-and-timeouts}", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": {}, + "showTitleIcon": false, + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 16, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "24px" + }, + "titleColor": "rgba(0, 0, 0, 0.87)", + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "units": "", + "decimals": null, + "noDataDisplayMessage": "", + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "4px", + "iconSize": "0px" + }, + "row": 0, + "col": 0, + "id": "2ee89893-4e38-5331-95b7-3fd4f310c5a7" + }, + "85240e8c-7af7-90a9-ad0a-726013c479a6": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "filterId": null, + "dataKeys": [ + { + "name": "transportMsgCountHourly", + "type": "timeseries", + "label": "{i18n:api-usage.transport-messages}", + "color": "#2196f3", + "settings": { + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" + } + ], + "comparisonSettings": { + "showValuesForComparison": true + }, + "type": "bar" + }, + "_hash": 0.0661644137210089, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": false, + "postFuncBody": null, + "aggregationType": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } + } + ], + "timewindow": { + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "aggregation": { + "type": "SUM", + "limit": 50000 + }, + "timezone": null + }, + "showTitle": true, + "backgroundColor": "#FFFFFF", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null + } + }, + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": null, + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 3600000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" + }, + "title": "{i18n:api-usage.transport-messages-hourly-activity}", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": { + "headerButton": [ + { + "name": "{i18n:api-usage.view-details}", + "icon": "insert_chart", + "type": "openDashboardState", + "targetDashboardStateId": "transport", + "setEntityId": false, + "stateEntityParamName": null, + "openRightLayout": false, + "id": "6ef12f6a-0266-25cf-6ca5-5dcb772252c6" + } + ] + }, + "showTitleIcon": false, + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 16, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "24px" + }, + "titleColor": "rgba(0, 0, 0, 0.87)", + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "units": "", + "decimals": null, + "noDataDisplayMessage": "", + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "4px", + "iconSize": "0px" + }, + "row": 0, + "col": 0, + "id": "85240e8c-7af7-90a9-ad0a-726013c479a6" + }, + "d0a10a8f-8f48-f9d6-8306-d12af9b49690": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "filterId": null, + "dataKeys": [ + { + "name": "transportDataPointsCountHourly", + "type": "timeseries", + "label": "{i18n:api-usage.transport-data-points}", + "color": "#4caf50", + "settings": { + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" + } + ], + "comparisonSettings": { + "showValuesForComparison": true + }, + "type": "bar" + }, + "_hash": 0.46849996721308895, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } + } + ], + "timewindow": { + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "aggregation": { + "type": "SUM", + "limit": 50000 + }, + "timezone": null + }, + "showTitle": true, + "backgroundColor": "#FFFFFF", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null + } + }, + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": null, + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 3600000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" + }, + "title": "{i18n:api-usage.transport-data-point-hourly-activity}", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": { + "headerButton": [ + { + "name": "{i18n:api-usage.view-details}", + "icon": "insert_chart", + "type": "openDashboardState", + "targetDashboardStateId": "transport", + "setEntityId": false, + "stateEntityParamName": null, + "openRightLayout": false, + "id": "6ef12f6a-0266-25cf-6ca5-5dcb772252c6" + } + ] + }, + "showTitleIcon": false, + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 16, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "24px" + }, + "titleColor": "rgba(0, 0, 0, 0.87)", + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "units": "", + "decimals": null, + "noDataDisplayMessage": "", + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "4px", + "iconSize": "0px" + }, + "row": 0, + "col": 0, + "id": "d0a10a8f-8f48-f9d6-8306-d12af9b49690" + }, + "4544080d-9b6f-b592-9cd4-0e0335d33857": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "filterId": null, + "dataKeys": [ + { + "name": "ruleEngineExecutionCountHourly", + "type": "timeseries", + "label": "{i18n:api-usage.rule-engine-executions}", + "color": "#ab00ff", + "settings": { + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "bar", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "pointShape": "emptyCircle", + "pointSize": 4, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + } + }, + "_hash": 0.0661644137210089, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null, + "aggregationType": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } + } + ], + "timewindow": { + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_YEAR", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "aggregation": { + "type": "SUM", + "limit": 50000 + }, + "timezone": null + }, + "showTitle": true, + "backgroundColor": "#FFFFFF", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null + } + }, + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 3600000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" + }, + "title": "{i18n:api-usage.rule-engine-hourly-activity}", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": { + "headerButton": [ + { + "name": "{i18n:api-usage.view-statistics}", + "icon": "show_chart", + "type": "openDashboardState", + "targetDashboardStateId": "rule_engine_statistics", + "setEntityId": false, + "stateEntityParamName": null, + "openRightLayout": false, + "id": "f9f08190-9ed9-d802-5b7a-c57ff84b5648" + }, + { + "name": "{i18n:api-usage.view-details}", + "icon": "insert_chart", + "type": "openDashboardState", + "targetDashboardStateId": "rule_engine_execution", + "setEntityId": false, + "stateEntityParamName": null, + "openRightLayout": false, + "id": "1aec196b-44ba-ddf4-c4dc-c3f60c1eb6fc" + } + ] + }, + "showTitleIcon": false, + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 16, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "24px" + }, + "titleColor": "rgba(0, 0, 0, 0.87)", + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "units": "", + "decimals": null, + "noDataDisplayMessage": "", + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "4px", + "iconSize": "0px" + }, + "row": 0, + "col": 0, + "id": "4544080d-9b6f-b592-9cd4-0e0335d33857" + }, + "5d0f2f57-499d-1324-8e1b-cfbc0b3149d2": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "filterId": null, + "dataKeys": [ + { + "name": "storageDataPointsCountHourly", + "type": "timeseries", + "label": "{i18n:api-usage.data-points-storage-days}", + "color": "#1039ee", + "settings": { + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "bar", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "pointShape": "emptyCircle", + "pointSize": 4, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + } + }, + "_hash": 0.0661644137210089, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null, + "aggregationType": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } + } + ], + "timewindow": { + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "aggregation": { + "type": "SUM", + "limit": 50000 + }, + "timezone": null + }, + "showTitle": true, + "backgroundColor": "#FFFFFF", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null + } + }, + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 3600000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" + }, + "title": "{i18n:api-usage.telemetry-persistence-hourly-activity}", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": { + "headerButton": [ + { + "name": "{i18n:api-usage.view-details}", + "icon": "insert_chart", + "type": "openDashboardState", + "targetDashboardStateId": "telemetry_persistence", + "setEntityId": false, + "stateEntityParamName": null, + "openRightLayout": false, + "id": "16707efb-e572-bd02-c219-55fc1b0f672a" + } + ] + }, + "showTitleIcon": false, + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 16, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "24px" + }, + "titleColor": "rgba(0, 0, 0, 0.87)", + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "units": "", + "decimals": null, + "noDataDisplayMessage": "", + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "4px", + "iconSize": "0px" + }, + "row": 0, + "col": 0, + "id": "5d0f2f57-499d-1324-8e1b-cfbc0b3149d2" + }, + "51608a74-f213-d8c9-8df8-b42238ef93a6": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "filterId": null, + "dataKeys": [ + { + "name": "transportMsgCountHourly", + "type": "timeseries", + "label": "{i18n:api-usage.transport-messages}", + "color": "#2196f3", + "settings": { + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" + } + ], + "comparisonSettings": { + "showValuesForComparison": true + }, + "type": "bar" + }, + "_hash": 0.0661644137210089, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } + } + ], + "timewindow": { + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 86400000, + "timewindowMs": 2592000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "aggregation": { + "type": "SUM", + "limit": 25000 + }, + "timezone": null + }, + "showTitle": true, + "backgroundColor": "#FFFFFF", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null + } + }, + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" + }, + "title": "{i18n:api-usage.transport-msg-hourly-activity}", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": {}, + "showTitleIcon": false, + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 16, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "24px" + }, + "titleColor": "rgba(0, 0, 0, 0.87)", + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "units": "", + "decimals": null, + "noDataDisplayMessage": "", + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "4px", + "iconSize": "0px" + }, + "row": 0, + "col": 0, + "id": "51608a74-f213-d8c9-8df8-b42238ef93a6" + }, + "fb155957-1af4-233e-e2fb-09e648e75d6e": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "filterId": null, + "dataKeys": [ + { + "name": "transportMsgCountHourly", + "type": "timeseries", + "label": "{i18n:api-usage.transport-messages}", + "color": "#2196f3", + "settings": { + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" + } + ], + "comparisonSettings": { + "showValuesForComparison": true + }, + "type": "bar" + }, + "_hash": 0.0661644137210089, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } + } + ], + "timewindow": { + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 1, + "history": { + "historyType": 0, + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "SUM", + "limit": 25000 + }, + "timezone": null + }, + "showTitle": true, + "backgroundColor": "#FFFFFF", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null + } + }, + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" + }, + "title": "{i18n:api-usage.transport-msg-daily-activity}", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": {}, + "showTitleIcon": false, + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 16, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "24px" + }, + "titleColor": "rgba(0, 0, 0, 0.87)", + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "units": "", + "decimals": null, + "noDataDisplayMessage": "", + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "4px", + "iconSize": "0px" + }, + "row": 0, + "col": 0, + "id": "fb155957-1af4-233e-e2fb-09e648e75d6e" + }, + "4817e33b-87be-5be3-eaca-ca68a2eb4e0c": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "filterId": null, + "dataKeys": [ + { + "name": "transportMsgCountHourly", + "type": "timeseries", + "label": "{i18n:api-usage.transport-messages}", + "color": "#2196f3", + "settings": { + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" + } + ], + "comparisonSettings": { + "showValuesForComparison": true + }, + "type": "bar" + }, + "_hash": 0.0661644137210089, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null + } + ] + } + ], + "timewindow": { + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 1, + "realtime": { + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "aggregation": { + "type": "NONE", + "limit": 25000 + }, + "timezone": null + }, + "showTitle": true, + "backgroundColor": "#FFFFFF", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null + } + }, + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" + }, + "title": "{i18n:api-usage.transport-msg-monthly-activity}", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": {}, + "showTitleIcon": false, + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 16, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "24px" + }, + "titleColor": "rgba(0, 0, 0, 0.87)", + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "units": "", + "decimals": null, + "noDataDisplayMessage": "", + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "4px", + "iconSize": "0px" + }, + "row": 0, + "col": 0, + "id": "4817e33b-87be-5be3-eaca-ca68a2eb4e0c" + }, + "9e00cc90-520d-2108-1d2f-bba68ed5cbf1": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "filterId": null, + "dataKeys": [ + { + "name": "transportDataPointsCountHourly", + "type": "timeseries", + "label": "{i18n:api-usage.transport-data-points}", + "color": "#4CAF50", + "settings": { + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" + } + ], + "comparisonSettings": { + "showValuesForComparison": true + }, + "type": "bar", + "yAxisId": "default" + }, + "_hash": 0.0661644137210089, + "units": null, + "decimals": null, + "funcBody": null, + "usePostProcessing": null, + "postFuncBody": null, + "aggregationType": null + } + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } + } + ], + "timewindow": { + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 86400000, + "timewindowMs": 2592000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false + }, + "aggregation": { + "type": "SUM", + "limit": 25000 + }, + "timezone": null + }, + "showTitle": true, + "backgroundColor": "#FFFFFF", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null + } + }, + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 } - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1708518962586, - "endTimeMs": 1708605362586 + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" }, - "quickInterval": "CURRENT_DAY" + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, - "showTitle": false, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "0px", - "settings": { - "useMarkdownTextFunction": true, - "markdownTextPattern": "", - "markdownTextFunction": "function toShortNumber(number) {\n const rounder = Math.pow(10, 1);\n const powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n ];\n let key = '';\n for (const power of powers) {\n const reduced = number / power.value;\n for (const power of powers) {\n let reduced = number / power.value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n number = reduced;\n key = power.key;\n break;\n }\n }\n }\n \n return number + key;\n}\n\nfunction calculateBarValues(count, limit) {\n let apiUsageBar = '0%';\n let apiUsagePercent = '';\n let apiUsageValue = `${toShortNumber(count)} / ∞`;\n if (Number.isFinite(limit) && limit > 0) {\n var percent = Math.min(100, ((count / limit) * 100));\n apiUsageBar = `${percent}%`\n apiUsagePercent = `${percent.toFixed(2)}%`;\n apiUsageValue = `${toShortNumber(count)} / ${toShortNumber(limit)}`;\n }\n \n return [apiUsageBar, apiUsagePercent, apiUsageValue]\n}\n\nconst [jsUsageBar, jsUsagePercent, jsUsageValue] = calculateBarValues(data[0].jsCount, data[0].jsLimit);\nconst [tbelUsageBar, tbelUsagePercent, tbelUsageValue] = calculateBarValues(data[0].tbelCount, data[0].tbelLimit);\n\nconst jsApiState = data[0].jsApiState;\nconst tbelApiState = data[0].tbelApiState;\nlet currentState;\nif (jsApiState === 'DISABLED' || tbelApiState === 'DISABLED') {\n currentState = 'DISABLED';\n} else if (jsApiState === 'WARNING' || tbelApiState === 'WARNING') {\n currentState = 'WARNING';\n} else {\n currentState = 'ENABLED';\n}\nconst cardClass = currentState.toLowerCase()\n\nreturn `
` +\n '
' +\n '
' +\n '
' +\n '
' +\n `${data[0].title}` +\n '
' +\n `
${currentState}
` +\n '
' +\n '
' +\n '
' +\n '
' +\n `
${data[0].jsUnit}
` +\n '
' +\n `
` +\n '
' +\n '
' +\n `
${jsUsagePercent}
` +\n '
' +\n `
${jsUsageValue}
` +\n '
' +\n '
' +\n '
' +\n '
' +\n '
' +\n `
${data[0].tbelUnit}
` +\n '
' +\n `
` +\n '
' +\n '
' +\n `
${tbelUsagePercent}
` +\n '
' +\n `
${tbelUsageValue}
` +\n '
' +\n '
' + \n '
' +\n '
' +\n '
' +\n '
' +\n '' +\n '
'+\n '' +\n '
' +\n '
'\n", - "applyDefaultMarkdownStyle": false, - "markdownCss": "\n" - }, - "title": "JavaScript functions", - "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", - "dropShadow": true, - "enableFullscreen": false, - "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, - "widgetCss": "", - "pageSize": 1024, - "noDataDisplayMessage": "", - "actions": { - "elementClick": [ - { - "name": "script_functions_details", - "icon": "insert_chart", - "useShowWidgetActionFunction": null, - "showWidgetActionFunction": "return true;", - "type": "openDashboardState", - "targetDashboardStateId": "script_functions", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "openInSeparateDialog": false, - "openInPopover": false, - "id": "d4961bea-84de-e1af-e50f-666b98d34cd5" - } - ] - } - }, - "row": 0, - "col": 0, - "id": "d70d26d4-e22d-4ca9-9ea7-f9c87c093321" - }, - "4d3ea95c-3188-9872-1817-2f989c7729e0": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, - "config": { - "datasources": [ - { - "type": "entity", - "name": null, - "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", - "filterId": null, - "dataKeys": [ - { - "name": "storageDataPointsLimit", - "type": "timeseries", - "label": "limit", - "color": "#4caf50", - "settings": {}, - "_hash": 0.5463603803546802, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "storageDataPointsCount", - "type": "timeseries", - "label": "count", - "color": "#f44336", - "settings": {}, - "_hash": 0.5564241862015964, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "dbApiState", - "type": "timeseries", - "label": "apiStatus", - "color": "#ffc107", - "settings": {}, - "_hash": 0.8737107059960671, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value ? value : 'enabled';", - "aggregationType": "NONE" - }, - { - "name": "dbApiState", - "type": "timeseries", - "label": "title", - "color": "#9c27b0", - "settings": {}, - "_hash": 0.6301889725474652, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.telemetry}\";" - }, - { - "name": "dbApiState", - "type": "timeseries", - "label": "unit", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.0027742924142306613, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.data-points-storage-days}\";" - } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - } - } - ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1708518962586, - "endTimeMs": 1708605362586 - }, - "quickInterval": "CURRENT_DAY" + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, - "showTitle": false, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "0px", - "settings": { - "useMarkdownTextFunction": true, - "markdownTextPattern": "", - "markdownTextFunction": "function toShortNumber(number) {\n const rounder = Math.pow(10, 1);\n const powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n ];\n let key = '';\n for (const power of powers) {\n let reduced = number / power.value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n number = reduced;\n key = power.key;\n break;\n }\n }\n \n return number + key;\n}\n\nfunction calculateBarValues(count, limit) {\n let apiUsageBar = '0%';\n let apiUsagePercent = '';\n let apiUsageValue = `${toShortNumber(count)} / ∞`;\n if (Number.isFinite(limit) && limit > 0) {\n var percent = Math.min(100, ((count / limit) * 100));\n apiUsageBar = `${percent}%`\n apiUsagePercent = `${percent.toFixed(2)}%`;\n apiUsageValue = `${toShortNumber(count)} / ${toShortNumber(limit)}`;\n }\n \n return [apiUsageBar, apiUsagePercent, apiUsageValue]\n}\n\nconst [apiUsageBar, apiUsagePercent, apiUsageValue] = calculateBarValues(data[0].count, data[0].limit);\n\n\nreturn `
` +\n '
' +\n '
' +\n '
' +\n '
${title}
' +\n `
${data[0].apiStatus.toUpperCase()}
` +\n '
' +\n '
' +\n `
${data[0].unit}
` +\n '
' +\n `
` +\n '
' +\n '
' +\n `
${apiUsagePercent}
` +\n '
' +\n `
${apiUsageValue}
` +\n '
' +\n '
' +\n '
' +\n '
' +\n '' +\n '
'+\n '' +\n '
' +\n '
'\n", - "applyDefaultMarkdownStyle": false, - "markdownCss": "\n" + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "Telemetry persistence", + "title": "{i18n:api-usage.transport-data-points-hourly-activity}", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": {}, "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 16, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "24px" + }, + "titleColor": "rgba(0, 0, 0, 0.87)", "titleTooltip": "", - "dropShadow": true, - "enableFullscreen": false, "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, "widgetCss": "", "pageSize": 1024, + "units": "", + "decimals": null, "noDataDisplayMessage": "", - "actions": { - "elementClick": [ - { - "name": "telemetry_persistence_details", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "telemetry_persistence", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "id": "6248831c-5b3f-8879-8548-afcf43f10610" - } - ] - } + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "4px", + "iconSize": "0px" }, "row": 0, "col": 0, - "id": "4d3ea95c-3188-9872-1817-2f989c7729e0" + "id": "9e00cc90-520d-2108-1d2f-bba68ed5cbf1" }, - "2d0d6ff6-cd59-51d4-b916-38e22cdd0702": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, + "79056202-c92b-1dae-ce49-318ec52e2d3b": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, "config": { "datasources": [ { @@ -903,72 +4261,42 @@ "filterId": null, "dataKeys": [ { - "name": "createdAlarmsLimit", - "type": "timeseries", - "label": "limit", - "color": "#4caf50", - "settings": {}, - "_hash": 0.5463603803546802, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "createdAlarmsCount", - "type": "timeseries", - "label": "count", - "color": "#f44336", - "settings": {}, - "_hash": 0.5564241862015964, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "alarmApiState", - "type": "timeseries", - "label": "apiStatus", - "color": "#ffc107", - "settings": {}, - "_hash": 0.8737107059960671, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value ? value : 'enabled';", - "aggregationType": "NONE" - }, - { - "name": "alarmApiState", - "type": "timeseries", - "label": "title", - "color": "#9c27b0", - "settings": {}, - "_hash": 0.43439375716502227, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.alarm}\";" - }, - { - "name": "alarmApiState", + "name": "transportDataPointsCountHourly", "type": "timeseries", - "label": "unit", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.9964061963495883, + "label": "{i18n:api-usage.transport-data-points}", + "color": "#4CAF50", + "settings": { + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" + } + ], + "comparisonSettings": { + "showValuesForComparison": true + }, + "type": "bar", + "yAxisId": "default" + }, + "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.alarms-created}\";" + "usePostProcessing": null, + "postFuncBody": null, + "aggregationType": null } ], "alarmFilterConfig": { @@ -979,87 +4307,314 @@ } ], "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" - }, + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 1, "history": { "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, + "timewindowMs": 2592000000, + "interval": 86400000, "fixedTimewindow": { - "startTimeMs": 1708518962586, - "endTimeMs": 1708605362586 + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" + }, + "aggregation": { + "type": "SUM", + "limit": 25000 + }, + "timezone": null + }, + "showTitle": true, + "backgroundColor": "#FFFFFF", + "color": "rgba(0, 0, 0, 0.87)", + "padding": "0px", + "settings": { + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null + } + }, + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" }, - "quickInterval": "CURRENT_DAY" + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, - "showTitle": false, - "backgroundColor": "#fff", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "0px", - "settings": { - "useMarkdownTextFunction": true, - "markdownTextPattern": "", - "markdownTextFunction": "function toShortNumber(number) {\n const rounder = Math.pow(10, 1);\n const powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n ];\n let key = '';\n for (const power of powers) {\n let reduced = number / power.value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n number = reduced;\n key = power.key;\n break;\n }\n }\n \n return number + key;\n}\n\nfunction calculateBarValues(count, limit) {\n let apiUsageBar = '0%';\n let apiUsagePercent = '';\n let apiUsageValue = `${toShortNumber(count)} / ∞`;\n if (Number.isFinite(limit) && limit > 0) {\n var percent = Math.min(100, ((count / limit) * 100));\n apiUsageBar = `${percent}%`\n apiUsagePercent = `${percent.toFixed(2)}%`;\n apiUsageValue = `${toShortNumber(count)} / ${toShortNumber(limit)}`;\n }\n \n return [apiUsageBar, apiUsagePercent, apiUsageValue]\n}\n\nconst [apiUsageBar, apiUsagePercent, apiUsageValue] = calculateBarValues(data[0].count, data[0].limit);\n\n\nreturn `
` +\n '
' +\n '
' +\n '
' +\n '
${title}
' +\n `
${data[0].apiStatus.toUpperCase()}
` +\n '
' +\n '
' +\n `
${data[0].unit}
` +\n '
' +\n `
` +\n '
' +\n '
' +\n `
${apiUsagePercent}
` +\n '
' +\n `
${apiUsageValue}
` +\n '
' +\n '
' +\n '
' +\n '
' +\n '' +\n '
'+\n '' +\n '
' +\n '
'\n", - "applyDefaultMarkdownStyle": false, - "markdownCss": "\n" + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "Alarm created", + "title": "{i18n:api-usage.transport-data-points-daily-activity}", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": {}, "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 16, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "24px" + }, + "titleColor": "rgba(0, 0, 0, 0.87)", "titleTooltip": "", - "dropShadow": true, - "enableFullscreen": false, "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "showLegend": false, - "useDashboardTimewindow": true, - "displayTimewindow": true, "widgetCss": "", "pageSize": 1024, + "units": "", + "decimals": null, "noDataDisplayMessage": "", - "actions": { - "elementClick": [ - { - "name": "email_messages_details", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "alarms_created", - "setEntityId": false, - "stateEntityParamName": null, - "openInSeparateDialog": null, - "dialogTitle": null, - "dialogHideDashboardToolbar": true, - "dialogWidth": null, - "dialogHeight": null, - "openRightLayout": false, - "id": "946ba769-84ac-1507-6baa-94701de8967b" - } - ] - } + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "4px", + "iconSize": "0px" }, "row": 0, "col": 0, - "id": "2d0d6ff6-cd59-51d4-b916-38e22cdd0702" + "id": "79056202-c92b-1dae-ce49-318ec52e2d3b" }, - "120573cc-e246-eb49-7d80-68e5d3b3c0cc": { - "typeFullFqn": "system.cards.markdown_card", - "type": "latest", - "sizeX": 5, - "sizeY": 3.5, + "966ffee7-ba0d-8e54-f903-e8d015ca8cd2": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, "config": { "datasources": [ { @@ -1069,128 +4624,86 @@ "filterId": null, "dataKeys": [ { - "name": "emailApiState", - "type": "timeseries", - "label": "apiState", - "color": "#2196f3", - "settings": {}, - "_hash": 0.8830669138660703, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - }, - { - "name": "emailLimit", - "type": "timeseries", - "label": "limit", - "color": "#4caf50", - "settings": {}, - "_hash": 0.5463603803546802, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "emailCount", - "type": "timeseries", - "label": "count", - "color": "#f44336", - "settings": {}, - "_hash": 0.5564241862015964, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "smsApiState", - "type": "timeseries", - "label": "apiStatePoint", - "color": "#e91e63", - "settings": {}, - "_hash": 0.2969682764607864, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - }, - { - "name": "smsLimit", - "type": "timeseries", - "label": "pointsLimit", - "color": "#9c27b0", - "settings": {}, - "_hash": 0.22082255831864894, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "smsCount", - "type": "timeseries", - "label": "pointsCount", - "color": "#8bc34a", - "settings": {}, - "_hash": 0.6340356364819146, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return value !== '' ? parseInt(value, 10) : 0;", - "aggregationType": "NONE" - }, - { - "name": "notificationApiState", - "type": "timeseries", - "label": "title", - "color": "#3f51b5", - "settings": {}, - "_hash": 0.6894070537030252, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return \"{i18n:api-usage.notifications}\";", - "aggregationType": "NONE" - }, - { - "name": "notificationApiState", - "type": "timeseries", - "label": "unit", - "color": "#3f51b5", - "settings": {}, - "_hash": 0.0005447336528170421, - "aggregationType": "NONE", - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return '{i18n:api-usage.email}';" - }, - { - "name": "notificationApiState", + "name": "transportDataPointsCountHourly", "type": "timeseries", - "label": "pointsUnit", - "color": "#e91e63", - "settings": {}, - "_hash": 0.12117146988088967, - "aggregationType": "NONE", + "label": "{i18n:api-usage.transport-data-points}", + "color": "#4CAF50", + "settings": { + "yAxisId": "default", + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "bar", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "enablePointLabelBackground": false, + "pointLabelBackground": "rgba(255,255,255,0.56)", + "pointShape": "emptyCircle", + "pointSize": 4, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "enableLabelBackground": false, + "labelBackground": "rgba(255,255,255,0.56)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "comparisonSettings": { + "showValuesForComparison": false, + "comparisonValuesLabel": "", + "color": "" + } + }, + "_hash": 0.12814821361119078, + "aggregationType": null, "units": null, "decimals": null, "funcBody": null, - "usePostProcessing": true, - "postFuncBody": "return '{i18n:api-usage.sms}';" + "usePostProcessing": null, + "postFuncBody": null } ], "alarmFilterConfig": { @@ -1201,83 +4714,320 @@ } ], "timewindow": { - "displayValue": "", - "selectedTab": 0, + "hideAggregation": false, + "hideAggInterval": false, + "hideTimezone": false, + "selectedTab": 1, "realtime": { - "realtimeType": 1, + "realtimeType": 0, "interval": 1000, "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY" + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false }, "history": { "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1708518962586, - "endTimeMs": 1708605362586 - }, - "quickInterval": "CURRENT_DAY" + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { - "type": "AVG", + "type": "NONE", "limit": 25000 - } + }, + "timezone": null }, - "showTitle": false, - "backgroundColor": "#fff", + "showTitle": true, + "backgroundColor": "#FFFFFF", "color": "rgba(0, 0, 0, 0.87)", "padding": "0px", "settings": { - "useMarkdownTextFunction": true, - "markdownTextPattern": "", - "markdownTextFunction": "function toShortNumber(number) {\n const rounder = Math.pow(10, 1);\n const powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n ];\n let key = '';\n for (const power of powers) {\n const reduced = number / power.value;\n for (const power of powers) {\n let reduced = number / power.value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n number = reduced;\n key = power.key;\n break;\n }\n }\n }\n \n return number + key;\n}\n\nfunction calculateBarValues(count, limit) {\n let apiUsageBar = '0%';\n let apiUsagePercent = '';\n let apiUsageValue = `${toShortNumber(count)} / ∞`;\n if (Number.isFinite(limit) && limit > 0) {\n var percent = Math.min(100, ((count / limit) * 100));\n apiUsageBar = `${percent}%`\n apiUsagePercent = `${percent.toFixed(2)}%`;\n apiUsageValue = `${toShortNumber(count)} / ${toShortNumber(limit)}`;\n }\n \n return [apiUsageBar, apiUsagePercent, apiUsageValue]\n}\n\nconst [apiUsageBar, apiUsagePercent, apiUsageValue] = calculateBarValues(data[0].count, data[0].limit);\nconst [apiUsageBar2, apiUsagePercent2, apiUsageValue2] = calculateBarValues(data[0].pointsCount, data[0].pointsLimit);\n\nconst apiState = data[0].apiState;\nconst apiStatePoint = data[0].apiStatePoint;\nlet currentState;\nif (apiState === 'DISABLED' || apiStatePoint === 'DISABLED') {\n currentState = 'DISABLED';\n} else if (apiState === 'WARNING' || apiStatePoint === 'WARNING') {\n currentState = 'WARNING';\n} else {\n currentState = 'ENABLED';\n}\n\n\n\nreturn `
` +\n '
' +\n '
' +\n '
' +\n '
' +\n `${data[0].title}` +\n '
' +\n `
${currentState}
` +\n '
' +\n '
' +\n '
' +\n '
' +\n `
${data[0].unit}
` +\n '
' +\n `
` +\n '
' +\n '
' +\n `
${apiUsagePercent}
` +\n '
' +\n `
${apiUsageValue}
` +\n '
' +\n '
' +\n '
' +\n '
' +\n '
' +\n `
${data[0].pointsUnit}
` +\n '
' +\n `
` +\n '
' +\n '
' +\n `
${apiUsagePercent2}
` +\n '
' +\n `
${apiUsageValue2}
` +\n '
' +\n '
' + \n '
' +\n '
' +\n '
' +\n '
' +\n '' +\n '
'+\n '' +\n '
' +\n '
'\n", - "applyDefaultMarkdownStyle": false, - "markdownCss": "\n" + "yAxes": { + "default": { + "units": null, + "decimals": 0, + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "left", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)", + "id": "default", + "order": 0, + "min": null, + "max": null + } + }, + "thresholds": [], + "dataZoom": false, + "stack": false, + "xAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "bottom", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "noAggregationBarWidthSettings": { + "strategy": "group", + "groupWidth": { + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 + }, + "barWidth": { + "relative": true, + "relativeWidth": 2, + "absoluteWidth": 1000 + } + }, + "showLegend": true, + "legendLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendLabelColor": "rgba(0, 0, 0, 0.76)", + "legendConfig": { + "direction": "column", + "position": "bottom", + "sortDataKeys": false, + "showMin": false, + "showMax": false, + "showAvg": false, + "showTotal": true, + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "Notifications (Email/SMS)", - "showTitleIcon": false, - "iconColor": "rgba(0, 0, 0, 0.87)", - "iconSize": "24px", - "titleTooltip": "", + "title": "{i18n:api-usage.transport-data-points-monthly-activity}", "dropShadow": true, - "enableFullscreen": false, - "widgetStyle": {}, - "titleStyle": { - "fontSize": "16px", - "fontWeight": 400 - }, - "showLegend": false, - "useDashboardTimewindow": true, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": {}, + "showTitleIcon": false, + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, "displayTimewindow": true, + "titleFont": { + "size": 16, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "24px" + }, + "titleColor": "rgba(0, 0, 0, 0.87)", + "titleTooltip": "", + "widgetStyle": {}, "widgetCss": "", "pageSize": 1024, + "units": "", + "decimals": null, "noDataDisplayMessage": "", - "actions": { - "elementClick": [ - { - "name": "transport_details", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "notifications", - "setEntityId": false, - "stateEntityParamName": null, - "openInSeparateDialog": null, - "dialogTitle": null, - "dialogHideDashboardToolbar": true, - "dialogWidth": null, - "dialogHeight": null, - "openRightLayout": false, - "id": "46b7cefe-e1f2-67c1-4055-3a214520f869" - } - ] - } + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "4px", + "iconSize": "0px" }, "row": 0, "col": 0, - "id": "120573cc-e246-eb49-7d80-68e5d3b3c0cc" + "id": "966ffee7-ba0d-8e54-f903-e8d015ca8cd2" }, - "63f99d90-23ab-f8c2-3290-1e693ded5a2e": { + "b1a9a51f-e5a6-9d5f-ef5c-25c2a68af1b0": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -1291,72 +5041,81 @@ "filterId": null, "dataKeys": [ { - "name": "transportMsgCountHourly", + "name": "ruleEngineExecutionCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.transport-messages}", - "color": "#2196f3", + "label": "{i18n:api-usage.rule-engine-executions}", + "color": "#AB00FF", "settings": { - "excludeFromStacking": false, - "hideDataByDefault": false, - "disableDataHiding": false, - "removeFromLegend": false, - "showLines": false, - "fillLines": false, - "showPoints": false, - "showPointShape": "circle", - "pointShapeFormatter": "", - "showPointsLineWidth": 5, - "showPointsRadius": 3, - "showSeparateAxis": false, - "axisPosition": "left", - "thresholds": [ - { - "thresholdValueSource": "predefinedValue" + "yAxisId": "default", + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "bar", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "enablePointLabelBackground": false, + "pointLabelBackground": "rgba(255,255,255,0.56)", + "pointShape": "emptyCircle", + "pointSize": 4, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } } - ], - "comparisonSettings": { - "showValuesForComparison": true }, - "type": "bar" - }, - "_hash": 0.0661644137210089, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": false, - "postFuncBody": null, - "aggregationType": null - }, - { - "name": "transportDataPointsCountHourly", - "type": "timeseries", - "label": "{i18n:api-usage.transport-data-points}", - "color": "#4caf50", - "settings": { - "excludeFromStacking": false, - "hideDataByDefault": false, - "disableDataHiding": false, - "removeFromLegend": false, - "showLines": false, - "fillLines": false, - "showPoints": false, - "showPointShape": "circle", - "pointShapeFormatter": "", - "showPointsLineWidth": 5, - "showPointsRadius": 3, - "showSeparateAxis": false, - "axisPosition": "left", - "thresholds": [ - { - "thresholdValueSource": "predefinedValue" + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "enableLabelBackground": false, + "labelBackground": "rgba(255,255,255,0.56)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } } - ], - "comparisonSettings": { - "showValuesForComparison": true }, - "type": "bar" + "comparisonSettings": { + "showValuesForComparison": false, + "comparisonValuesLabel": "", + "color": "" + } }, - "_hash": 0.46849996721308895, + "_hash": 0.5078724779454146, + "aggregationType": null, "units": null, "decimals": null, "funcBody": null, @@ -1372,22 +5131,33 @@ } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, "selectedTab": 0, "realtime": { "realtimeType": 0, + "interval": 3600000, "timewindowMs": 86400000, "quickInterval": "CURRENT_DAY", - "interval": 300000 + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 86400000, + "timewindowMs": 2592000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { - "type": "NONE", - "limit": 50000 + "type": "SUM", + "limit": 25000 }, "timezone": null }, @@ -1460,7 +5230,8 @@ "weight": "400", "lineHeight": "1" }, - "tickLabelColor": null, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -1471,9 +5242,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -1499,7 +5270,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -1516,7 +5288,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -1548,9 +5322,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.transport-hourly-activity}", + "title": "{i18n:api-usage.rule-engine-hourly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -1558,14 +5401,21 @@ "actions": { "headerButton": [ { - "name": "{i18n:api-usage.view-details}", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "transport", + "name": "{i18n:api-usage.view-statistics}", + "buttonType": "icon", + "icon": "show_chart", + "buttonColor": "rgba(0, 0, 0, 0.87)", + "customButtonStyle": {}, + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", + "type": "updateDashboardState", + "targetDashboardStateId": "rule_engine_statistics", "setEntityId": false, "stateEntityParamName": null, "openRightLayout": false, - "id": "6ef12f6a-0266-25cf-6ca5-5dcb772252c6" + "openInSeparateDialog": false, + "openInPopover": false, + "id": "8b57e118-84fc-4add-2536-d3cfde018b83" } ] }, @@ -1607,14 +5457,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "63f99d90-23ab-f8c2-3290-1e693ded5a2e" + "id": "b1a9a51f-e5a6-9d5f-ef5c-25c2a68af1b0" }, - "a2b7e906-2d8a-41a8-99a6-409531bfa743": { + "84fbe63a-bcb6-7bc1-8af0-46b3b1ee5adc": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -1631,8 +5481,9 @@ "name": "ruleEngineExecutionCountHourly", "type": "timeseries", "label": "{i18n:api-usage.rule-engine-executions}", - "color": "#ab00ff", + "color": "#AB00FF", "settings": { + "yAxisId": "default", "showInLegend": true, "dataHiddenByDefault": false, "type": "bar", @@ -1655,6 +5506,8 @@ "lineHeight": "1" }, "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "enablePointLabelBackground": false, + "pointLabelBackground": "rgba(255,255,255,0.56)", "pointShape": "emptyCircle", "pointSize": 4, "fillAreaSettings": { @@ -1681,6 +5534,8 @@ "lineHeight": "1" }, "labelColor": "rgba(0, 0, 0, 0.76)", + "enableLabelBackground": false, + "labelBackground": "rgba(255,255,255,0.56)", "backgroundSettings": { "type": "none", "opacity": 0.4, @@ -1689,15 +5544,20 @@ "end": 0 } } + }, + "comparisonSettings": { + "showValuesForComparison": false, + "comparisonValuesLabel": "", + "color": "" } }, - "_hash": 0.0661644137210089, + "_hash": 0.01948850513940492, + "aggregationType": null, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null, - "aggregationType": null + "postFuncBody": null } ], "alarmFilterConfig": { @@ -1708,22 +5568,23 @@ } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, - "selectedTab": 0, - "realtime": { - "realtimeType": 0, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_YEAR", - "interval": 7200000 + "selectedTab": 1, + "history": { + "historyType": 0, + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { - "type": "NONE", - "limit": 50000 + "type": "SUM", + "limit": 25000 }, "timezone": null }, @@ -1797,6 +5658,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -1807,9 +5669,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -1835,7 +5697,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -1852,7 +5715,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -1884,9 +5749,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.rule-engine-hourly-activity}", + "title": "{i18n:api-usage.rule-engine-daily-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -1895,23 +5829,20 @@ "headerButton": [ { "name": "{i18n:api-usage.view-statistics}", + "buttonType": "icon", "icon": "show_chart", - "type": "openDashboardState", + "buttonColor": "rgba(0, 0, 0, 0.87)", + "customButtonStyle": {}, + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", + "type": "updateDashboardState", "targetDashboardStateId": "rule_engine_statistics", "setEntityId": false, "stateEntityParamName": null, "openRightLayout": false, - "id": "f9f08190-9ed9-d802-5b7a-c57ff84b5648" - }, - { - "name": "{i18n:api-usage.view-details}", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "rule_engine_execution", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "id": "1aec196b-44ba-ddf4-c4dc-c3f60c1eb6fc" + "openInSeparateDialog": false, + "openInPopover": false, + "id": "2592147a-3f62-987a-78c0-cdb775fb4233" } ] }, @@ -1953,14 +5884,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "a2b7e906-2d8a-41a8-99a6-409531bfa743" + "id": "84fbe63a-bcb6-7bc1-8af0-46b3b1ee5adc" }, - "ca996b66-ab7e-f977-152c-98e4ebf2a901": { + "43a2b982-6c02-d9bd-71ee-34e8e6cf8893": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -1974,11 +5905,12 @@ "filterId": null, "dataKeys": [ { - "name": "jsExecutionCountHourly", + "name": "ruleEngineExecutionCount", "type": "timeseries", - "label": "{i18n:api-usage.javascript-executions}", - "color": "#ff9900", + "label": "{i18n:api-usage.rule-engine-executions}", + "color": "#AB00FF", "settings": { + "yAxisId": "default", "showInLegend": true, "dataHiddenByDefault": false, "type": "bar", @@ -2001,6 +5933,8 @@ "lineHeight": "1" }, "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "enablePointLabelBackground": false, + "pointLabelBackground": "rgba(255,255,255,0.56)", "pointShape": "emptyCircle", "pointSize": 4, "fillAreaSettings": { @@ -2027,6 +5961,8 @@ "lineHeight": "1" }, "labelColor": "rgba(0, 0, 0, 0.76)", + "enableLabelBackground": false, + "labelBackground": "rgba(255,255,255,0.56)", "backgroundSettings": { "type": "none", "opacity": 0.4, @@ -2035,81 +5971,14 @@ "end": 0 } } - } - }, - "_hash": 0.0661644137210089, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null, - "aggregationType": null - }, - { - "name": "tbelExecutionCountHourly", - "type": "timeseries", - "label": "{i18n:api-usage.tbel-executions}", - "color": "#4caf50", - "settings": { - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "bar", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "emptyCircle", - "pointSize": 4, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } + "comparisonSettings": { + "showValuesForComparison": false, + "comparisonValuesLabel": "", + "color": "" } }, - "_hash": 0.6818645685001823, + "_hash": 0.5125470598651091, "aggregationType": null, "units": null, "decimals": null, @@ -2126,22 +5995,33 @@ } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, - "selectedTab": 0, + "selectedTab": 1, "realtime": { "realtimeType": 0, - "timewindowMs": 86400000, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, "quickInterval": "CURRENT_DAY", - "interval": 3600000 + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "NONE", - "limit": 50000 + "limit": 25000 }, "timezone": null }, @@ -2215,6 +6095,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -2225,9 +6106,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -2253,11 +6134,109 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" }, - "showTooltip": true, - "tooltipTrigger": "axis", - "tooltipValueFont": { + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { "family": "Roboto", "size": 12, "sizeUnit": "px", @@ -2265,46 +6244,20 @@ "weight": "500", "lineHeight": "16px" }, - "tooltipValueColor": "rgba(0, 0, 0, 0.76)", - "tooltipShowDate": true, - "tooltipDateFormat": { - "format": "yyyy-MM-dd HH:mm:ss", - "lastUpdateAgo": false, - "custom": false - }, - "tooltipDateFont": { + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { "family": "Roboto", - "size": 11, + "size": 12, "sizeUnit": "px", "style": "normal", "weight": "400", "lineHeight": "16px" }, - "tooltipDateColor": "rgba(0, 0, 0, 0.76)", - "tooltipDateInterval": true, - "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", - "tooltipBackgroundBlur": 4, - "animation": { - "animation": true, - "animationThreshold": 2000, - "animationDuration": 1000, - "animationEasing": "cubicOut", - "animationDelay": 0, - "animationDurationUpdate": 300, - "animationEasingUpdate": "cubicOut", - "animationDelayUpdate": 0 - }, - "background": { - "type": "color", - "color": "#fff", - "overlay": { - "enabled": false, - "color": "rgba(255,255,255,0.72)", - "blur": 3 - } - } + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.scripts-hourly-activity}", + "title": "{i18n:api-usage.rule-engine-monthly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -2312,18 +6265,21 @@ "actions": { "headerButton": [ { - "name": "{i18n:api-usage.view-details}", - "icon": "insert_chart", + "name": "{i18n:api-usage.view-statistics}", + "buttonType": "icon", + "icon": "show_chart", + "buttonColor": "rgba(0, 0, 0, 0.87)", + "customButtonStyle": {}, "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", - "type": "openDashboardState", - "targetDashboardStateId": "script_functions", + "type": "updateDashboardState", + "targetDashboardStateId": "rule_engine_statistics", "setEntityId": false, "stateEntityParamName": null, "openRightLayout": false, "openInSeparateDialog": false, "openInPopover": false, - "id": "4687d3f6-8800-a3b6-26e5-0d33f3b828a9" + "id": "b6ba96cf-48b8-f40f-f010-10b95e7dc819" } ] }, @@ -2365,14 +6321,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "ca996b66-ab7e-f977-152c-98e4ebf2a901" + "id": "43a2b982-6c02-d9bd-71ee-34e8e6cf8893" }, - "a3c2f1bb-7d3a-f11c-7b3d-28cd84fdfe34": { + "76fe83c9-c30f-00a5-6299-40c759ca6705": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -2386,68 +6342,34 @@ "filterId": null, "dataKeys": [ { - "name": "storageDataPointsCountHourly", + "name": "jsExecutionCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.data-points-storage-days}", - "color": "#1039ee", + "label": "{i18n:api-usage.javascript-function-executions}", + "color": "#FF9900", "settings": { - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "bar", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "emptyCircle", - "pointSize": 4, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" } + ], + "comparisonSettings": { + "showValuesForComparison": true }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, @@ -2466,22 +6388,33 @@ } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, "selectedTab": 0, "realtime": { "realtimeType": 0, + "interval": 3600000, "timewindowMs": 86400000, "quickInterval": "CURRENT_DAY", - "interval": 300000 + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 86400000, + "timewindowMs": 2592000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { - "type": "NONE", - "limit": 50000 + "type": "SUM", + "limit": 25000 }, "timezone": null }, @@ -2555,6 +6488,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -2565,9 +6499,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -2593,7 +6527,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -2610,7 +6545,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -2642,27 +6579,83 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.telemetry-persistence-hourly-activity}", + "title": "{i18n:api-usage.javascript-function-executions-hourly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", - "actions": { - "headerButton": [ - { - "name": "{i18n:api-usage.view-details}", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "telemetry_persistence", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "id": "16707efb-e572-bd02-c219-55fc1b0f672a" - } - ] - }, + "actions": {}, "showTitleIcon": false, "titleIcon": "thermostat", "iconColor": "#1F6BDD", @@ -2701,14 +6694,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "a3c2f1bb-7d3a-f11c-7b3d-28cd84fdfe34" + "id": "76fe83c9-c30f-00a5-6299-40c759ca6705" }, - "5cebd4f1-ff6e-62f9-025c-8e7583c3d66a": { + "a43598d1-7bfd-f329-ee61-c343f34f069f": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -2722,68 +6715,34 @@ "filterId": null, "dataKeys": [ { - "name": "createdAlarmsCountHourly", + "name": "jsExecutionCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.alarms-created}", - "color": "#d35a00", + "label": "{i18n:api-usage.javascript-function-executions}", + "color": "#FF9900", "settings": { - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "bar", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "emptyCircle", - "pointSize": 4, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" } + ], + "comparisonSettings": { + "showValuesForComparison": true }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, @@ -2802,22 +6761,23 @@ } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, - "selectedTab": 0, - "realtime": { - "realtimeType": 0, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "interval": 300000 + "selectedTab": 1, + "history": { + "historyType": 0, + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { - "type": "NONE", - "limit": 50000 + "type": "SUM", + "limit": 25000 }, "timezone": null }, @@ -2891,6 +6851,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -2901,9 +6862,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -2929,7 +6890,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -2946,7 +6908,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -2978,32 +6942,83 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.alarms-created-hourly-activity}", + "title": "{i18n:api-usage.javascript-function-executions-daily-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", - "actions": { - "headerButton": [ - { - "name": "{i18n:api-usage.view-details}", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "alarms_created", - "setEntityId": false, - "stateEntityParamName": null, - "openInSeparateDialog": null, - "dialogTitle": null, - "dialogHideDashboardToolbar": true, - "dialogWidth": null, - "dialogHeight": null, - "openRightLayout": false, - "id": "371882f9-ea23-3abc-fca8-9449c5dfdd6b" - } - ] - }, + "actions": {}, "showTitleIcon": false, "titleIcon": "thermostat", "iconColor": "#1F6BDD", @@ -3042,14 +7057,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "5cebd4f1-ff6e-62f9-025c-8e7583c3d66a" + "id": "a43598d1-7bfd-f329-ee61-c343f34f069f" }, - "bc0c8840-a9b5-5583-de7b-9e9450f5d8fe": { + "3ebd62a8-dcb7-c96b-8571-e61084248f5b": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -3063,140 +7078,34 @@ "filterId": null, "dataKeys": [ { - "name": "emailCountHourly", - "type": "timeseries", - "label": "{i18n:api-usage.email-messages}", - "color": "#4caf50", - "settings": { - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "bar", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "emptyCircle", - "pointSize": 4, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } - }, - "_hash": 0.1348755140779876, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null, - "aggregationType": null - }, - { - "name": "smsCountHourly", + "name": "jsExecutionCount", "type": "timeseries", - "label": "{i18n:api-usage.sms-messages}", - "color": "#f36021", + "label": "{i18n:api-usage.javascript-function-executions}", + "color": "#FF9900", "settings": { - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "bar", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "emptyCircle", - "pointSize": 4, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" } - } + ], + "comparisonSettings": { + "showValuesForComparison": true + }, + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, @@ -3206,31 +7115,37 @@ "postFuncBody": null, "aggregationType": null } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - } + ] } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, - "selectedTab": 0, + "selectedTab": 1, "realtime": { "realtimeType": 0, - "timewindowMs": 86400000, + "interval": 1000, + "timewindowMs": 60000, "quickInterval": "CURRENT_DAY", - "interval": 300000 + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "NONE", - "limit": 50000 + "limit": 25000 }, "timezone": null }, @@ -3304,6 +7219,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -3314,9 +7230,9 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 3600000 + "relative": true, + "relativeWidth": 6, + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -3342,7 +7258,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -3359,7 +7276,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -3391,32 +7310,83 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.notifications-hourly-activity}", + "title": "{i18n:api-usage.javascript-function-executions-monthly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", - "actions": { - "headerButton": [ - { - "name": "{i18n:api-usage.view-details}", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "notifications", - "setEntityId": false, - "stateEntityParamName": null, - "openInSeparateDialog": null, - "dialogTitle": null, - "dialogHideDashboardToolbar": true, - "dialogWidth": null, - "dialogHeight": null, - "openRightLayout": false, - "id": "49aefac0-ec5e-d6f3-f39c-8744759f4b19" - } - ] - }, + "actions": {}, "showTitleIcon": false, "titleIcon": "thermostat", "iconColor": "#1F6BDD", @@ -3455,14 +7425,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "bc0c8840-a9b5-5583-de7b-9e9450f5d8fe" + "id": "3ebd62a8-dcb7-c96b-8571-e61084248f5b" }, - "0b091dc3-eec3-847e-d0ad-fdf12d474e7a": { + "88e25971-e5cb-eebb-3c7c-1ce33a8a38f4": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -3476,10 +7446,10 @@ "filterId": null, "dataKeys": [ { - "name": "transportMsgCountHourly", + "name": "tbelExecutionCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.transport-messages}", - "color": "#2196f3", + "label": "{i18n:api-usage.tbel-function-executions}", + "color": "#4CAF50", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -3502,71 +7472,49 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null - }, - { - "name": "transportDataPointsCountHourly", - "type": "timeseries", - "label": "{i18n:api-usage.transport-data-points}", - "color": "#4caf50", - "settings": { - "excludeFromStacking": false, - "hideDataByDefault": false, - "disableDataHiding": false, - "removeFromLegend": false, - "showLines": false, - "fillLines": false, - "showPoints": false, - "showPointShape": "circle", - "pointShapeFormatter": "", - "showPointsLineWidth": 5, - "showPointsRadius": 3, - "showSeparateAxis": false, - "axisPosition": "left", - "thresholds": [ - { - "thresholdValueSource": "predefinedValue" - } - ], - "comparisonSettings": { - "showValuesForComparison": true - }, - "type": "bar" - }, - "_hash": 0.46849996721308895, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, - "selectedTab": 1, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, - "timewindowMs": 2592000000, "interval": 86400000, - "fixedTimewindow": { - "startTimeMs": 1709729389667, - "endTimeMs": 1709815789667 - }, - "quickInterval": "CURRENT_DAY" + "timewindowMs": 2592000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "SUM", @@ -3644,6 +7592,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -3654,8 +7603,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -3682,58 +7631,130 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" }, - "showTooltip": true, - "tooltipTrigger": "axis", - "tooltipValueFont": { + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { "family": "Roboto", "size": 12, "sizeUnit": "px", "style": "normal", - "weight": "500", + "weight": "400", "lineHeight": "16px" }, - "tooltipValueColor": "rgba(0, 0, 0, 0.76)", - "tooltipShowDate": true, - "tooltipDateFormat": { - "format": "yyyy-MM-dd HH:mm:ss", - "lastUpdateAgo": false, - "custom": false + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" }, - "tooltipDateFont": { + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { "family": "Roboto", - "size": 11, + "size": 12, "sizeUnit": "px", "style": "normal", "weight": "400", "lineHeight": "16px" }, - "tooltipDateColor": "rgba(0, 0, 0, 0.76)", - "tooltipDateInterval": true, - "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", - "tooltipBackgroundBlur": 4, - "animation": { - "animation": true, - "animationThreshold": 2000, - "animationDuration": 1000, - "animationEasing": "cubicOut", - "animationDelay": 0, - "animationDurationUpdate": 300, - "animationEasingUpdate": "cubicOut", - "animationDelayUpdate": 0 - }, - "background": { - "type": "color", - "color": "#fff", - "overlay": { - "enabled": false, - "color": "rgba(255,255,255,0.72)", - "blur": 3 - } - } + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.transport-daily-activity}", + "title": "{i18n:api-usage.tbel-function-executions-hourly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -3777,14 +7798,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "0b091dc3-eec3-847e-d0ad-fdf12d474e7a" + "id": "88e25971-e5cb-eebb-3c7c-1ce33a8a38f4" }, - "536d7104-49f8-fde6-5827-61b8419f15ec": { + "a1b5731c-e3b3-8cfb-7c50-3abcdce891d2": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -3798,10 +7819,10 @@ "filterId": null, "dataKeys": [ { - "name": "transportMsgCount", + "name": "tbelExecutionCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.transport-messages}", - "color": "#2196f3", + "label": "{i18n:api-usage.tbel-function-executions}", + "color": "#4CAF50", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -3824,7 +7845,8 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, @@ -3833,43 +7855,6 @@ "usePostProcessing": null, "postFuncBody": null, "aggregationType": null - }, - { - "name": "transportDataPointsCount", - "type": "timeseries", - "label": "{i18n:api-usage.transport-data-points}", - "color": "#4caf50", - "settings": { - "excludeFromStacking": false, - "hideDataByDefault": false, - "disableDataHiding": false, - "removeFromLegend": false, - "showLines": false, - "fillLines": false, - "showPoints": false, - "showPointShape": "circle", - "pointShapeFormatter": "", - "showPointsLineWidth": 5, - "showPointsRadius": 3, - "showSeparateAxis": false, - "axisPosition": "left", - "thresholds": [ - { - "thresholdValueSource": "predefinedValue" - } - ], - "comparisonSettings": { - "showValuesForComparison": true - }, - "type": "bar" - }, - "_hash": 0.46849996721308895, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null, - "aggregationType": null } ], "alarmFilterConfig": { @@ -3880,16 +7865,13 @@ } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, "selectedTab": 1, "history": { "historyType": 0, - "timewindowMs": 31536000000, + "timewindowMs": 2592000000, "interval": 86400000, "fixedTimewindow": { "startTimeMs": 1709729389667, @@ -3898,8 +7880,8 @@ "quickInterval": "CURRENT_DAY" }, "aggregation": { - "type": "NONE", - "limit": 1000 + "type": "SUM", + "limit": 25000 }, "timezone": null }, @@ -3973,6 +7955,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -3985,7 +7968,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -4011,7 +7994,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -4028,7 +8012,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -4060,9 +8046,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.transport-daily-activity}", + "title": "{i18n:api-usage.tbel-function-executions-daily-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -4106,14 +8161,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "536d7104-49f8-fde6-5827-61b8419f15ec" + "id": "a1b5731c-e3b3-8cfb-7c50-3abcdce891d2" }, - "c77e417c-ad9d-8e23-3ea1-c75edd653bc0": { + "efc8d4e9-dee2-b677-c378-c1a666543bf4": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -4127,10 +8182,10 @@ "filterId": null, "dataKeys": [ { - "name": "ruleEngineExecutionCountHourly", + "name": "tbelExecutionCount", "type": "timeseries", - "label": "{i18n:api-usage.rule-engine-executions}", - "color": "#ab00ff", + "label": "{i18n:api-usage.tbel-function-executions}", + "color": "#4CAF50", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -4153,38 +8208,47 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } ] } ], "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, "selectedTab": 1, + "realtime": { + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, - "timewindowMs": 2592000000, - "interval": 86400000, - "fixedTimewindow": { - "startTimeMs": 1709729900300, - "endTimeMs": 1709816300300 - }, - "quickInterval": "CURRENT_DAY" + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { - "type": "SUM", + "type": "NONE", "limit": 25000 }, "timezone": null @@ -4259,6 +8323,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -4269,8 +8334,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -4297,11 +8362,109 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" }, - "showTooltip": true, - "tooltipTrigger": "axis", - "tooltipValueFont": { + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { "family": "Roboto", "size": 12, "sizeUnit": "px", @@ -4309,46 +8472,20 @@ "weight": "500", "lineHeight": "16px" }, - "tooltipValueColor": "rgba(0, 0, 0, 0.76)", - "tooltipShowDate": true, - "tooltipDateFormat": { - "format": "yyyy-MM-dd HH:mm:ss", - "lastUpdateAgo": false, - "custom": false - }, - "tooltipDateFont": { + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { "family": "Roboto", - "size": 11, + "size": 12, "sizeUnit": "px", "style": "normal", "weight": "400", "lineHeight": "16px" }, - "tooltipDateColor": "rgba(0, 0, 0, 0.76)", - "tooltipDateInterval": true, - "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", - "tooltipBackgroundBlur": 4, - "animation": { - "animation": true, - "animationThreshold": 2000, - "animationDuration": 1000, - "animationEasing": "cubicOut", - "animationDelay": 0, - "animationDurationUpdate": 300, - "animationEasingUpdate": "cubicOut", - "animationDelayUpdate": 0 - }, - "background": { - "type": "color", - "color": "#fff", - "overlay": { - "enabled": false, - "color": "rgba(255,255,255,0.72)", - "blur": 3 - } - } + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.rule-engine-daily-activity}", + "title": "{i18n:api-usage.tbel-function-executions-monthly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -4392,14 +8529,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "c77e417c-ad9d-8e23-3ea1-c75edd653bc0" + "id": "efc8d4e9-dee2-b677-c378-c1a666543bf4" }, - "870904d2-d2e1-a1b9-ce56-b03fd47259b5": { + "61a23bd5-329f-aae7-3168-8a14a51dc10b": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -4413,10 +8550,10 @@ "filterId": null, "dataKeys": [ { - "name": "ruleEngineExecutionCount", + "name": "storageDataPointsCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.rule-engine-executions}", - "color": "#ab00ff", + "label": "{i18n:api-usage.data-points-storage-days}", + "color": "#1039EE", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -4439,32 +8576,55 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, - "selectedTab": 1, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, - "timewindowMs": 31536000000, - "interval": 1000 + "interval": 86400000, + "timewindowMs": 2592000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { - "type": "NONE", - "limit": 1000 - } + "type": "SUM", + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -4536,6 +8696,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -4548,7 +8709,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -4574,7 +8735,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -4591,7 +8753,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -4623,9 +8787,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.rule-engine-monthly-activity}", + "title": "{i18n:api-usage.data-points-storage-days-hourly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -4669,14 +8902,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "870904d2-d2e1-a1b9-ce56-b03fd47259b5" + "id": "61a23bd5-329f-aae7-3168-8a14a51dc10b" }, - "c66e5060-57fd-11e7-6616-65b82c294ac2": { + "1249d3e2-6b3a-4e4a-65e9-6ed22959871e": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -4690,10 +8923,10 @@ "filterId": null, "dataKeys": [ { - "name": "jsExecutionCountHourly", + "name": "storageDataPointsCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.javascript-executions}", - "color": "#ff9900", + "label": "{i18n:api-usage.data-points-storage-days}", + "color": "#1039EE", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -4716,48 +8949,45 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null - }, - { - "name": "tbelExecutionCountHourly", - "type": "timeseries", - "label": "{i18n:api-usage.tbel-executions}", - "color": "#4caf50", - "settings": { - "type": "bar" - }, - "_hash": 0.5212969314724616, - "aggregationType": null, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 1, "history": { "historyType": 0, "timewindowMs": 2592000000, - "interval": 86400000 + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { "type": "SUM", - "limit": 1000 - } + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -4829,6 +9059,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -4839,8 +9070,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -4867,11 +9098,109 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null + }, + "showTooltip": true, + "tooltipTrigger": "axis", + "tooltipValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "tooltipValueColor": "rgba(0, 0, 0, 0.76)", + "tooltipShowDate": true, + "tooltipDateFormat": { + "format": "yyyy-MM-dd HH:mm:ss", + "lastUpdateAgo": false, + "custom": false, + "auto": true, + "autoDateFormatSettings": {} + }, + "tooltipDateFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" }, - "showTooltip": true, - "tooltipTrigger": "axis", - "tooltipValueFont": { + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { "family": "Roboto", "size": 12, "sizeUnit": "px", @@ -4879,46 +9208,20 @@ "weight": "500", "lineHeight": "16px" }, - "tooltipValueColor": "rgba(0, 0, 0, 0.76)", - "tooltipShowDate": true, - "tooltipDateFormat": { - "format": "yyyy-MM-dd HH:mm:ss", - "lastUpdateAgo": false, - "custom": false - }, - "tooltipDateFont": { + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { "family": "Roboto", - "size": 11, + "size": 12, "sizeUnit": "px", "style": "normal", "weight": "400", "lineHeight": "16px" }, - "tooltipDateColor": "rgba(0, 0, 0, 0.76)", - "tooltipDateInterval": true, - "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", - "tooltipBackgroundBlur": 4, - "animation": { - "animation": true, - "animationThreshold": 2000, - "animationDuration": 1000, - "animationEasing": "cubicOut", - "animationDelay": 0, - "animationDurationUpdate": 300, - "animationEasingUpdate": "cubicOut", - "animationDelayUpdate": 0 - }, - "background": { - "type": "color", - "color": "#fff", - "overlay": { - "enabled": false, - "color": "rgba(255,255,255,0.72)", - "blur": 3 - } - } + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.scripts-daily-activity}", + "title": "{i18n:api-usage.data-points-storage-days-daily-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -4962,14 +9265,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "c66e5060-57fd-11e7-6616-65b82c294ac2" + "id": "1249d3e2-6b3a-4e4a-65e9-6ed22959871e" }, - "d0e8603e-5d2e-9287-e2c6-8ccbe9c66806": { + "c2f2da29-741d-54f6-5f1d-6f6ae616ea02": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -4983,10 +9286,10 @@ "filterId": null, "dataKeys": [ { - "name": "jsExecutionCount", + "name": "storageDataPointsCount", "type": "timeseries", - "label": "{i18n:api-usage.javascript-executions}", - "color": "#ff9900", + "label": "{i18n:api-usage.data-points-storage-days}", + "color": "#1039EE", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -5009,48 +9312,55 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null - }, - { - "name": "tbelExecutionCount", - "type": "timeseries", - "label": "{i18n:api-usage.tbel-executions}", - "color": "#4caf50", - "settings": { - "type": "bar" - }, - "_hash": 0.49748239768082403, - "aggregationType": null, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 1, + "realtime": { + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, + "interval": 2592000000, "timewindowMs": 31536000000, - "interval": 1000 + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "NONE", - "limit": 1000 - } + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -5122,6 +9432,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -5134,12 +9445,12 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { - "relative": false, + "relative": true, "relativeWidth": 2, - "absoluteWidth": 900000000 + "absoluteWidth": 1000 } }, "showLegend": true, @@ -5160,7 +9471,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -5177,7 +9489,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -5209,9 +9523,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.scripts-monthly-activity}", + "title": "{i18n:api-usage.data-points-storage-days-monthly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -5255,14 +9638,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "d0e8603e-5d2e-9287-e2c6-8ccbe9c66806" + "id": "c2f2da29-741d-54f6-5f1d-6f6ae616ea02" }, - "7f4100d2-41be-4954-d353-1d45000dbbbb": { + "8e07dbe5-aa7a-19c1-c470-5f055df948a7": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -5276,10 +9659,10 @@ "filterId": null, "dataKeys": [ { - "name": "storageDataPointsCountHourly", + "name": "createdAlarmsCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.data-points-storage-days}", - "color": "#1039ee", + "label": "{i18n:api-usage.alarms-created}", + "color": "#D35A00", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -5302,32 +9685,55 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, - "selectedTab": 1, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, + "interval": 86400000, "timewindowMs": 2592000000, - "interval": 86400000 + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "SUM", - "limit": 1000 - } + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -5399,6 +9805,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -5409,8 +9816,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -5437,7 +9844,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -5454,7 +9862,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -5486,9 +9896,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.telemetry-persistence-daily-activity}", + "title": "{i18n:api-usage.alarms-created-hourly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -5532,14 +10011,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "7f4100d2-41be-4954-d353-1d45000dbbbb" + "id": "8e07dbe5-aa7a-19c1-c470-5f055df948a7" }, - "226ef8c9-8488-3664-21ac-0b6217336202": { + "e0fe9887-d61c-7813-05a7-f60811e5c5bf": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -5553,10 +10032,10 @@ "filterId": null, "dataKeys": [ { - "name": "storageDataPointsCount", + "name": "createdAlarmsCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.data-points-storage-days}", - "color": "#1039ee", + "label": "{i18n:api-usage.alarms-created}", + "color": "#D35A00", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -5579,32 +10058,45 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 1, "history": { "historyType": 0, - "timewindowMs": 31536000000, - "interval": 1000 + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { - "type": "NONE", - "limit": 1000 - } + "type": "SUM", + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -5676,6 +10168,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -5688,7 +10181,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -5714,7 +10207,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -5731,7 +10225,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -5763,9 +10259,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.telemetry-persistence-monthly-activity}", + "title": "{i18n:api-usage.alarms-created-daily-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -5809,14 +10374,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "226ef8c9-8488-3664-21ac-0b6217336202" + "id": "e0fe9887-d61c-7813-05a7-f60811e5c5bf" }, - "bef6c27b-9fe7-ee92-40d9-9696c501a1f9": { + "99a40c35-c232-16c5-c42f-3cc80ddb9243": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -5830,10 +10395,10 @@ "filterId": null, "dataKeys": [ { - "name": "createdAlarmsCountHourly", + "name": "createdAlarmsCount", "type": "timeseries", "label": "{i18n:api-usage.alarms-created}", - "color": "#d35a00", + "color": "#D35A00", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -5843,7 +10408,7 @@ "fillLines": false, "showPoints": false, "showPointShape": "circle", - "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", + "pointShapeFormatter": "", "showPointsLineWidth": 5, "showPointsRadius": 3, "showSeparateAxis": false, @@ -5856,32 +10421,55 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 1, + "realtime": { + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, - "timewindowMs": 2592000000, - "interval": 86400000 + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { - "type": "SUM", - "limit": 1000 - } + "type": "NONE", + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -5953,6 +10541,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -5963,8 +10552,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -5991,7 +10580,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -6008,7 +10598,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -6040,9 +10632,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.alarms-created-daily-activity}", + "title": "{i18n:api-usage.alarms-created-monthly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -6086,14 +10747,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "bef6c27b-9fe7-ee92-40d9-9696c501a1f9" + "id": "99a40c35-c232-16c5-c42f-3cc80ddb9243" }, - "52305cf8-2258-5745-a0e7-41a171594bb3": { + "407f7630-406e-9c24-cb3d-b1cbdd190f15": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -6107,10 +10768,10 @@ "filterId": null, "dataKeys": [ { - "name": "createdAlarmsCount", + "name": "emailCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.alarms-created}", - "color": "#d35a00", + "label": "{i18n:api-usage.email-messages}", + "color": "#F36021", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -6120,7 +10781,7 @@ "fillLines": false, "showPoints": false, "showPointShape": "circle", - "pointShapeFormatter": "var size = radius * Math.sqrt(Math.PI) / 2;\nctx.moveTo(x - size, y - size);\nctx.lineTo(x + size, y + size);\nctx.moveTo(x - size, y + size);\nctx.lineTo(x + size, y - size);", + "pointShapeFormatter": "", "showPointsLineWidth": 5, "showPointsRadius": 3, "showSeparateAxis": false, @@ -6133,32 +10794,55 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, - "selectedTab": 1, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, - "timewindowMs": 31536000000, - "interval": 1000 + "interval": 86400000, + "timewindowMs": 2592000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { - "type": "NONE", - "limit": 1000 - } + "type": "SUM", + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -6230,6 +10914,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -6242,7 +10927,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -6268,7 +10953,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -6285,7 +10971,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -6317,9 +11005,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.alarms-created-monthly-activity}", + "title": "{i18n:api-usage.emails-hourly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -6363,14 +11120,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "52305cf8-2258-5745-a0e7-41a171594bb3" + "id": "407f7630-406e-9c24-cb3d-b1cbdd190f15" }, - "36fdf999-ca22-9a4c-269d-3f004d792792": { + "b12fb875-89fe-af4c-b344-bf4178de419f": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -6387,7 +11144,7 @@ "name": "emailCountHourly", "type": "timeseries", "label": "{i18n:api-usage.email-messages}", - "color": "#d35a00", + "color": "#F36021", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -6410,32 +11167,45 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 1, "history": { "historyType": 0, "timewindowMs": 2592000000, - "interval": 86400000 + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { "type": "SUM", - "limit": 1000 - } + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -6507,6 +11277,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -6517,8 +11288,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -6545,7 +11316,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -6562,7 +11334,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -6594,9 +11368,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.email-messages-daily-activity}", + "title": "{i18n:api-usage.emails-daily-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -6640,14 +11483,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "36fdf999-ca22-9a4c-269d-3f004d792792" + "id": "b12fb875-89fe-af4c-b344-bf4178de419f" }, - "9a191755-499d-535e-86c5-061102729c02": { + "0b00099d-d131-3e8b-97ce-c4b8d7bcab1f": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -6661,10 +11504,10 @@ "filterId": null, "dataKeys": [ { - "name": "smsCountHourly", + "name": "emailCount", "type": "timeseries", - "label": "{i18n:api-usage.sms-messages}", - "color": "#f36021", + "label": "{i18n:api-usage.email-messages}", + "color": "#F36021", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -6687,32 +11530,50 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } ] } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 1, + "realtime": { + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, - "timewindowMs": 2592000000, - "interval": 86400000 + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { - "type": "SUM", - "limit": 1000 - } + "type": "NONE", + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -6784,6 +11645,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -6794,8 +11656,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -6822,7 +11684,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -6839,7 +11702,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -6871,9 +11736,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.sms-messages-daily-activity}", + "title": "{i18n:api-usage.emails-monthly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -6917,14 +11851,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "9a191755-499d-535e-86c5-061102729c02" + "id": "0b00099d-d131-3e8b-97ce-c4b8d7bcab1f" }, - "4b266318-8357-33ef-ca5a-74cbf90e014f": { + "5648a56e-5a33-3018-92bd-d8e3dbe8aeee": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -6938,10 +11872,10 @@ "filterId": null, "dataKeys": [ { - "name": "emailCount", + "name": "smsCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.email-messages}", - "color": "#d35a00", + "label": "{i18n:api-usage.sms-messages}", + "color": "#F36021", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -6964,32 +11898,55 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, - "selectedTab": 1, + "hideTimezone": false, + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, - "timewindowMs": 31536000000, - "interval": 1000 + "interval": 86400000, + "timewindowMs": 2592000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { - "type": "NONE", - "limit": 1000 - } + "type": "SUM", + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -7061,6 +12018,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -7073,7 +12031,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -7099,7 +12057,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -7116,7 +12075,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -7148,9 +12109,78 @@ "color": "rgba(255,255,255,0.72)", "blur": 3 } - } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" }, - "title": "{i18n:api-usage.email-messages-monthly-activity}", + "title": "{i18n:api-usage.sms-messages-hourly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -7194,14 +12224,14 @@ "displayTypePrefix": true }, "margin": "0px", - "borderRadius": "0px", + "borderRadius": "4px", "iconSize": "0px" }, "row": 0, "col": 0, - "id": "4b266318-8357-33ef-ca5a-74cbf90e014f" + "id": "5648a56e-5a33-3018-92bd-d8e3dbe8aeee" }, - "5aa33b0b-3bd5-7fe7-ee72-f564c2ca79d8": { + "ab5518c1-34d6-7e17-04b4-6520496d5fe1": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -7215,10 +12245,10 @@ "filterId": null, "dataKeys": [ { - "name": "smsCount", + "name": "smsCountHourly", "type": "timeseries", "label": "{i18n:api-usage.sms-messages}", - "color": "#f36021", + "color": "#F36021", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -7241,32 +12271,45 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, + "hideTimezone": false, "selectedTab": 1, "history": { "historyType": 0, - "timewindowMs": 31536000000, - "interval": 1000 + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { - "type": "NONE", - "limit": 1000 - } + "type": "SUM", + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -7338,6 +12381,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -7350,7 +12394,7 @@ "groupWidth": { "relative": true, "relativeWidth": 6, - "absoluteWidth": 900000000 + "absoluteWidth": 1800000 }, "barWidth": { "relative": true, @@ -7376,7 +12420,8 @@ "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -7393,360 +12438,246 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "16px" - }, - "tooltipDateColor": "rgba(0, 0, 0, 0.76)", - "tooltipDateInterval": true, - "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", - "tooltipBackgroundBlur": 4, - "animation": { - "animation": true, - "animationThreshold": 2000, - "animationDuration": 1000, - "animationEasing": "cubicOut", - "animationDelay": 0, - "animationDurationUpdate": 300, - "animationEasingUpdate": "cubicOut", - "animationDelayUpdate": 0 - }, - "background": { - "type": "color", - "color": "#fff", - "overlay": { - "enabled": false, - "color": "rgba(255,255,255,0.72)", - "blur": 3 - } - } - }, - "title": "{i18n:api-usage.sms-messages-monthly-activity}", - "dropShadow": true, - "enableFullscreen": true, - "titleStyle": null, - "configMode": "basic", - "actions": {}, - "showTitleIcon": false, - "titleIcon": "thermostat", - "iconColor": "#1F6BDD", - "useDashboardTimewindow": false, - "displayTimewindow": true, - "titleFont": { - "size": 16, - "sizeUnit": "px", - "family": "Roboto", - "weight": "500", - "style": "normal", - "lineHeight": "24px" - }, - "titleColor": "rgba(0, 0, 0, 0.87)", - "titleTooltip": "", - "widgetStyle": {}, - "widgetCss": "", - "pageSize": 1024, - "units": "", - "decimals": null, - "noDataDisplayMessage": "", - "timewindowStyle": { - "showIcon": false, - "iconSize": "24px", - "icon": null, - "iconPosition": "left", - "font": { - "size": 12, - "sizeUnit": "px", - "family": "Roboto", - "weight": "400", - "style": "normal", - "lineHeight": "16px" - }, - "color": "rgba(0, 0, 0, 0.38)", - "displayTypePrefix": true - }, - "margin": "0px", - "borderRadius": "0px", - "iconSize": "0px" - }, - "row": 0, - "col": 0, - "id": "5aa33b0b-3bd5-7fe7-ee72-f564c2ca79d8" - }, - "fa938580-33db-f1b3-fafc-bc3e3784ad57": { - "typeFullFqn": "system.time_series_chart", - "type": "timeseries", - "sizeX": 8, - "sizeY": 5, - "config": { - "datasources": [ - { - "type": "entity", - "entityAliasId": "2e4c97b0-257a-a1b9-690c-141d9bf2ec6f", - "dataKeys": [ - { - "name": "successfulMsgs", - "type": "timeseries", - "label": "{i18n:api-usage.successful}", - "color": "#4caf50", - "settings": { - "yAxisId": "default", - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "line", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2.5, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "circle", - "pointSize": 12, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } - }, - "_hash": 0.15490750967648736, - "aggregationType": null, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - }, - { - "name": "failedMsgs", - "type": "timeseries", - "label": "{i18n:api-usage.permanent-failures}", - "color": "#ef5350", - "settings": { - "yAxisId": "default", - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "line", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2.5, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "circle", - "pointSize": 12, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } - }, - "_hash": 0.4186621166514697, - "aggregationType": null, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - }, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipDateColor": "rgba(0, 0, 0, 0.76)", + "tooltipDateInterval": true, + "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", + "tooltipBackgroundBlur": 4, + "animation": { + "animation": true, + "animationThreshold": 2000, + "animationDuration": 1000, + "animationEasing": "cubicOut", + "animationDelay": 0, + "animationDurationUpdate": 300, + "animationEasingUpdate": "cubicOut", + "animationDelayUpdate": 0 + }, + "background": { + "type": "color", + "color": "#fff", + "overlay": { + "enabled": false, + "color": "rgba(255,255,255,0.72)", + "blur": 3 + } + }, + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" + }, + "title": "{i18n:api-usage.sms-messages-daily-activity}", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": {}, + "showTitleIcon": false, + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 16, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "24px" + }, + "titleColor": "rgba(0, 0, 0, 0.87)", + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "units": "", + "decimals": null, + "noDataDisplayMessage": "", + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "4px", + "iconSize": "0px" + }, + "row": 0, + "col": 0, + "id": "ab5518c1-34d6-7e17-04b4-6520496d5fe1" + }, + "2e7326ac-98d3-e68c-b7cf-948118a3f140": { + "typeFullFqn": "system.time_series_chart", + "type": "timeseries", + "sizeX": 8, + "sizeY": 5, + "config": { + "datasources": [ + { + "type": "entity", + "name": null, + "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "filterId": null, + "dataKeys": [ { - "name": "tmpFailed", + "name": "smsCount", "type": "timeseries", - "label": "{i18n:api-usage.processing-failures}", - "color": "#ffc107", + "label": "{i18n:api-usage.sms-messages}", + "color": "#F36021", "settings": { - "yAxisId": "default", - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "line", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2.5, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "circle", - "pointSize": 12, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } + "excludeFromStacking": false, + "hideDataByDefault": false, + "disableDataHiding": false, + "removeFromLegend": false, + "showLines": false, + "fillLines": false, + "showPoints": false, + "showPointShape": "circle", + "pointShapeFormatter": "", + "showPointsLineWidth": 5, + "showPointsRadius": 3, + "showSeparateAxis": false, + "axisPosition": "left", + "thresholds": [ + { + "thresholdValueSource": "predefinedValue" } + ], + "comparisonSettings": { + "showValuesForComparison": true }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } + "type": "bar", + "yAxisId": "default" }, - "_hash": 0.49891007198715376, - "aggregationType": null, + "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null - } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - }, - "latestDataKeys": [ - { - "name": "queueName", - "type": "entityField", - "label": "Queue name", - "color": "#ffc107", - "settings": {}, - "_hash": 0.7021721434431745 - }, - { - "name": "serviceId", - "type": "entityField", - "label": "Service Id", - "color": "#607d8b", - "settings": {}, - "_hash": 0.5924381120750077 + "postFuncBody": null, + "aggregationType": null } ] } ], "timewindow": { - "hideInterval": false, "hideAggregation": false, "hideAggInterval": false, - "selectedTab": 0, + "hideTimezone": false, + "selectedTab": 1, "realtime": { - "timewindowMs": 3600000, - "interval": 1000 + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { "type": "NONE", - "limit": 10000 - } + "limit": 25000 + }, + "timezone": null }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -7818,6 +12749,7 @@ "lineHeight": "1" }, "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, "showTicks": true, "ticksColor": "rgba(0, 0, 0, 0.54)", "showLine": true, @@ -7828,8 +12760,8 @@ "noAggregationBarWidthSettings": { "strategy": "group", "groupWidth": { - "relative": false, - "relativeWidth": 2, + "relative": true, + "relativeWidth": 6, "absoluteWidth": 1800000 }, "barWidth": { @@ -7852,11 +12784,12 @@ "direction": "column", "position": "bottom", "sortDataKeys": false, - "showMin": true, - "showMax": true, + "showMin": false, + "showMax": false, "showAvg": false, "showTotal": true, - "showLatest": false + "showLatest": false, + "valueFormat": null }, "showTooltip": true, "tooltipTrigger": "axis", @@ -7873,7 +12806,9 @@ "tooltipDateFormat": { "format": "yyyy-MM-dd HH:mm:ss", "lastUpdateAgo": false, - "custom": false + "custom": false, + "auto": true, + "autoDateFormatSettings": {} }, "tooltipDateFont": { "family": "Roboto", @@ -7885,13 +12820,12 @@ }, "tooltipDateColor": "rgba(0, 0, 0, 0.76)", "tooltipDateInterval": true, - "tooltipHideZeroValues": true, "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", "tooltipBackgroundBlur": 4, "animation": { "animation": true, "animationThreshold": 2000, - "animationDuration": 300, + "animationDuration": 1000, "animationEasing": "cubicOut", "animationDelay": 0, "animationDurationUpdate": 300, @@ -7907,406 +12841,405 @@ "blur": 3 } }, - "padding": "12px" - }, - "title": "{i18n:api-usage.queue-stats}", - "dropShadow": true, - "enableFullscreen": true, - "titleStyle": null, - "configMode": "basic", - "actions": {}, - "showTitleIcon": false, - "titleIcon": "thermostat", - "iconColor": "#1F6BDD", - "useDashboardTimewindow": false, - "displayTimewindow": true, - "titleFont": { - "size": 16, - "sizeUnit": "px", - "family": "Roboto", - "weight": "500", - "style": "normal", - "lineHeight": "24px" - }, - "titleColor": "rgba(0, 0, 0, 0.87)", - "titleTooltip": "", - "widgetStyle": {}, - "widgetCss": "", - "pageSize": 1024, - "units": "", - "decimals": null, - "noDataDisplayMessage": "", - "timewindowStyle": { - "showIcon": false, - "iconSize": "24px", - "icon": null, - "iconPosition": "left", - "font": { - "size": 12, - "sizeUnit": "px", - "family": "Roboto", - "weight": "400", - "style": "normal", - "lineHeight": "16px" - }, - "color": "rgba(0, 0, 0, 0.38)", - "displayTypePrefix": true - }, - "margin": "0px", - "borderRadius": "0px", - "iconSize": "0px" - }, - "row": 0, - "col": 0, - "id": "fa938580-33db-f1b3-fafc-bc3e3784ad57" - }, - "2ee89893-4e38-5331-95b7-3fd4f310c5a7": { - "typeFullFqn": "system.time_series_chart", - "type": "timeseries", - "sizeX": 8, - "sizeY": 5, - "config": { - "datasources": [ - { - "type": "entity", - "entityAliasId": "2e4c97b0-257a-a1b9-690c-141d9bf2ec6f", - "dataKeys": [ - { - "name": "timeoutMsgs", - "type": "timeseries", - "label": "{i18n:api-usage.permanent-timeouts}", - "color": "#4caf50", - "settings": { - "yAxisId": "default", - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "line", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2.5, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "circle", - "pointSize": 12, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } - }, - "_hash": 0.565222981550328, - "aggregationType": null, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - }, - { - "name": "tmpTimeout", - "type": "timeseries", - "label": "{i18n:api-usage.processing-timeouts}", - "color": "#9c27b0", - "settings": { - "yAxisId": "default", - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "line", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2.5, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "pointShape": "circle", - "pointSize": 12, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - } - }, - "_hash": 0.2679547062508352, - "aggregationType": null, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] + "comparisonEnabled": false, + "timeForComparison": "previousInterval", + "comparisonCustomIntervalValue": 7200000, + "comparisonXAxis": { + "show": true, + "label": "", + "labelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "600", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.54)", + "position": "top", + "showTickLabels": true, + "tickLabelFont": { + "family": "Roboto", + "size": 10, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" }, - "latestDataKeys": [ - { - "name": "queueName", - "type": "entityField", - "label": "Queue name", - "color": "#f44336", - "settings": {}, - "_hash": 0.7066844328378095 - }, - { - "name": "serviceId", - "type": "entityField", - "label": "Service Id", - "color": "#ffc107", - "settings": {}, - "_hash": 0.1371570237026627 - } - ] + "tickLabelColor": "rgba(0, 0, 0, 0.54)", + "ticksFormat": {}, + "showTicks": true, + "ticksColor": "rgba(0, 0, 0, 0.54)", + "showLine": true, + "lineColor": "rgba(0, 0, 0, 0.54)", + "showSplitLines": true, + "splitLinesColor": "rgba(0, 0, 0, 0.12)" + }, + "grid": { + "show": false, + "backgroundColor": null, + "borderWidth": 1, + "borderColor": "#ccc" + }, + "legendColumnTitleFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", + "legendValueFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "500", + "lineHeight": "16px" + }, + "legendValueColor": "rgba(0, 0, 0, 0.87)", + "tooltipLabelFont": { + "family": "Roboto", + "size": 12, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "16px" + }, + "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", + "tooltipHideZeroValues": null, + "padding": "12px" + }, + "title": "{i18n:api-usage.sms-messages-monthly-activity}", + "dropShadow": true, + "enableFullscreen": true, + "titleStyle": null, + "configMode": "basic", + "actions": {}, + "showTitleIcon": false, + "titleIcon": "thermostat", + "iconColor": "#1F6BDD", + "useDashboardTimewindow": false, + "displayTimewindow": true, + "titleFont": { + "size": 16, + "sizeUnit": "px", + "family": "Roboto", + "weight": "500", + "style": "normal", + "lineHeight": "24px" + }, + "titleColor": "rgba(0, 0, 0, 0.87)", + "titleTooltip": "", + "widgetStyle": {}, + "widgetCss": "", + "pageSize": 1024, + "units": "", + "decimals": null, + "noDataDisplayMessage": "", + "timewindowStyle": { + "showIcon": false, + "iconSize": "24px", + "icon": null, + "iconPosition": "left", + "font": { + "size": 12, + "sizeUnit": "px", + "family": "Roboto", + "weight": "400", + "style": "normal", + "lineHeight": "16px" + }, + "color": "rgba(0, 0, 0, 0.38)", + "displayTypePrefix": true + }, + "margin": "0px", + "borderRadius": "4px", + "iconSize": "0px" + }, + "row": 0, + "col": 0, + "id": "2e7326ac-98d3-e68c-b7cf-948118a3f140" + }, + "07e3a570-c961-b72d-3371-5b29f3617b73": { + "typeFullFqn": "system.api_usage", + "type": "latest", + "sizeX": 7.5, + "sizeY": 3, + "config": { + "datasources": [ + { + "type": "entity", + "name": "", + "dataKeys": [] } ], "timewindow": { - "hideInterval": false, - "hideAggregation": false, - "hideAggInterval": false, + "displayValue": "", "selectedTab": 0, "realtime": { - "timewindowMs": 3600000, - "interval": 1000 + "realtimeType": 1, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, + "history": { + "historyType": 0, + "interval": 1000, + "timewindowMs": 60000, + "fixedTimewindow": { + "startTimeMs": 1756302747649, + "endTimeMs": 1756389147649 + }, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { - "type": "NONE", - "limit": 10000 + "type": "AVG", + "limit": 25000 } }, "showTitle": true, - "backgroundColor": "#FFFFFF", + "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", - "padding": "0px", + "padding": "0", "settings": { - "yAxes": { - "default": { - "units": null, - "decimals": 0, - "show": true, - "label": "", - "labelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "600", - "lineHeight": "1" + "dsEntityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", + "dataKeys": [ + { + "label": "{i18n:api-usage.transport-messages}", + "state": "transport_messages", + "status": { + "name": "transportApiState", + "label": "transportApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" }, - "labelColor": "rgba(0, 0, 0, 0.54)", - "position": "left", - "showTickLabels": true, - "tickLabelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" + "maxLimit": { + "name": "transportMsgLimit", + "label": "transportMsgLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" }, - "tickLabelColor": "rgba(0, 0, 0, 0.54)", - "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", - "showTicks": true, - "ticksColor": "rgba(0, 0, 0, 0.54)", - "showLine": true, - "lineColor": "rgba(0, 0, 0, 0.54)", - "showSplitLines": true, - "splitLinesColor": "rgba(0, 0, 0, 0.12)", - "id": "default", - "order": 0, - "min": null, - "max": null - } - }, - "thresholds": [], - "dataZoom": false, - "stack": false, - "xAxis": { - "show": true, - "label": "", - "labelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "600", - "lineHeight": "1" + "current": { + "name": "transportMsgCount", + "label": "transportMsgCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } + }, + { + "label": "{i18n:api-usage.transport-data-points}", + "state": "transport_data_points", + "status": { + "name": "transportApiState", + "label": "transportApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "transportDataPointsLimit", + "label": "transportDataPointsLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "transportDataPointsCount", + "label": "transportDataPointsCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } + }, + { + "label": "{i18n:api-usage.rule-engine-executions}", + "state": "rule_engine_executions", + "status": { + "name": "ruleEngineApiState", + "label": "ruleEngineApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "ruleEngineExecutionLimit", + "label": "ruleEngineExecutionLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "ruleEngineExecutionCount", + "label": "ruleEngineExecutionCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } + }, + { + "label": "{i18n:api-usage.javascript-function-executions}", + "state": "javascript_function_executions", + "status": { + "name": "jsExecutionApiState", + "label": "jsExecutionApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "jsExecutionLimit", + "label": "jsExecutionLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "jsExecutionCount", + "label": "jsExecutionCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } + }, + { + "label": "{i18n:api-usage.tbel-function-executions}", + "state": "tbel_function_executions", + "status": { + "name": "tbelExecutionApiState", + "label": "tbelExecutionApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "tbelExecutionLimit", + "label": "tbelExecutionLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "tbelExecutionCount", + "label": "tbelExecutionCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } + }, + { + "label": "{i18n:api-usage.data-points-storage-days}", + "state": "data_points_storage_days", + "status": { + "name": "dbApiState", + "label": "dbApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "storageDataPointsLimit", + "label": "storageDataPointsLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "storageDataPointsCount", + "label": "storageDataPointsCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } }, - "labelColor": "rgba(0, 0, 0, 0.54)", - "position": "bottom", - "showTickLabels": true, - "tickLabelFont": { - "family": "Roboto", - "size": 10, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" + { + "label": "{i18n:api-usage.alarms-created}", + "state": "alarms_created", + "status": { + "name": "alarmApiState", + "label": "alarmApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "createdAlarmsLimit", + "label": "createdAlarmsLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "createdAlarmsCount", + "label": "createdAlarmsCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } }, - "tickLabelColor": "rgba(0, 0, 0, 0.54)", - "showTicks": true, - "ticksColor": "rgba(0, 0, 0, 0.54)", - "showLine": true, - "lineColor": "rgba(0, 0, 0, 0.54)", - "showSplitLines": true, - "splitLinesColor": "rgba(0, 0, 0, 0.12)" - }, - "noAggregationBarWidthSettings": { - "strategy": "group", - "groupWidth": { - "relative": false, - "relativeWidth": 2, - "absoluteWidth": 1800000 + { + "label": "{i18n:api-usage.emails}", + "state": "emails", + "status": { + "name": "emailApiState", + "label": "emailApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "emailLimit", + "label": "emailLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "emailCount", + "label": "emailCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } }, - "barWidth": { - "relative": true, - "relativeWidth": 2, - "absoluteWidth": 1000 + { + "label": "{i18n:api-usage.sms}", + "state": "sms", + "status": { + "name": "notificationApiState", + "label": "notificationApiState", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "maxLimit": { + "name": "smsLimit", + "label": "smsLimit", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + }, + "current": { + "name": "smsCount", + "label": "smsCount", + "type": "timeseries", + "settings": {}, + "color": "#2196f3" + } } - }, - "showLegend": true, - "legendLabelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "16px" - }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", - "legendConfig": { - "direction": "column", - "position": "bottom", - "sortDataKeys": false, - "showMin": true, - "showMax": true, - "showAvg": false, - "showTotal": true, - "showLatest": false - }, - "showTooltip": true, - "tooltipTrigger": "axis", - "tooltipValueFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "500", - "lineHeight": "16px" - }, - "tooltipValueColor": "rgba(0, 0, 0, 0.76)", - "tooltipShowDate": true, - "tooltipDateFormat": { - "format": "yyyy-MM-dd HH:mm:ss", - "lastUpdateAgo": false, - "custom": false - }, - "tooltipDateFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "16px" - }, - "tooltipDateColor": "rgba(0, 0, 0, 0.76)", - "tooltipDateInterval": true, - "tooltipHideZeroValues": true, - "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", - "tooltipBackgroundBlur": 4, - "animation": { - "animation": true, - "animationThreshold": 2000, - "animationDuration": 300, - "animationEasing": "cubicOut", - "animationDelay": 0, - "animationDurationUpdate": 300, - "animationEasingUpdate": "cubicOut", - "animationDelayUpdate": 0 - }, + ], + "targetDashboardState": "default", "background": { "type": "color", "color": "#fff", @@ -8316,58 +13249,54 @@ "blur": 3 } }, - "padding": "12px" + "padding": "0" }, - "title": "{i18n:api-usage.processing-failures-and-timeouts}", - "dropShadow": true, - "enableFullscreen": true, - "titleStyle": null, - "configMode": "basic", - "actions": {}, + "title": "{i18n:api-usage.api-usage}", + "decimals": null, "showTitleIcon": false, - "titleIcon": "thermostat", - "iconColor": "#1F6BDD", - "useDashboardTimewindow": false, - "displayTimewindow": true, - "titleFont": { - "size": 16, - "sizeUnit": "px", - "family": "Roboto", - "weight": "500", - "style": "normal", - "lineHeight": "24px" - }, - "titleColor": "rgba(0, 0, 0, 0.87)", "titleTooltip": "", + "dropShadow": true, + "enableFullscreen": false, "widgetStyle": {}, - "widgetCss": "", + "widgetCss": ".tb-widget-header {\n height: 48px;\n align-items: center !important;\n padding: 5px 10px 0 10px;\n}", + "titleStyle": {}, "pageSize": 1024, - "units": "", - "decimals": null, "noDataDisplayMessage": "", - "timewindowStyle": { - "showIcon": false, - "iconSize": "24px", - "icon": null, - "iconPosition": "left", - "font": { - "size": 12, - "sizeUnit": "px", - "family": "Roboto", - "weight": "400", - "style": "normal", - "lineHeight": "16px" - }, - "color": "rgba(0, 0, 0, 0.38)", - "displayTypePrefix": true + "actions": { + "headerButton": [ + { + "name": "Go back", + "buttonType": "stroked", + "showIcon": true, + "icon": "undo", + "buttonColor": "#305680", + "buttonBorderColor": "#0000001F", + "customButtonStyle": { + "padding": "0 16px" + }, + "useShowWidgetActionFunction": true, + "showWidgetActionFunction": "console.log(widgetContext.stateController.getStateId(), widgetContext.settings.targetDashboardState)\nreturn widgetContext.stateController.getStateId() !== widgetContext.settings.targetDashboardState && widgetContext.settings.targetDashboardState;", + "type": "custom", + "customFunction": "const state = widgetContext.settings.targetDashboardState?.length ? widgetContext.settings.targetDashboardState : 'default';\nwidgetContext.stateController.updateState(state, widgetContext.stateController.getStateParams(), false);", + "openInSeparateDialog": false, + "openInPopover": false, + "id": "1ea1cca6-47d1-3539-d051-9535129fb12b" + } + ] }, - "margin": "0px", - "borderRadius": "0px", - "iconSize": "0px" + "titleFont": { + "size": 16, + "sizeUnit": "px", + "family": null, + "weight": "500", + "style": null, + "lineHeight": "21px" + }, + "borderRadius": "4px" }, "row": 0, "col": 0, - "id": "2ee89893-4e38-5331-95b7-3fd4f310c5a7" + "id": "07e3a570-c961-b72d-3371-5b29f3617b73" } }, "states": { @@ -8377,346 +13306,777 @@ "layouts": { "main": { "widgets": { - "aab68ab5-8e40-8694-c55c-8eb1c89b88fb": { - "sizeX": 4, - "sizeY": 2, + "07e3a570-c961-b72d-3371-5b29f3617b73": { + "sizeX": 24, + "sizeY": 39, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "color": "rgba(0,0,0,0.870588)", + "columns": 12, + "margin": 8, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 100, + "outerMargin": true, + "layoutType": "divider", + "minColumns": 12, + "viewFormat": "grid", + "rowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "85240e8c-7af7-90a9-ad0a-726013c479a6": { + "sizeX": 7, + "sizeY": 5, + "row": 0, + "col": 0 + }, + "d0a10a8f-8f48-f9d6-8306-d12af9b49690": { + "sizeX": 7, + "sizeY": 5, + "row": 0, + "col": 7 + }, + "4544080d-9b6f-b592-9cd4-0e0335d33857": { + "sizeX": 7, + "sizeY": 5, + "row": 5, + "col": 0 + }, + "5d0f2f57-499d-1324-8e1b-cfbc0b3149d2": { + "sizeX": 7, + "sizeY": 5, + "row": 5, + "col": 7 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 8, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": false, + "mobileRowHeight": 70, + "mobileDisplayLayoutFirst": false + } + } + } + }, + "rule_engine_statistics": { + "name": "{i18n:api-usage.rule-engine-statistics}", + "root": false, + "layouts": { + "main": { + "widgets": { + "a669cf86-e715-efa4-dd9a-b839abf499e9": { + "sizeX": 24, + "sizeY": 5, + "row": 7, + "col": 0 + }, + "fa938580-33db-f1b3-fafc-bc3e3784ad57": { + "sizeX": 12, + "sizeY": 7, + "row": 0, + "col": 0 + }, + "2ee89893-4e38-5331-95b7-3fd4f310c5a7": { + "sizeX": 12, + "sizeY": 7, + "row": 0, + "col": 12 + } + }, + "gridSettings": { + "backgroundColor": "#eeeeee", + "color": "rgba(0,0,0,0.870588)", + "columns": 24, + "margin": 8, + "backgroundSizeMode": "100%", + "autoFillHeight": true, + "backgroundImageUrl": null, + "mobileAutoFillHeight": false, + "mobileRowHeight": 70, + "outerMargin": true, + "layoutType": "default", + "minColumns": 24, + "viewFormat": "grid", + "rowHeight": 70 + } + } + } + }, + "transport_messages": { + "name": "{i18n:api-usage.transport-messages}", + "root": false, + "layouts": { + "main": { + "widgets": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { + "sizeX": 24, + "sizeY": 39, "row": 0, "col": 0 - }, - "a84fa70a-ddfa-3b24-9aa4-cf9ce91f919a": { - "sizeX": 4, - "sizeY": 2, - "row": 0, - "col": 4 - }, - "d70d26d4-e22d-4ca9-9ea7-f9c87c093321": { - "sizeX": 4, - "sizeY": 2, - "row": 0, - "col": 8 - }, - "4d3ea95c-3188-9872-1817-2f989c7729e0": { - "sizeX": 4, - "sizeY": 2, - "row": 0, - "col": 12 - }, - "2d0d6ff6-cd59-51d4-b916-38e22cdd0702": { - "sizeX": 4, - "sizeY": 2, - "row": 0, - "col": 16 - }, - "120573cc-e246-eb49-7d80-68e5d3b3c0cc": { - "sizeX": 4, - "sizeY": 2, - "row": 0, - "col": 20 - }, - "63f99d90-23ab-f8c2-3290-1e693ded5a2e": { - "sizeX": 8, + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 8, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "51608a74-f213-d8c9-8df8-b42238ef93a6": { + "sizeX": 12, "sizeY": 4, - "row": 2, + "row": 0, "col": 0 }, - "a2b7e906-2d8a-41a8-99a6-409531bfa743": { - "sizeX": 8, + "fb155957-1af4-233e-e2fb-09e648e75d6e": { + "sizeX": 6, "sizeY": 4, - "row": 2, - "col": 8 + "row": 4, + "col": 0 }, - "ca996b66-ab7e-f977-152c-98e4ebf2a901": { - "sizeX": 8, + "4817e33b-87be-5be3-eaca-ca68a2eb4e0c": { + "sizeX": 6, "sizeY": 4, - "row": 2, - "col": 16 - }, - "a3c2f1bb-7d3a-f11c-7b3d-28cd84fdfe34": { - "sizeX": 8, + "row": 4, + "col": 6 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 8, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": false, + "mobileRowHeight": 70, + "mobileDisplayLayoutFirst": false + } + } + } + }, + "transport_data_points": { + "name": "{i18n:api-usage.transport-data-points}", + "root": false, + "layouts": { + "main": { + "widgets": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { + "sizeX": 24, + "sizeY": 39, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 8, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "9e00cc90-520d-2108-1d2f-bba68ed5cbf1": { + "sizeX": 12, "sizeY": 4, - "row": 6, + "row": 0, "col": 0 }, - "5cebd4f1-ff6e-62f9-025c-8e7583c3d66a": { - "sizeX": 8, + "79056202-c92b-1dae-ce49-318ec52e2d3b": { + "sizeX": 6, "sizeY": 4, - "row": 6, - "col": 8 + "row": 4, + "col": 0 }, - "bc0c8840-a9b5-5583-de7b-9e9450f5d8fe": { - "sizeX": 8, + "966ffee7-ba0d-8e54-f903-e8d015ca8cd2": { + "sizeX": 6, "sizeY": 4, - "row": 6, - "col": 16 + "row": 4, + "col": 6 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 8, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, - "mobileRowHeight": 100, - "outerMargin": true + "mobileRowHeight": 70, + "mobileDisplayLayoutFirst": false } } } }, - "transport": { - "name": "{i18n:api-usage.transport}", + "rule_engine_executions": { + "name": "{i18n:api-usage.rule-engine-executions}", "root": false, "layouts": { "main": { "widgets": { - "0b091dc3-eec3-847e-d0ad-fdf12d474e7a": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { "sizeX": 24, - "sizeY": 6, + "sizeY": 39, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 8, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "b1a9a51f-e5a6-9d5f-ef5c-25c2a68af1b0": { + "sizeX": 12, + "sizeY": 4, "row": 0, "col": 0 }, - "536d7104-49f8-fde6-5827-61b8419f15ec": { - "sizeX": 24, - "sizeY": 6, - "row": 6, + "84fbe63a-bcb6-7bc1-8af0-46b3b1ee5adc": { + "sizeX": 6, + "sizeY": 4, + "row": 4, "col": 0 + }, + "43a2b982-6c02-d9bd-71ee-34e8e6cf8893": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 6 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 8, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "outerMargin": true + "mobileDisplayLayoutFirst": false } } } }, - "rule_engine_execution": { - "name": "{i18n:api-usage.rule-engine-executions}", + "javascript_function_executions": { + "name": "{i18n:api-usage.javascript-function-executions}", "root": false, "layouts": { "main": { "widgets": { - "c77e417c-ad9d-8e23-3ea1-c75edd653bc0": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { "sizeX": 24, - "sizeY": 6, + "sizeY": 39, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 8, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "76fe83c9-c30f-00a5-6299-40c759ca6705": { + "sizeX": 12, + "sizeY": 4, "row": 0, "col": 0 }, - "870904d2-d2e1-a1b9-ce56-b03fd47259b5": { - "sizeX": 24, - "sizeY": 6, - "row": 6, + "a43598d1-7bfd-f329-ee61-c343f34f069f": { + "sizeX": 6, + "sizeY": 4, + "row": 4, "col": 0 + }, + "3ebd62a8-dcb7-c96b-8571-e61084248f5b": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 6 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 8, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "outerMargin": true + "mobileDisplayLayoutFirst": false } } } }, - "telemetry_persistence": { - "name": "{i18n:api-usage.telemetry-persistence}", + "tbel_function_executions": { + "name": "{i18n:api-usage.tbel-function-executions}", "root": false, "layouts": { "main": { "widgets": { - "7f4100d2-41be-4954-d353-1d45000dbbbb": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { "sizeX": 24, - "sizeY": 6, + "sizeY": 39, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 8, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "88e25971-e5cb-eebb-3c7c-1ce33a8a38f4": { + "sizeX": 12, + "sizeY": 4, "row": 0, "col": 0 }, - "226ef8c9-8488-3664-21ac-0b6217336202": { - "sizeX": 24, - "sizeY": 6, - "row": 6, + "a1b5731c-e3b3-8cfb-7c50-3abcdce891d2": { + "sizeX": 6, + "sizeY": 4, + "row": 4, "col": 0 + }, + "efc8d4e9-dee2-b677-c378-c1a666543bf4": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 6 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 8, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "outerMargin": true + "mobileDisplayLayoutFirst": false } } } }, - "rule_engine_statistics": { - "name": "{i18n:api-usage.rule-engine-statistics}", + "data_points_storage_days": { + "name": "{i18n:api-usage.data-points-storage-days}", "root": false, "layouts": { "main": { "widgets": { - "a669cf86-e715-efa4-dd9a-b839abf499e9": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { "sizeX": 24, - "sizeY": 5, - "row": 7, + "sizeY": 39, + "row": 0, "col": 0 - }, - "fa938580-33db-f1b3-fafc-bc3e3784ad57": { + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 8, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "61a23bd5-329f-aae7-3168-8a14a51dc10b": { "sizeX": 12, - "sizeY": 7, + "sizeY": 4, "row": 0, "col": 0 }, - "2ee89893-4e38-5331-95b7-3fd4f310c5a7": { - "sizeX": 12, - "sizeY": 7, - "row": 0, - "col": 12 + "1249d3e2-6b3a-4e4a-65e9-6ed22959871e": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 0 + }, + "c2f2da29-741d-54f6-5f1d-6f6ae616ea02": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 6 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 8, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "outerMargin": true + "mobileDisplayLayoutFirst": false } } } }, - "notifications": { - "name": "{i18n:api-usage.notifications-email-sms}", + "emails": { + "name": "{i18n:api-usage.emails}", "root": false, "layouts": { "main": { "widgets": { - "36fdf999-ca22-9a4c-269d-3f004d792792": { - "sizeX": 12, - "sizeY": 6, + "07e3a570-c961-b72d-3371-5b29f3617b73": { + "sizeX": 24, + "sizeY": 39, "row": 0, "col": 0 - }, - "9a191755-499d-535e-86c5-061102729c02": { + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 8, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "407f7630-406e-9c24-cb3d-b1cbdd190f15": { "sizeX": 12, - "sizeY": 6, + "sizeY": 4, "row": 0, - "col": 12 + "col": 0 }, - "4b266318-8357-33ef-ca5a-74cbf90e014f": { - "sizeX": 12, - "sizeY": 6, - "row": 6, + "b12fb875-89fe-af4c-b344-bf4178de419f": { + "sizeX": 6, + "sizeY": 4, + "row": 4, "col": 0 }, - "5aa33b0b-3bd5-7fe7-ee72-f564c2ca79d8": { - "sizeX": 12, - "sizeY": 6, - "row": 6, - "col": 12 + "0b00099d-d131-3e8b-97ce-c4b8d7bcab1f": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 6 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 8, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "outerMargin": true + "mobileDisplayLayoutFirst": false } } } }, - "alarms_created": { - "name": "{i18n:api-usage.alarms-created}", + "sms": { + "name": "{i18n:api-usage.sms}", "root": false, "layouts": { "main": { "widgets": { - "bef6c27b-9fe7-ee92-40d9-9696c501a1f9": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { "sizeX": 24, - "sizeY": 6, + "sizeY": 39, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 8, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "5648a56e-5a33-3018-92bd-d8e3dbe8aeee": { + "sizeX": 12, + "sizeY": 4, "row": 0, "col": 0 }, - "52305cf8-2258-5745-a0e7-41a171594bb3": { - "sizeX": 24, - "sizeY": 6, - "row": 6, + "ab5518c1-34d6-7e17-04b4-6520496d5fe1": { + "sizeX": 6, + "sizeY": 4, + "row": 4, "col": 0 + }, + "2e7326ac-98d3-e68c-b7cf-948118a3f140": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 6 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 8, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "outerMargin": true + "mobileDisplayLayoutFirst": false } } } }, - "script_functions": { - "name": "{i18n:api-usage.scripts}", + "alarms_created": { + "name": "{i18n:api-usage.alarms-created}", "root": false, "layouts": { "main": { "widgets": { - "c66e5060-57fd-11e7-6616-65b82c294ac2": { + "07e3a570-c961-b72d-3371-5b29f3617b73": { "sizeX": 24, - "sizeY": 6, + "sizeY": 39, + "row": 0, + "col": 0 + } + }, + "gridSettings": { + "layoutType": "divider", + "backgroundColor": "#eeeeee", + "columns": 12, + "margin": 8, + "outerMargin": true, + "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", + "autoFillHeight": true, + "rowHeight": 70, + "backgroundImageUrl": null, + "mobileAutoFillHeight": true, + "mobileRowHeight": 70, + "layoutDimension": { + "type": "percentage", + "leftWidthPercentage": 30 + } + } + }, + "right": { + "widgets": { + "8e07dbe5-aa7a-19c1-c470-5f055df948a7": { + "sizeX": 12, + "sizeY": 4, "row": 0, "col": 0 }, - "d0e8603e-5d2e-9287-e2c6-8ccbe9c66806": { - "sizeX": 24, - "sizeY": 6, - "row": 6, + "e0fe9887-d61c-7813-05a7-f60811e5c5bf": { + "sizeX": 6, + "sizeY": 4, + "row": 4, "col": 0 + }, + "99a40c35-c232-16c5-c42f-3cc80ddb9243": { + "sizeX": 6, + "sizeY": 4, + "row": 4, + "col": 6 } }, "gridSettings": { + "layoutType": "divider", "backgroundColor": "#eeeeee", - "color": "rgba(0,0,0,0.870588)", - "columns": 24, - "margin": 5, + "columns": 12, + "margin": 8, + "outerMargin": true, "backgroundSizeMode": "100%", + "minColumns": 12, + "viewFormat": "grid", "autoFillHeight": true, + "rowHeight": 70, "backgroundImageUrl": null, "mobileAutoFillHeight": false, "mobileRowHeight": 70, - "outerMargin": true + "mobileDisplayLayoutFirst": false } } } @@ -8743,9 +14103,6 @@ }, "filters": {}, "timewindow": { - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false, "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, @@ -8776,7 +14133,7 @@ "dashboardLogoUrl": null, "hideToolbar": false, "showUpdateDashboardImage": false, - "dashboardCss": ".card .bars-row {\n flex: 1;\n display: flex;\n flex-direction: row;\n}\n\n.card .bar-column {\n flex: 1;\n display: flex;\n flex-direction: column;\n}\n\n\n.card {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n}\n\n.card > img {\n height: 0;\n}\n\n.card .content {\n flex: 1; \n padding: 12px 12px 0;\n display: flex;\n box-sizing: border-box;\n}\n\n.card .content .column {\n display: flex;\n flex-direction: column; \n justify-content: space-around;\n flex: 1;\n}\n\n.card .content .title-row {\n display: flex;\n flex-direction: row;\n padding-bottom: 10px;\n}\n\n.card .title {\n flex: 1;\n font-size: 20px;\n font-weight: 400;\n color: #666666;\n}\n\n.card .state {\n text-transform: uppercase;\n font-size: 20px;\n font-weight: bold;\n}\n\n.card.enabled .state {\n color: #00B260;\n}\n\n.card.warning .state {\n color: #FFAD6F;\n}\n\n.card.disabled .state {\n color: #F73243;\n}\n\n.card .bar-container {\n flex: 1;\n display: flex;\n flex-direction: column;\n justify-content: center;\n}\n\n.card .bar {\n flex: 1;\n max-height: 30px;\n margin-top: 3.5px;\n margin-bottom: 4px;\n background-color: #F0F0F0;\n border: 1px solid #DADCDB;\n border-radius: 2px;\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, .2);\n}\n\n.card.enabled .bar {\n border-color: #00B260;\n background-color: #F0FBF7;\n}\n\n.card.warning .bar {\n border-color: #FFAD6F;\n background-color: #FFFAF6;\n}\n\n.card.disabled .bar {\n border-color: #F73243;\n background-color: #FFF0F0;\n}\n\n.card .bar .bar-fill {\n background-color: #F0F0F0;\n border-radius: 2px;\n height: 100%;\n width: 0%;\n}\n\n.card.enabled .bar-fill {\n background-color: #00C46C;\n}\n\n.card.warning .bar-fill {\n background-color: #FFD099;\n}\n\n.card.disabled .bar-fill {\n background-color: #FF9494;\n}\n\n.card .bar-labels {\n height: 20px;\n font-size: 16px;\n color: #666;\n display: flex;\n flex-direction: row;\n}\n\n\n.card .mat-mdc-button-base {\n text-transform: uppercase;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.card .mdc-button__label {\n pointer-events: none;\n}\n\n.action-row {\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n padding: 8px 0;\n}\n\n.card .unit {\n color: #666666;\n}\n\n@media screen and (min-width: 960px) and (max-width: 1279px) {\n .card .title {\n font-size: 12px;\n }\n .card .state {\n font-size: 12px;\n }\n .card .unit {\n font-size: 8px;\n }\n .card .bar-labels {\n font-size: 8px;\n }\n .card .mat-mdc-button-base {\n font-size: 8px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1280px) and (max-width: 1599px) {\n .card .title {\n font-size: 14px;\n }\n .card .state {\n font-size: 14px;\n }\n .card .unit {\n font-size: 10px;\n }\n .card .bar-labels {\n font-size: 10px;\n }\n .card .mat-mdc-button-base {\n font-size: 10px;\n }\n .card .action-row {\n padding: 0;\n }\n}\n\n@media screen and (min-width: 1600px) and (max-width: 1919px) {\n .card .title {\n font-size: 16px;\n }\n .card .state {\n font-size: 16px;\n }\n .card .unit {\n font-size: 12px;\n }\n .card .bar-labels {\n font-size: 12px;\n }\n .card .mat-mdc-button-base {\n font-size: 12px;\n }\n .card .action-row {\n padding: 0;\n }\n} " + "dashboardCss": "" } }, "name": "Api Usage" diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 80328181f6..f1fc7835f5 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -865,15 +865,18 @@ "api-features": "API features", "api-usage": "API usage", "alarm": "Alarm", - "alarms-created": "Alarms created", + "alarms-created": "Created alarms", "queue-stats": "Queue Stats", "processing-failures-and-timeouts": "Processing Failures and Timeouts", "exceptions": "Exceptions", - "alarms-created-daily-activity": "Alarms created daily activity", - "alarms-created-hourly-activity": "Alarms created hourly activity", - "alarms-created-monthly-activity": "Alarms created monthly activity", + "alarms-created-daily-activity": "Created alarms daily activity", + "alarms-created-hourly-activity": "Created alarms hourly activity", + "alarms-created-monthly-activity": "Created alarms monthly activity", "data-points": "Data points", "data-points-storage-days": "Data points storage days", + "data-points-storage-days-hourly-activity": "Data points storage days hourly activity", + "data-points-storage-days-daily-activity": "Data points storage days daily activity", + "data-points-storage-days-monthly-activity": "Data points storage days monthly activity", "device-api": "Device API", "email": "Email", "email-messages": "Email messages", @@ -899,14 +902,15 @@ "processing-timeouts": "${entityName} Processing Timeouts", "rule-chain": "Rule Chain", "rule-engine": "Rule Engine", - "rule-engine-daily-activity": "Rule Engine daily activity", "rule-engine-executions": "Rule Engine executions", "rule-engine-hourly-activity": "Rule Engine hourly activity", + "rule-engine-daily-activity": "Rule Engine daily activity", "rule-engine-monthly-activity": "Rule Engine monthly activity", "rule-engine-statistics": "Rule Engine Statistics", "rule-node": "Rule Node", "sms": "SMS", "sms-messages": "SMS messages", + "sms-messages-hourly-activity": "SMS messages hourly activity", "sms-messages-daily-activity": "SMS messages daily activity", "sms-messages-monthly-activity": "SMS messages monthly activity", "successful": "${entityName} Successful", @@ -916,13 +920,40 @@ "telemetry-persistence-hourly-activity": "Telemetry persistence hourly activity", "telemetry-persistence-monthly-activity": "Telemetry persistence monthly activity", "transport": "Transport", + "transport-msg-hourly-activity": "Transport messages hourly activity", + "transport-msg-daily-activity": "Transport messages daily activity", + "transport-msg-monthly-activity": "Transport messages monthly activity", "transport-daily-activity": "Transport daily activity", "transport-data-points": "Transport data points", - "transport-hourly-activity": "Transport hourly activity", - "transport-messages": "Transport messages", - "transport-monthly-activity": "Transport monthly activity", + "transport-data-points-hourly-activity": "Transport data points hourly activity", + "transport-data-points-daily-activity": "Transport data points daily activity", + "transport-data-points-monthly-activity": "Transport data points monthly activity", "view-details": "View details", - "view-statistics": "View statistics" + "view-statistics": "View statistics", + "transport-messages": "Transport messages", + "transport-messages-hourly-activity": "Transport messages hourly activity", + "transport-data-point-hourly-activity": "Transport data point hourly activity", + "javascript-function-executions": "JavaScript function executions", + "javascript-function-executions-hourly-activity": "JavaScript function executions hourly activity", + "javascript-function-executions-daily-activity": "JavaScript function executions daily activity", + "javascript-function-executions-monthly-activity": "JavaScript function executions monthly activity", + "tbel-function-executions": "TBEL function executions", + "tbel-function-executions-hourly-activity": "TBEL function executions hourly activity", + "tbel-function-executions-daily-activity": "TBEL function executions daily activity", + "tbel-function-executions-monthly-activity": "TBEL function executions monthly activity", + "created-reports": "Created reports", + "created-reports-hourly-activity": "Created reports hourly activity", + "created-reports-daily-activity": "Created reports daily activity", + "created-reports-monthly-activity": "Created reports monthly activity", + "emails": "Emails", + "emails-hourly-activity": "Emails hourly activity", + "emails-daily-activity": "Emails daily activity", + "emails-monthly-activity": "Emails monthly activity", + "status": { + "enabled": "Enabled", + "disabled": "Disabled", + "warning": "Warning" + } }, "api-limit": { "cassandra-write-queries-core": "Rest API Cassandra write queries", @@ -9483,6 +9514,18 @@ "how-to-create-customer-and-assign-dashboard": "How to create Customer and assign Dashboard" } } + }, + "api-usage": { + "api-usage": "API usage", + "label": "Label", + "state-name": "State name", + "status": "Status", + "limit": "Max limit", + "current-number": "Current number", + "add-key": "Add key", + "no-key": "No key", + "delete-key": "Delete key", + "target-dashboard-state": "Target dashboard state" } }, "color": { From eb36297b691175c6c641e0164192dd3da5a1aed0 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Fri, 29 Aug 2025 16:29:16 +0300 Subject: [PATCH 082/216] refactoring after merge to PE --- ...CalculatedFieldEntityMessageProcessor.java | 2 +- ...alculatedFieldManagerMessageProcessor.java | 4 +- .../service/cf/CalculatedFieldResult.java | 10 +-- ...faultCalculatedFieldProcessingService.java | 62 +++------------- .../cf/ctx/state/CalculatedFieldCtx.java | 11 ++- .../utils/CalculatedFieldArgumentUtils.java | 72 +++++++++++++++++++ .../GeofencingCalculatedFieldStateTest.java | 1 - .../CfArgumentDynamicSourceConfiguration.java | 2 +- ...eofencingCalculatedFieldConfiguration.java | 9 ++- ...upportedCalculatedFieldConfiguration.java} | 2 +- .../geofencing/EntityCoordinates.java | 4 -- .../server/common/util/ProtoUtils.java | 4 +- common/proto/src/main/proto/queue.proto | 6 +- .../server/common/util/ProtoUtilsTest.java | 2 +- .../geo/PerimeterDefinitionSerializer.java | 4 +- .../dao/cf/BaseCalculatedFieldService.java | 4 +- .../CalculatedFieldDataValidator.java | 11 ++- .../service/CalculatedFieldServiceTest.java | 3 - 18 files changed, 115 insertions(+), 98 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java rename common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/{ScheduleSupportedCalculatedFieldConfiguration.java => ScheduledUpdateSupportedCalculatedFieldConfiguration.java} (89%) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 4b277eb3a3..fa51cd2e3d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -321,7 +321,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM callback.onSuccess(); } if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) { - systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, calculationResult.getResult().toString(), null); + systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, calculationResult.toStringOrElseNull(), null); } } } else { diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index 6aa47a48b8..ee30a4b030 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -27,7 +27,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.cf.configuration.ScheduleSupportedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.DeviceId; @@ -482,7 +482,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private void scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(CalculatedFieldCtx cfCtx) { CalculatedField cf = cfCtx.getCalculatedField(); - if (!(cf.getConfiguration() instanceof ScheduleSupportedCalculatedFieldConfiguration scheduledCfConfig)) { + if (!(cf.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledCfConfig)) { return; } if (!scheduledCfConfig.isScheduledUpdateEnabled()) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java index 7bec9ae964..c779c27419 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/CalculatedFieldResult.java @@ -34,14 +34,8 @@ public final class CalculatedFieldResult { (result.isTextual() && result.asText().isEmpty()); } - public String getResultAsString() { - if (result == null) { - return null; - } - if (result.isTextual()) { - return result.asText(); - } - return result.toString(); + public String toStringOrElseNull() { + return result == null ? null : result.toString(); } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index a6e64bde81..a31944a132 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -23,7 +23,6 @@ import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.math.NumberUtils; import org.springframework.stereotype.Service; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; @@ -31,7 +30,6 @@ import org.thingsboard.server.actors.calculatedField.MultipleTbCallback; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; @@ -45,11 +43,7 @@ import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; 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.KvEntry; import org.thingsboard.server.common.data.kv.ReadTsKvQuery; -import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.relation.RelationTypeGroup; @@ -76,10 +70,6 @@ import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; -import org.thingsboard.server.service.cf.ctx.state.GeofencingCalculatedFieldState; -import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState; -import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState; -import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry; import java.util.ArrayList; @@ -96,6 +86,9 @@ import java.util.stream.Collectors; import static org.thingsboard.server.common.data.DataConstants.SCOPE; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultKvEntry; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformSingleValueArgument; import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto; @TbRuleEngineComponent @@ -144,7 +137,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP var result = createStateByType(ctx); result.updateState(ctx, resolveArgumentFutures(argFutures)); return result; - }, calculatedFieldCallbackExecutor); + }, MoreExecutors.directExecutor()); } @Override @@ -171,7 +164,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP default -> { var resolvedEntityIdsFuture = resolveGeofencingEntityIds(ctx.getTenantId(), entityId, entry); argFutures.put(entry.getKey(), Futures.transformAsync(resolvedEntityIdsFuture, resolvedEntityIds -> - fetchGeofencingKvEntry(ctx.getTenantId(), resolvedEntityIds, entry.getValue()), calculatedFieldCallbackExecutor)); + fetchGeofencingKvEntry(ctx.getTenantId(), resolvedEntityIds, entry.getValue()), MoreExecutors.directExecutor())); } } } @@ -210,7 +203,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP OutputType type = calculatedFieldResult.getType(); TbMsgType msgType = OutputType.ATTRIBUTES.equals(type) ? TbMsgType.POST_ATTRIBUTES_REQUEST : TbMsgType.POST_TELEMETRY_REQUEST; TbMsgMetaData md = OutputType.ATTRIBUTES.equals(type) ? new TbMsgMetaData(Map.of(SCOPE, calculatedFieldResult.getScope().name())) : TbMsgMetaData.EMPTY; - TbMsg msg = TbMsg.newMsg().type(msgType).originator(entityId).previousCalculatedFieldIds(cfIds).metaData(md).data(calculatedFieldResult.getResult().toString()).build(); + TbMsg msg = TbMsg.newMsg().type(msgType).originator(entityId).previousCalculatedFieldIds(cfIds).metaData(md).data(calculatedFieldResult.toStringOrElseNull()).build(); clusterService.pushMsgToRuleEngine(tenantId, entityId, msg, new TbQueueCallback() { @Override public void onSuccess(TbQueueMsgMetadata metadata) { @@ -337,20 +330,16 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP ListenableFuture>> allFutures = Futures.allAsList(kvFutures); return Futures.transform(allFutures, entries -> ArgumentEntry.createGeofencingValueArgument(entries.stream() - .collect(Collectors.toMap(Entry::getKey, Entry::getValue))), - calculatedFieldCallbackExecutor - ); + .collect(Collectors.toMap(Entry::getKey, Entry::getValue))), MoreExecutors.directExecutor()); } private ListenableFuture fetchKvEntry(TenantId tenantId, EntityId entityId, Argument argument) { return switch (argument.getRefEntityKey().getType()) { case TS_ROLLING -> fetchTsRolling(tenantId, entityId, argument); case ATTRIBUTE -> transformSingleValueArgument( - Futures.transform( - attributesService.find(tenantId, entityId, argument.getRefEntityKey().getScope(), argument.getRefEntityKey().getKey()), + Futures.transform(attributesService.find(tenantId, entityId, argument.getRefEntityKey().getScope(), argument.getRefEntityKey().getKey()), result -> result.or(() -> Optional.of(new BaseAttributeKvEntry(createDefaultKvEntry(argument), System.currentTimeMillis(), 0L))), - calculatedFieldCallbackExecutor) - ); + calculatedFieldCallbackExecutor)); case TS_LATEST -> transformSingleValueArgument( Futures.transform( timeseriesService.findLatest(tenantId, entityId, argument.getRefEntityKey().getKey()), @@ -359,16 +348,6 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP }; } - private ListenableFuture transformSingleValueArgument(ListenableFuture> kvEntryFuture) { - return Futures.transform(kvEntryFuture, kvEntry -> { - if (kvEntry.isPresent() && kvEntry.get().getValue() != null) { - return ArgumentEntry.createSingleValueArgument(kvEntry.get()); - } else { - return new SingleValueArgumentEntry(); - } - }, calculatedFieldCallbackExecutor); - } - private ListenableFuture fetchTsRolling(TenantId tenantId, EntityId entityId, Argument argument) { long currentTime = System.currentTimeMillis(); long timeWindow = argument.getTimeWindow() == 0 ? System.currentTimeMillis() : argument.getTimeWindow(); @@ -383,29 +362,6 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP return Futures.transform(tsRollingFuture, tsRolling -> tsRolling == null ? new TsRollingArgumentEntry(limit, timeWindow) : ArgumentEntry.createTsRollingArgument(tsRolling, limit, timeWindow), calculatedFieldCallbackExecutor); } - private KvEntry createDefaultKvEntry(Argument argument) { - String key = argument.getRefEntityKey().getKey(); - String defaultValue = argument.getDefaultValue(); - if (StringUtils.isBlank(defaultValue)) { - return new StringDataEntry(key, null); - } - if (NumberUtils.isParsable(defaultValue)) { - return new DoubleDataEntry(key, Double.parseDouble(defaultValue)); - } - if ("true".equalsIgnoreCase(defaultValue) || "false".equalsIgnoreCase(defaultValue)) { - return new BooleanDataEntry(key, Boolean.parseBoolean(defaultValue)); - } - return new StringDataEntry(key, defaultValue); - } - - private CalculatedFieldState createStateByType(CalculatedFieldCtx ctx) { - return switch (ctx.getCfType()) { - case SIMPLE -> new SimpleCalculatedFieldState(ctx.getArgNames()); - case SCRIPT -> new ScriptCalculatedFieldState(ctx.getArgNames()); - case GEOFENCING -> new GeofencingCalculatedFieldState(ctx.getArgNames()); - }; - } - private static class TbCallbackWrapper implements TbQueueCallback { private final TbCallback callback; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 139218e8f3..08ee81282f 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalcula import org.thingsboard.server.common.data.cf.configuration.ExpressionBasedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; -import org.thingsboard.server.common.data.cf.configuration.ScheduleSupportedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; @@ -67,11 +67,10 @@ public class CalculatedFieldCtx { private String expression; private boolean useLatestTs; private TbelInvokeService tbelInvokeService; + private RelationService relationService; private CalculatedFieldScriptEngine calculatedFieldScriptEngine; private ThreadLocal customExpression; - private RelationService relationService; - private boolean initialized; private long maxDataPointsPerRollingArg; @@ -129,7 +128,7 @@ public class CalculatedFieldCtx { } } case GEOFENCING -> initialized = true; - default -> { + case SIMPLE -> { if (isValidExpression(expression)) { this.customExpression = ThreadLocal.withInitial(() -> new ExpressionBuilder(expression) @@ -323,8 +322,8 @@ public class CalculatedFieldCtx { } public boolean hasSchedulingConfigChanges(CalculatedFieldCtx other) { - if (calculatedField.getConfiguration() instanceof ScheduleSupportedCalculatedFieldConfiguration thisConfig - && other.calculatedField.getConfiguration() instanceof ScheduleSupportedCalculatedFieldConfiguration otherConfig) { + if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration thisConfig + && other.calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration otherConfig) { boolean refreshTriggerChanged = thisConfig.isScheduledUpdateEnabled() != otherConfig.isScheduledUpdateEnabled(); boolean refreshIntervalChanged = thisConfig.getScheduledUpdateIntervalSec() != otherConfig.getScheduledUpdateIntervalSec(); return refreshTriggerChanged || refreshIntervalChanged; diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java new file mode 100644 index 0000000000..008fc17acd --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java @@ -0,0 +1,72 @@ +/** + * 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.utils; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import org.apache.commons.lang3.math.NumberUtils; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; + +import java.util.Optional; + +public class CalculatedFieldArgumentUtils { + + public static ListenableFuture transformSingleValueArgument(ListenableFuture> kvEntryFuture) { + return Futures.transform(kvEntryFuture, kvEntry -> { + if (kvEntry.isPresent() && kvEntry.get().getValue() != null) { + return ArgumentEntry.createSingleValueArgument(kvEntry.get()); + } + return new SingleValueArgumentEntry(); + }, MoreExecutors.directExecutor()); + } + + public static KvEntry createDefaultKvEntry(Argument argument) { + String key = argument.getRefEntityKey().getKey(); + String defaultValue = argument.getDefaultValue(); + if (StringUtils.isBlank(defaultValue)) { + return new StringDataEntry(key, null); + } + if (NumberUtils.isParsable(defaultValue)) { + return new DoubleDataEntry(key, Double.parseDouble(defaultValue)); + } + if ("true".equalsIgnoreCase(defaultValue) || "false".equalsIgnoreCase(defaultValue)) { + return new BooleanDataEntry(key, Boolean.parseBoolean(defaultValue)); + } + return new StringDataEntry(key, defaultValue); + } + + public static CalculatedFieldState createStateByType(CalculatedFieldCtx ctx) { + return switch (ctx.getCfType()) { + case SIMPLE -> new SimpleCalculatedFieldState(ctx.getArgNames()); + case SCRIPT -> new ScriptCalculatedFieldState(ctx.getArgNames()); + case GEOFENCING -> new GeofencingCalculatedFieldState(ctx.getArgNames()); + }; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index 21198133df..a7204f259f 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -448,7 +448,6 @@ public class GeofencingCalculatedFieldStateTest { var config = new GeofencingCalculatedFieldConfiguration(); EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); - entityCoordinates.setRefEntityId(DEVICE_ID); config.setEntityCoordinates(entityCoordinates); ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("allowedZones", "zone", reportStrategy, true); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java index 3fe432917b..f36071615e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java @@ -26,7 +26,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; property = "type" ) @JsonSubTypes({ - @JsonSubTypes.Type(value = RelationQueryDynamicSourceConfiguration.class, name = "RELATION_QUERY"), + @JsonSubTypes.Type(value = RelationQueryDynamicSourceConfiguration.class, name = "RELATION_QUERY") }) @JsonIgnoreProperties(ignoreUnknown = true) public interface CfArgumentDynamicSourceConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java index 1c51db2274..ef20ad16bb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java @@ -20,15 +20,17 @@ import lombok.Data; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; +import org.thingsboard.server.common.data.id.EntityId; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; @Data -public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduleSupportedCalculatedFieldConfiguration { +public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration { private EntityCoordinates entityCoordinates; private List zoneGroups; @@ -49,6 +51,11 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal return args; } + @Override + public List getReferencedEntities() { + return zoneGroups.stream().map(ZoneGroupConfiguration::getRefEntityId).filter(Objects::nonNull).toList(); + } + @Override public Output getOutput() { return output; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduleSupportedCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java similarity index 89% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduleSupportedCalculatedFieldConfiguration.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java index d5aa9e1b22..0d386577ab 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduleSupportedCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java @@ -17,7 +17,7 @@ package org.thingsboard.server.common.data.cf.configuration; import com.fasterxml.jackson.annotation.JsonIgnore; -public interface ScheduleSupportedCalculatedFieldConfiguration extends CalculatedFieldConfiguration { +public interface ScheduledUpdateSupportedCalculatedFieldConfiguration extends CalculatedFieldConfiguration { @JsonIgnore boolean isScheduledUpdateEnabled(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinates.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinates.java index 07876f16b9..9aed948b76 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinates.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinates.java @@ -35,9 +35,6 @@ public class EntityCoordinates { private final String latitudeKeyName; private final String longitudeKeyName; - @Nullable - private EntityId refEntityId; - public void validate() { if (StringUtils.isBlank(latitudeKeyName)) { throw new IllegalArgumentException("Entity coordinates latitude key name must be specified!"); @@ -56,7 +53,6 @@ public class EntityCoordinates { private Argument toArgument(String keyName) { var argument = new Argument(); - argument.setRefEntityId(refEntityId); argument.setRefEntityKey(new ReferencedEntityKey(keyName, ArgumentType.TS_LATEST, null)); return argument; } diff --git a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java index d2d0e59c27..f5a07cf07e 100644 --- a/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java +++ b/common/proto/src/main/java/org/thingsboard/server/common/util/ProtoUtils.java @@ -1379,14 +1379,14 @@ public class ProtoUtils { public static TransportProtos.EntityIdProto toProto(EntityId entityId) { return TransportProtos.EntityIdProto.newBuilder() - .setEntityType(toProto(entityId.getEntityType())) .setEntityIdMSB(getMsb(entityId)) .setEntityIdLSB(getLsb(entityId)) + .setType(toProto(entityId.getEntityType())) .build(); } public static EntityId fromProto(TransportProtos.EntityIdProto entityIdProto) { - return EntityIdFactory.getByTypeAndUuid(fromProto(entityIdProto.getEntityType()), new UUID(entityIdProto.getEntityIdMSB(), entityIdProto.getEntityIdLSB())); + return EntityIdFactory.getByTypeAndUuid(fromProto(entityIdProto.getType()), new UUID(entityIdProto.getEntityIdMSB(), entityIdProto.getEntityIdLSB())); } private static boolean isNotNull(Object obj) { diff --git a/common/proto/src/main/proto/queue.proto b/common/proto/src/main/proto/queue.proto index e232d1973d..a05fdd5d36 100644 --- a/common/proto/src/main/proto/queue.proto +++ b/common/proto/src/main/proto/queue.proto @@ -83,9 +83,9 @@ enum ApiUsageRecordKeyProto { } message EntityIdProto { - EntityTypeProto entityType = 1; - int64 entityIdMSB = 2; - int64 entityIdLSB = 3; + int64 entityIdMSB = 1; + int64 entityIdLSB = 2; + EntityTypeProto type = 4; } /** diff --git a/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java b/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java index 0a2952827a..0f4733cafb 100644 --- a/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java +++ b/common/proto/src/test/java/org/thingsboard/server/common/util/ProtoUtilsTest.java @@ -357,7 +357,7 @@ class ProtoUtilsTest { // toProto TransportProtos.EntityIdProto proto = ProtoUtils.toProto(original); assertThat(proto).isNotNull(); - assertThat(proto.getEntityType().getNumber()).isEqualTo(entityType.getProtoNumber()); + assertThat(proto.getType().getNumber()).isEqualTo(entityType.getProtoNumber()); assertThat(proto.getEntityIdMSB()).isEqualTo(uuid.getMostSignificantBits()); assertThat(proto.getEntityIdLSB()).isEqualTo(uuid.getLeastSignificantBits()); diff --git a/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializer.java b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializer.java index 386a7e67ff..d27aafecc0 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializer.java +++ b/common/util/src/main/java/org/thingsboard/common/util/geo/PerimeterDefinitionSerializer.java @@ -29,9 +29,9 @@ public class PerimeterDefinitionSerializer extends JsonSerializer } private void validateNumberOfArgumentsPerCF(TenantId tenantId, CalculatedField calculatedField) { + if (!(calculatedField instanceof ArgumentsBasedCalculatedFieldConfiguration argumentsBasedCfg)) { + return; + } long maxArgumentsPerCF = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxArgumentsPerCF); if (maxArgumentsPerCF <= 0) { return; } - if (CalculatedFieldType.GEOFENCING.equals(calculatedField.getType()) && maxArgumentsPerCF < 3) { - throw new DataValidationException("Geofencing calculated field requires at least 3 arguments, but the system limit is " + - maxArgumentsPerCF + ". Contact your administrator to increase the limit." - ); - } - if (calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration configuration - && configuration.getArguments().size() > maxArgumentsPerCF) { + if (argumentsBasedCfg.getArguments().size() > maxArgumentsPerCF) { throw new DataValidationException("Calculated field arguments limit reached!"); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index 80e82bb25f..81041180c7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java @@ -109,7 +109,6 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { // Coordinates: TS_LATEST, no dynamic source EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); - entityCoordinates.setRefEntityId(device.getId()); cfg.setEntityCoordinates(entityCoordinates); // Zone-group argument (ATTRIBUTE) — no DYNAMIC configuration, so no scheduling even if the scheduled interval is set @@ -156,7 +155,6 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { // Coordinates: TS_LATEST, no dynamic source EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); - entityCoordinates.setRefEntityId(device.getId()); cfg.setEntityCoordinates(entityCoordinates); // Zone-group argument (ATTRIBUTE) — make it DYNAMIC so scheduling is enabled @@ -208,7 +206,6 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { // Coordinates: TS_LATEST, no dynamic source EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); - entityCoordinates.setRefEntityId(device.getId()); cfg.setEntityCoordinates(entityCoordinates); // Zone-group argument (ATTRIBUTE) — make it DYNAMIC so scheduling is enabled From 8a92f222154155b925d2bf6a513c64ec3b527e0e Mon Sep 17 00:00:00 2001 From: dshvaika Date: Fri, 29 Aug 2025 18:12:49 +0300 Subject: [PATCH 083/216] Avoid re-initializing CFs for entities on scheduling config changes --- .../CalculatedFieldManagerMessageProcessor.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index ee30a4b030..ff24bbb955 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -310,6 +310,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx); addLinks(cf); + scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx); initCf(cfCtx, callback, false); } } @@ -342,6 +343,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware boolean hasSchedulingConfigChanges = newCfCtx.hasSchedulingConfigChanges(oldCfCtx); if (hasSchedulingConfigChanges) { cancelCfDynamicArgumentsRefreshTaskIfExists(cfId, false); + scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(newCfCtx); } List newCfList = new CopyOnWriteArrayList<>(); @@ -365,7 +367,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware // We use copy on write lists to safely pass the reference to another actor for the iteration. // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) var stateChanges = newCfCtx.hasStateChanges(oldCfCtx); - if (stateChanges || newCfCtx.hasOtherSignificantChanges(oldCfCtx) || hasSchedulingConfigChanges) { + if (stateChanges || newCfCtx.hasOtherSignificantChanges(oldCfCtx)) { initCf(newCfCtx, callback, stateChanges); } else { callback.onSuccess(); @@ -476,7 +478,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } private void initCf(CalculatedFieldCtx cfCtx, TbCallback callback, boolean forceStateReinit) { - scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx); applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> initCfForEntity(id, cfCtx, forceStateReinit, cb)); } From ea65bd44e06af57c62dce72f671163b0a9e7146a Mon Sep 17 00:00:00 2001 From: dshvaika Date: Mon, 1 Sep 2025 10:34:51 +0300 Subject: [PATCH 084/216] fixed NPE --- .../server/service/cf/ctx/state/CalculatedFieldCtx.java | 2 +- .../data/cf/configuration/geofencing/EntityCoordinates.java | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 08ee81282f..9f6896392e 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -310,7 +310,7 @@ public class CalculatedFieldCtx { } public boolean hasOtherSignificantChanges(CalculatedFieldCtx other) { - boolean expressionChanged = !expression.equals(other.expression); + boolean expressionChanged = calculatedField.getConfiguration() instanceof ExpressionBasedCalculatedFieldConfiguration && !expression.equals(other.expression); boolean outputChanged = !output.equals(other.output); return expressionChanged || outputChanged; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinates.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinates.java index 9aed948b76..9ea5c19e8c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinates.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/EntityCoordinates.java @@ -17,12 +17,10 @@ package org.thingsboard.server.common.data.cf.configuration.geofencing; import lombok.Data; -import org.springframework.lang.Nullable; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; -import org.thingsboard.server.common.data.id.EntityId; import java.util.Map; From bdb5d55697ce8130c9d45d2b700f24765017f41e Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 1 Sep 2025 11:20:52 +0300 Subject: [PATCH 085/216] fixed user name field retrieval from edqs --- .../thingsboard/server/common/data/edqs/fields/UserFields.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/UserFields.java b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/UserFields.java index 6f4c7643c0..31e0f4118c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/UserFields.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edqs/fields/UserFields.java @@ -37,7 +37,7 @@ public class UserFields extends AbstractEntityFields { @Override public String getName() { - return super.getEmail(); + return getEmail(); } public UserFields(UUID id, long createdTime, UUID tenantId, UUID customerId, From 6c0773d9ef9cdb56232c0e89c162bc2b8ed10c49 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Mon, 1 Sep 2025 12:46:56 +0300 Subject: [PATCH 086/216] minor improvement to entity coordinates fetching logic --- .../cf/DefaultCalculatedFieldProcessingService.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index a31944a132..e4e6fce649 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -160,7 +160,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP for (var entry : entries) { switch (entry.getKey()) { case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> - argFutures.put(entry.getKey(), fetchKvEntry(ctx.getTenantId(), resolveEntityId(entityId, entry), entry.getValue())); + argFutures.put(entry.getKey(), fetchKvEntry(ctx.getTenantId(), entityId, entry.getValue())); default -> { var resolvedEntityIdsFuture = resolveGeofencingEntityIds(ctx.getTenantId(), entityId, entry); argFutures.put(entry.getKey(), Futures.transformAsync(resolvedEntityIdsFuture, resolvedEntityIds -> @@ -175,6 +175,9 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP public Map fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map arguments) { Map> argFutures = new HashMap<>(); for (var entry : arguments.entrySet()) { + if (entry.getValue().hasDynamicSource()) { + continue; + } var argEntityId = resolveEntityId(entityId, entry); var argValueFuture = fetchKvEntry(tenantId, argEntityId, entry.getValue()); argFutures.put(entry.getKey(), argValueFuture); @@ -330,7 +333,7 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP ListenableFuture>> allFutures = Futures.allAsList(kvFutures); return Futures.transform(allFutures, entries -> ArgumentEntry.createGeofencingValueArgument(entries.stream() - .collect(Collectors.toMap(Entry::getKey, Entry::getValue))), MoreExecutors.directExecutor()); + .collect(Collectors.toMap(Entry::getKey, Entry::getValue))), MoreExecutors.directExecutor()); } private ListenableFuture fetchKvEntry(TenantId tenantId, EntityId entityId, Argument argument) { From 5b14dc67fccb0c6987223371ffad49043761353d Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Mon, 1 Sep 2025 14:12:34 +0300 Subject: [PATCH 087/216] Added feature to upload dashboard JSON file to update --- .../dashboard/dashboard-form.component.html | 13 +++++++++++++ .../pages/dashboard/dashboard-form.component.ts | 14 ++++++++++++++ .../dashboards-table-config.resolver.ts | 16 +++++++++++++++- ui-ngx/src/app/shared/models/dashboard.models.ts | 1 + .../src/assets/locale/locale.constant-en_US.json | 1 + 5 files changed, 44 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html index 379e37528f..4d29376ccd 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html @@ -144,6 +144,19 @@ formControlName="image"> + +
+
dashboard.update-dashboard
+ + +
diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts index c8174013aa..a612d2b127 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts @@ -45,6 +45,7 @@ export class DashboardFormComponent extends EntityComponent { publicLink: string; assignedCustomersText: string; entityType = EntityType; + currentFileName: string = ''; constructor(protected store: Store, protected translate: TranslateService, @@ -84,6 +85,7 @@ export class DashboardFormComponent extends EntityComponent { { title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]], image: [entity ? entity.image : null], + fileContent: [null], mobileHide: [entity ? entity.mobileHide : false], mobileOrder: [entity ? entity.mobileOrder : null, [Validators.pattern(/^-?[0-9]+$/)]], configuration: this.fb.group( @@ -101,9 +103,11 @@ export class DashboardFormComponent extends EntityComponent { } updateForm(entity: Dashboard) { + this.currentFileName = ''; this.updateFields(entity); this.entityForm.patchValue({title: entity.title}); this.entityForm.patchValue({image: entity.image}); + this.entityForm.patchValue({fileContent: entity.fileContent || null}); this.entityForm.patchValue({mobileHide: entity.mobileHide}); this.entityForm.patchValue({mobileOrder: entity.mobileOrder}); this.entityForm.patchValue({configuration: {description: entity.configuration ? entity.configuration.description : ''}}); @@ -143,4 +147,14 @@ export class DashboardFormComponent extends EntityComponent { this.publicLink = this.dashboardService.getPublicDashboardLink(entity); } } + + loadDataFromJsonContent(content: string): any { + try { + const importData = JSON.parse(content); + return importData ? importData['configuration'] : importData; + } catch (err) { + this.store.dispatch(new ActionNotificationShow({message: err.message, type: 'error'})); + return null; + } + } } diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts index b37c2731c6..4540fb9af1 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboards-table-config.resolver.ts @@ -110,7 +110,7 @@ export class DashboardsTableConfigResolver { this.config.deleteEntitiesContent = () => this.translate.instant('dashboard.delete-dashboards-text'); this.config.loadEntity = id => this.dashboardService.getDashboard(id.id); - this.config.saveEntity = dashboard => this.saveAndAssignDashboard(dashboard as DashboardSetup); + this.config.saveEntity = dashboard => this.saveAndAssignDashboard(this.dashboardContentModification(dashboard) as DashboardSetup); this.config.onEntityAction = action => this.onDashboardAction(action); this.config.detailsReadonly = () => (this.config.componentsData.dashboardScope === 'customer_user' || this.config.componentsData.dashboardScope === 'edge_customer_user'); @@ -179,6 +179,20 @@ export class DashboardsTableConfigResolver { ); } + private dashboardContentModification(dashboard: Dashboard): Dashboard{ + if(dashboard.fileContent != undefined){ + const { description, ...dashboardContent } = dashboard.fileContent; + + dashboard.configuration = { + ...dashboard.configuration, + ...dashboardContent + } + } + delete dashboard.fileContent; + + return dashboard; + } + configureColumns(dashboardScope: string): Array> { const columns: Array> = [ new DateEntityTableColumn('createdTime', 'common.created-time', this.datePipe, '150px'), diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts index 8fe92f1b80..acd7e63b68 100644 --- a/ui-ngx/src/app/shared/models/dashboard.models.ts +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -195,6 +195,7 @@ export interface Dashboard extends DashboardInfo { configuration?: DashboardConfiguration; dialogRef?: MatDialogRef; resources?: Array; + fileContent?: DashboardConfiguration; } export interface HomeDashboard extends Dashboard { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index fd368e2853..962d0867a9 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1347,6 +1347,7 @@ "mobile-order": "Dashboard order in mobile application", "mobile-hide": "Hide dashboard in mobile application", "update-image": "Update dashboard image", + "update-dashboard": "Update the dashboard", "take-screenshot": "Take screenshot", "select-widget-title": "Select widget", "select-widget-value": "{{title}}: select widget", From e4588616f36b251a6c0ac17b96c7e0e0ed0775df Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Tue, 2 Sep 2025 10:34:56 +0300 Subject: [PATCH 088/216] Updated logic for file upload, changed to dialog window upload --- .../dashboard/dashboard-form.component.html | 19 +--- .../dashboard/dashboard-form.component.ts | 19 +--- .../home/pages/dashboard/dashboard.module.ts | 4 +- .../dashboards-table-config.resolver.ts | 104 +++++++++-------- ...mport-dashboard-file-dialog.component.html | 73 ++++++++++++ .../import-dashboard-file-dialog.component.ts | 106 ++++++++++++++++++ .../assets/locale/locale.constant-en_US.json | 1 + 7 files changed, 248 insertions(+), 78 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html index 4d29376ccd..2e185d66ad 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html @@ -28,6 +28,12 @@ [class.!hidden]="isEdit || dashboardScope !== 'tenant'"> {{'dashboard.export' | translate }} + + + + +
+
+ + +
+ +
+ + +
+ diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts new file mode 100644 index 0000000000..4cfa5a06ab --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts @@ -0,0 +1,106 @@ +/// +/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL +/// +/// Copyright © 2016-2025 ThingsBoard, Inc. All Rights Reserved. +/// +/// NOTICE: All information contained herein is, and remains +/// the property of ThingsBoard, Inc. and its suppliers, +/// if any. The intellectual and technical concepts contained +/// herein are proprietary to ThingsBoard, Inc. +/// and its suppliers and may be covered by U.S. and Foreign Patents, +/// patents in process, and are protected by trade secret or copyright law. +/// +/// Dissemination of this information or reproduction of this material is strictly forbidden +/// unless prior written permission is obtained from COMPANY. +/// +/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, +/// managers or contractors who have executed Confidentiality and Non-disclosure agreements +/// explicitly covering such access. +/// +/// The copyright notice above does not evidence any actual or intended publication +/// or disclosure of this source code, which includes +/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY. +/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, +/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT +/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, +/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. +/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION +/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, +/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. +/// + +import {Component, Inject, OnInit} from '@angular/core'; +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; +import {Store} from '@ngrx/store'; +import {AppState} from '@core/core.state'; +import {UntypedFormBuilder, UntypedFormGroup} from '@angular/forms'; +import {DashboardService} from '@core/http/dashboard.service'; +import {Dashboard, DashboardInfo} from '@app/shared/models/dashboard.models'; +import {ActionNotificationShow} from '@core/notification/notification.actions'; +import {TranslateService} from '@ngx-translate/core'; +import {DialogComponent} from '@shared/components/dialog.component'; +import {Router} from '@angular/router'; + +export interface DashboardInfoDialogData { + dashboard: Dashboard; +} + +@Component({ + selector: 'tb-import-dashboard-file-dialog', + templateUrl: './import-dashboard-file-dialog.component.html', + styleUrls: [] +}) +export class ImportDashboardFileDialogComponent extends DialogComponent implements OnInit { + + dashboard: Dashboard; + currentFileName: string = ''; + uploadFileFormGroup: UntypedFormGroup; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: DashboardInfoDialogData, + public translate: TranslateService, + private dashboardService: DashboardService, + public dialogRef: MatDialogRef, + public fb: UntypedFormBuilder) { + super(store, router, dialogRef); + this.dashboard = data.dashboard; + } + + ngOnInit(): void { + this.uploadFileFormGroup = this.fb.group({ + file: [null] + }); + } + + cancel(): void { + this.dialogRef.close(); + } + + save(){ + const fileControl = this.uploadFileFormGroup.get('file'); + if(!fileControl || !fileControl.value){ + return; + } + + const dashboardContent = { + ...fileControl.value, + description: this.dashboard.configuration.description + }; + this.dashboard.configuration = dashboardContent; + + this.dashboardService.saveDashboard(this.dashboard).subscribe(()=>{ + this.dialogRef.close(true); + }) + } + + loadDataFromJsonContent(content: string): any { + try { + const importData = JSON.parse(content); + return importData ? importData['configuration'] : importData; + } catch (err) { + this.store.dispatch(new ActionNotificationShow({message: err.message, type: 'error'})); + return null; + } + } +} diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 962d0867a9..d8ab1ebc0b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1348,6 +1348,7 @@ "mobile-hide": "Hide dashboard in mobile application", "update-image": "Update dashboard image", "update-dashboard": "Update the dashboard", + "upload-file-to-update": "Upload file to update", "take-screenshot": "Take screenshot", "select-widget-title": "Select widget", "select-widget-value": "{{title}}: select widget", From 6f91d8dd1cee45ca00313a0a3558070c3d4773a2 Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Tue, 2 Sep 2025 10:40:52 +0300 Subject: [PATCH 089/216] Updated license --- ...mport-dashboard-file-dialog.component.html | 36 ++++++------------- .../import-dashboard-file-dialog.component.ts | 35 ++++++------------ 2 files changed, 20 insertions(+), 51 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.html index b91b6627cc..5881d59328 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.html @@ -1,36 +1,20 @@ -

{{ 'dashboard.update-dashboard' | translate }}

diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts index 4cfa5a06ab..fda1662f46 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts @@ -1,32 +1,17 @@ /// -/// ThingsBoard, Inc. ("COMPANY") CONFIDENTIAL +/// Copyright © 2016-2025 The Thingsboard Authors /// -/// Copyright © 2016-2025 ThingsBoard, Inc. All Rights Reserved. +/// 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 /// -/// NOTICE: All information contained herein is, and remains -/// the property of ThingsBoard, Inc. and its suppliers, -/// if any. The intellectual and technical concepts contained -/// herein are proprietary to ThingsBoard, Inc. -/// and its suppliers and may be covered by U.S. and Foreign Patents, -/// patents in process, and are protected by trade secret or copyright law. +/// http://www.apache.org/licenses/LICENSE-2.0 /// -/// Dissemination of this information or reproduction of this material is strictly forbidden -/// unless prior written permission is obtained from COMPANY. -/// -/// Access to the source code contained herein is hereby forbidden to anyone except current COMPANY employees, -/// managers or contractors who have executed Confidentiality and Non-disclosure agreements -/// explicitly covering such access. -/// -/// The copyright notice above does not evidence any actual or intended publication -/// or disclosure of this source code, which includes -/// information that is confidential and/or proprietary, and is a trade secret, of COMPANY. -/// ANY REPRODUCTION, MODIFICATION, DISTRIBUTION, PUBLIC PERFORMANCE, -/// OR PUBLIC DISPLAY OF OR THROUGH USE OF THIS SOURCE CODE WITHOUT -/// THE EXPRESS WRITTEN CONSENT OF COMPANY IS STRICTLY PROHIBITED, -/// AND IN VIOLATION OF APPLICABLE LAWS AND INTERNATIONAL TREATIES. -/// THE RECEIPT OR POSSESSION OF THIS SOURCE CODE AND/OR RELATED INFORMATION -/// DOES NOT CONVEY OR IMPLY ANY RIGHTS TO REPRODUCE, DISCLOSE OR DISTRIBUTE ITS CONTENTS, -/// OR TO MANUFACTURE, USE, OR SELL ANYTHING THAT IT MAY DESCRIBE, IN WHOLE OR IN PART. +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. /// import {Component, Inject, OnInit} from '@angular/core'; From 23d10733333c48caf376b1a8d883b327af0d185d Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 2 Sep 2025 10:48:10 +0300 Subject: [PATCH 090/216] added Cross-Origin-Opener-Policy: same-origin for security reasons --- .../server/config/ThingsboardSecurityConfiguration.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index 2fbc89a84d..ca741ea8c6 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -31,6 +31,7 @@ import org.springframework.security.config.annotation.method.configuration.Enabl import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.config.annotation.web.configurers.RequestCacheConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; @@ -38,6 +39,7 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.header.writers.CrossOriginOpenerPolicyHeaderWriter.CrossOriginOpenerPolicy; import org.springframework.security.web.header.writers.StaticHeadersWriter; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; @@ -210,9 +212,8 @@ public class ThingsboardSecurityConfiguration { @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http.headers(headers -> headers - .cacheControl(config -> {}) - .frameOptions(config -> {}).disable()) + http.headers(headers -> headers.defaultsDisabled() + .crossOriginOpenerPolicy(coop -> coop.policy(CrossOriginOpenerPolicy.SAME_ORIGIN))) .cors(cors -> {}) .csrf(AbstractHttpConfigurer::disable) .exceptionHandling(config -> {}) From ba440ba7c1f07dd1f7f3f95c194abfe782025691 Mon Sep 17 00:00:00 2001 From: VIacheslavKlimov Date: Tue, 2 Sep 2025 14:28:03 +0300 Subject: [PATCH 091/216] Cleanup upgrade for 4.3.0 --- .../main/data/upgrade/basic/schema_update.sql | 31 ------ .../DefaultDatabaseSchemaSettingsService.java | 2 +- .../update/DefaultDataUpdateService.java | 59 ------------ .../update/DefaultDataUpdateServiceTest.java | 95 ------------------- 4 files changed, 1 insertion(+), 186 deletions(-) delete mode 100644 application/src/test/java/org/thingsboard/server/service/install/update/DefaultDataUpdateServiceTest.java diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index add832ea6e..d17aba4267 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -13,34 +13,3 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -- - --- UPDATE OTA PACKAGE EXTERNAL ID START - -ALTER TABLE ota_package - ADD COLUMN IF NOT EXISTS external_id uuid; - -DO -$$ - BEGIN - IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'ota_package_external_id_unq_key') THEN - ALTER TABLE ota_package ADD CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id); - END IF; - END; -$$; - --- UPDATE OTA PACKAGE EXTERNAL ID END - --- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT START - -DROP INDEX IF EXISTS idx_device_external_id; -DROP INDEX IF EXISTS idx_device_profile_external_id; -DROP INDEX IF EXISTS idx_asset_external_id; -DROP INDEX IF EXISTS idx_entity_view_external_id; -DROP INDEX IF EXISTS idx_rule_chain_external_id; -DROP INDEX IF EXISTS idx_dashboard_external_id; -DROP INDEX IF EXISTS idx_customer_external_id; -DROP INDEX IF EXISTS idx_widgets_bundle_external_id; - --- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT END - -ALTER TABLE mobile_app ADD COLUMN IF NOT EXISTS title varchar(255); \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java index e5bd026fb7..f41a530630 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java @@ -32,7 +32,7 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti // This list should include all versions which are compatible for the upgrade. // The compatibility cycle usually breaks when we have some scripts written in Java that may not work after new release. - private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.1.0"); + private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.2.0"); private final ProjectInfo projectInfo; private final JdbcTemplate jdbcTemplate; diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java index 972d5ff36c..7ef691dc6d 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java @@ -15,8 +15,6 @@ */ package org.thingsboard.server.service.install.update; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -24,12 +22,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; -import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageDataIterable; -import org.thingsboard.server.common.data.query.DynamicValue; -import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.RuleNodeClassInfo; @@ -129,60 +124,6 @@ public class DefaultDataUpdateService implements DataUpdateService { return ruleNodeIds; } - boolean convertDeviceProfileForVersion330(JsonNode profileData) { - boolean isUpdated = false; - if (profileData.has("alarms") && !profileData.get("alarms").isNull()) { - JsonNode alarms = profileData.get("alarms"); - for (JsonNode alarm : alarms) { - if (alarm.has("createRules")) { - JsonNode createRules = alarm.get("createRules"); - for (AlarmSeverity severity : AlarmSeverity.values()) { - if (createRules.has(severity.name())) { - JsonNode spec = createRules.get(severity.name()).get("condition").get("spec"); - if (convertDeviceProfileAlarmRulesForVersion330(spec)) { - isUpdated = true; - } - } - } - } - if (alarm.has("clearRule") && !alarm.get("clearRule").isNull()) { - JsonNode spec = alarm.get("clearRule").get("condition").get("spec"); - if (convertDeviceProfileAlarmRulesForVersion330(spec)) { - isUpdated = true; - } - } - } - } - return isUpdated; - } - - boolean convertDeviceProfileAlarmRulesForVersion330(JsonNode spec) { - if (spec != null) { - if (spec.has("type") && spec.get("type").asText().equals("DURATION")) { - if (spec.has("value")) { - long value = spec.get("value").asLong(); - var predicate = new FilterPredicateValue<>( - value, null, new DynamicValue<>(null, null, false) - ); - ((ObjectNode) spec).remove("value"); - ((ObjectNode) spec).putPOJO("predicate", predicate); - return true; - } - } else if (spec.has("type") && spec.get("type").asText().equals("REPEATING")) { - if (spec.has("count")) { - int count = spec.get("count").asInt(); - var predicate = new FilterPredicateValue<>( - count, null, new DynamicValue<>(null, null, false) - ); - ((ObjectNode) spec).remove("count"); - ((ObjectNode) spec).putPOJO("predicate", predicate); - return true; - } - } - } - return false; - } - public static boolean getEnv(String name, boolean defaultValue) { String env = System.getenv(name); if (env == null) { diff --git a/application/src/test/java/org/thingsboard/server/service/install/update/DefaultDataUpdateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/install/update/DefaultDataUpdateServiceTest.java deleted file mode 100644 index abfab84491..0000000000 --- a/application/src/test/java/org/thingsboard/server/service/install/update/DefaultDataUpdateServiceTest.java +++ /dev/null @@ -1,95 +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.service.install.update; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.test.context.ActiveProfiles; -import org.thingsboard.common.util.JacksonUtil; - -import java.io.IOException; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.willCallRealMethod; - -@ActiveProfiles("install") -@SpringBootTest(classes = DefaultDataUpdateService.class) -class DefaultDataUpdateServiceTest { - - @MockBean - DefaultDataUpdateService service; - - @BeforeEach - void setUp() { - willCallRealMethod().given(service).convertDeviceProfileAlarmRulesForVersion330(any()); - willCallRealMethod().given(service).convertDeviceProfileForVersion330(any()); - } - - JsonNode readFromResource(String resourceName) throws IOException { - return JacksonUtil.OBJECT_MAPPER.readTree(this.getClass().getClassLoader().getResourceAsStream(resourceName)); - } - - @Test - void convertDeviceProfileAlarmRulesForVersion330FirstRun() throws IOException { - JsonNode spec = readFromResource("update/330/device_profile_001_in.json"); - JsonNode expected = readFromResource("update/330/device_profile_001_out.json"); - - assertThat(service.convertDeviceProfileForVersion330(spec.get("profileData"))).isTrue(); - assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString()); // use IDE feature - } - - @Test - void convertDeviceProfileAlarmRulesForVersion330SecondRun() throws IOException { - JsonNode spec = readFromResource("update/330/device_profile_001_out.json"); - JsonNode expected = readFromResource("update/330/device_profile_001_out.json"); - - assertThat(service.convertDeviceProfileForVersion330(spec.get("profileData"))).isFalse(); - assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString()); // use IDE feature - } - - @Test - void convertDeviceProfileAlarmRulesForVersion330EmptyJson() throws JsonProcessingException { - JsonNode spec = JacksonUtil.toJsonNode("{ }"); - JsonNode expected = JacksonUtil.toJsonNode("{ }"); - - assertThat(service.convertDeviceProfileForVersion330(spec)).isFalse(); - assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString()); - } - - @Test - void convertDeviceProfileAlarmRulesForVersion330AlarmNodeNull() throws JsonProcessingException { - JsonNode spec = JacksonUtil.toJsonNode("{ \"alarms\" : null }"); - JsonNode expected = JacksonUtil.toJsonNode("{ \"alarms\" : null }"); - - assertThat(service.convertDeviceProfileForVersion330(spec)).isFalse(); - assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString()); - } - - @Test - void convertDeviceProfileAlarmRulesForVersion330NoAlarmNode() throws JsonProcessingException { - JsonNode spec = JacksonUtil.toJsonNode("{ \"configuration\": { \"type\": \"DEFAULT\" } }"); - JsonNode expected = JacksonUtil.toJsonNode("{ \"configuration\": { \"type\": \"DEFAULT\" } }"); - - assertThat(service.convertDeviceProfileForVersion330(spec)).isFalse(); - assertThat(spec.toPrettyString()).isEqualTo(expected.toPrettyString()); - } - -} From a7f687443fe3a5760e32d86585e4cc7bc77fc3fb Mon Sep 17 00:00:00 2001 From: VIacheslavKlimov Date: Tue, 2 Sep 2025 14:36:05 +0300 Subject: [PATCH 092/216] Fix license header --- application/src/main/data/upgrade/basic/schema_update.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index d17aba4267..016e786776 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -13,3 +13,4 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -- + From 2fa1d234b3048e923d274ffd4b85d13fd099b3d7 Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Tue, 2 Sep 2025 16:58:55 +0300 Subject: [PATCH 093/216] Refactored formatting --- .../dashboard/dashboard-form.component.ts | 4 +- .../dashboards-table-config.resolver.ts | 50 +++++++++---------- .../import-dashboard-file-dialog.component.ts | 36 +++++++------ .../src/app/shared/models/dashboard.models.ts | 1 - 4 files changed, 44 insertions(+), 47 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts index eca7b61dcc..844e0822a1 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.ts @@ -31,7 +31,7 @@ import { DashboardService } from '@core/http/dashboard.service'; import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { isEqual } from '@core/utils'; import { EntityType } from '@shared/models/entity-type.models'; -import {PageLink} from "@shared/models/page/page-link"; +import { PageLink } from "@shared/models/page/page-link"; @Component({ selector: 'tb-dashboard-form', @@ -118,7 +118,7 @@ export class DashboardFormComponent extends EntityComponent implements OnInit { - dashboard: Dashboard; + private dashboard: Dashboard; currentFileName: string = ''; - uploadFileFormGroup: UntypedFormGroup; + uploadFileFormGroup: FormGroup; constructor(protected store: Store, protected router: Router, @Inject(MAT_DIALOG_DATA) public data: DashboardInfoDialogData, - public translate: TranslateService, private dashboardService: DashboardService, - public dialogRef: MatDialogRef, - public fb: UntypedFormBuilder) { + protected dialogRef: MatDialogRef, + public fb: FormBuilder) { super(store, router, dialogRef); this.dashboard = data.dashboard; } @@ -62,9 +60,9 @@ export class ImportDashboardFileDialogComponent extends DialogComponent{ + this.dashboardService.saveDashboard(this.dashboard).subscribe(() => { this.dialogRef.close(true); }) } diff --git a/ui-ngx/src/app/shared/models/dashboard.models.ts b/ui-ngx/src/app/shared/models/dashboard.models.ts index acd7e63b68..8fe92f1b80 100644 --- a/ui-ngx/src/app/shared/models/dashboard.models.ts +++ b/ui-ngx/src/app/shared/models/dashboard.models.ts @@ -195,7 +195,6 @@ export interface Dashboard extends DashboardInfo { configuration?: DashboardConfiguration; dialogRef?: MatDialogRef; resources?: Array; - fileContent?: DashboardConfiguration; } export interface HomeDashboard extends Dashboard { From 1754f29df79879dc3bcc1de6c573c202dac7f17e Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Tue, 2 Sep 2025 17:01:10 +0300 Subject: [PATCH 094/216] Refactored formatting --- .../home/pages/dashboard/dashboard.module.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts index 99140bf7b7..5604d7e4b3 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard.module.ts @@ -19,12 +19,16 @@ import { CommonModule } from '@angular/common'; import { SharedModule } from '@shared/shared.module'; import { HomeDialogsModule } from '../../dialogs/home-dialogs.module'; import { DashboardFormComponent } from '@modules/home/pages/dashboard/dashboard-form.component'; -import { ManageDashboardCustomersDialogComponent } from '@modules/home/pages/dashboard/manage-dashboard-customers-dialog.component'; +import { + ManageDashboardCustomersDialogComponent +} from '@modules/home/pages/dashboard/manage-dashboard-customers-dialog.component'; import { DashboardRoutingModule } from './dashboard-routing.module'; -import { MakeDashboardPublicDialogComponent } from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component'; +import { + MakeDashboardPublicDialogComponent +} from '@modules/home/pages/dashboard/make-dashboard-public-dialog.component'; import { HomeComponentsModule } from '@modules/home/components/home-components.module'; import { DashboardTabsComponent } from '@home/pages/dashboard/dashboard-tabs.component'; -import {ImportDashboardFileDialogComponent} from "@home/pages/dashboard/import-dashboard-file-dialog.component"; +import { ImportDashboardFileDialogComponent } from "@home/pages/dashboard/import-dashboard-file-dialog.component"; @NgModule({ declarations: [ @@ -42,4 +46,5 @@ import {ImportDashboardFileDialogComponent} from "@home/pages/dashboard/import-d DashboardRoutingModule ] }) -export class DashboardModule { } +export class DashboardModule { +} From 50794bdbc88a5ce3a0d6129eb102c4bc89891e8f Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Tue, 2 Sep 2025 17:05:05 +0300 Subject: [PATCH 095/216] Refactored formatting --- .../pages/dashboard/import-dashboard-file-dialog.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts index 26238655a0..01fca78bf9 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/pages/dashboard/import-dashboard-file-dialog.component.ts @@ -45,7 +45,7 @@ export class ImportDashboardFileDialogComponent extends DialogComponent, - public fb: FormBuilder) { + private fb: FormBuilder) { super(store, router, dialogRef); this.dashboard = data.dashboard; } From 159d779d77fd6905ba83429ac33abfd641c4b0fb Mon Sep 17 00:00:00 2001 From: dshvaika Date: Tue, 2 Sep 2025 17:42:25 +0300 Subject: [PATCH 096/216] rollback logback.xml --- application/src/main/resources/logback.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml index 28a8b9fcdc..8e1a49faef 100644 --- a/application/src/main/resources/logback.xml +++ b/application/src/main/resources/logback.xml @@ -57,7 +57,7 @@ - + From b3a139d1771070ba11fc9341faec04b07cb589f6 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 3 Sep 2025 15:47:16 +0300 Subject: [PATCH 097/216] UI: move widget to home widget bundle --- .../json/system/widget_bundles/cards.json | 3 +- .../widget_bundles/home_page_widgets.json | 3 +- .../json/system/widget_types/api_usage.json | 14 +- .../lib/cards/api-usage-widget.component.scss | 1 + .../lib/cards/api-usage-widget.component.ts | 3 +- .../api-usage-data-key-row.component.html | 95 ++++---- .../api-usage-data-key-row.component.scss | 28 ++- .../cards/api-usage-data-key-row.component.ts | 5 + .../api-usage-widget-settings.component.html | 13 +- .../api-usage-widget-settings.component.ts | 30 ++- .../home/models/widget-component.models.ts | 5 + ui-ngx/src/assets/dashboard/api_usage.json | 203 ++++++++++-------- .../assets/locale/locale.constant-en_US.json | 3 + 13 files changed, 234 insertions(+), 172 deletions(-) diff --git a/application/src/main/data/json/system/widget_bundles/cards.json b/application/src/main/data/json/system/widget_bundles/cards.json index 9c735ee558..81cb2fdbb1 100644 --- a/application/src/main/data/json/system/widget_bundles/cards.json +++ b/application/src/main/data/json/system/widget_bundles/cards.json @@ -24,7 +24,6 @@ "cards.html_value_card", "cards.markdown_card", "cards.simple_card", - "unread_notifications", - "api_usage" + "unread_notifications" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json index a30d6ee76f..2701abcb84 100644 --- a/application/src/main/data/json/system/widget_bundles/home_page_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/home_page_widgets.json @@ -13,6 +13,7 @@ "home_page_widgets.quick_links", "home_page_widgets.documentation_links", "home_page_widgets.dashboards", - "home_page_widgets.usage_info" + "home_page_widgets.usage_info", + "api_usage" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/api_usage.json b/application/src/main/data/json/system/widget_types/api_usage.json index e07cbef787..9d6f2bc464 100644 --- a/application/src/main/data/json/system/widget_types/api_usage.json +++ b/application/src/main/data/json/system/widget_types/api_usage.json @@ -2,7 +2,7 @@ "fqn": "api_usage", "name": "API Usage", "deprecated": false, - "image": "tb-image;/api/images/system/api-usage-widget.svg", + "image": "tb-image;/api/images/system/api-usage-widget.png", "description": null, "descriptor": { "type": "latest", @@ -15,18 +15,18 @@ "settingsForm": [], "dataKeySettingsForm": [], "settingsDirective": "tb-api-usage-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0\",\"settings\":{},\"title\":\"API usage\",\"decimals\":null,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\".tb-widget-header {\\n height: 48px;\\n align-items: center !important;\\n padding: 5px 10px 0 10px;\\n}\",\"titleStyle\":{},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"actions\":{\"headerButton\":[{\"name\":\"Go back\",\"buttonType\":\"stroked\",\"showIcon\":true,\"icon\":\"undo\",\"buttonColor\":\"#305680\",\"buttonBorderColor\":\"#0000001F\",\"customButtonStyle\":{\"padding\":\"0 16px\"},\"useShowWidgetActionFunction\":true,\"showWidgetActionFunction\":\"console.log(widgetContext.stateController.getStateId(), widgetContext.settings.targetDashboardState)\\nreturn widgetContext.stateController.getStateId() !== widgetContext.settings.targetDashboardState && widgetContext.settings.targetDashboardState;\",\"type\":\"custom\",\"customFunction\":\"const state = widgetContext.settings.targetDashboardState?.length ? widgetContext.settings.targetDashboardState : 'default';\\nwidgetContext.stateController.updateState(state, widgetContext.stateController.getStateParams(), false);\",\"openInSeparateDialog\":false,\"openInPopover\":false,\"id\":\"1ea1cca6-47d1-3539-d051-9535129fb12b\"}]},\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":null,\"weight\":\"500\",\"style\":null,\"lineHeight\":\"21px\"},\"borderRadius\":\"4px\"}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0\",\"settings\":{},\"title\":\"API usage\",\"decimals\":null,\"showTitleIcon\":false,\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":false,\"widgetStyle\":{},\"widgetCss\":\".tb-widget-header {\\n height: 48px;\\n align-items: center !important;\\n padding: 5px 10px 0 10px;\\n}\",\"titleStyle\":{},\"pageSize\":1024,\"noDataDisplayMessage\":\"\",\"actions\":{\"headerButton\":[{\"name\":\"Go back\",\"buttonType\":\"stroked\",\"showIcon\":true,\"icon\":\"undo\",\"buttonColor\":\"#305680\",\"buttonBorderColor\":\"#0000001F\",\"customButtonStyle\":{\"padding\":\"0 16px\"},\"useShowWidgetActionFunction\":true,\"showWidgetActionFunction\":\"return widgetContext.stateController.getStateId() !== widgetContext.settings.targetDashboardState && widgetContext.settings.targetDashboardState;\",\"type\":\"custom\",\"customFunction\":\"const state = widgetContext.settings.targetDashboardState?.length ? widgetContext.settings.targetDashboardState : 'default';\\nwidgetContext.stateController.updateState(state, widgetContext.stateController.getStateParams(), false);\",\"openInSeparateDialog\":false,\"openInPopover\":false,\"id\":\"1ea1cca6-47d1-3539-d051-9535129fb12b\"}]},\"titleFont\":{\"size\":16,\"sizeUnit\":\"px\",\"family\":null,\"weight\":\"500\",\"style\":null,\"lineHeight\":\"21px\"},\"borderRadius\":\"4px\"}" }, "resources": [ { - "link": "/api/images/system/api-usage-widget.svg", + "link": "/api/images/system/api-usage-widget.png", "title": "\"API Usage\" system widget image", "type": "IMAGE", "subType": "IMAGE", - "fileName": "api-usage-widget.svg", - "publicResourceKey": "esDzBtlpFrojaJq7b7BVzilQ1NtPfa0t", - "mediaType": "image/svg+xml", - "data": "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMTYwIiBmaWxsPSJub25lIj48ZyBjbGlwLXBhdGg9InVybCgjYSkiIGZpbHRlcj0idXJsKCNiKSI+PGcgY2xpcC1wYXRoPSJ1cmwoI2MpIj48cmVjdCB3aWR0aD0iMjAwIiBoZWlnaHQ9IjE2MCIgZmlsbD0iI2ZmZiIgcng9IjMuOTE4Ii8+PHBhdGggZmlsbD0iIzMwNTY4MCIgZmlsbC1vcGFjaXR5PSIuMDYiIGQ9Ik0wIDBoMjAwdjM4LjY1SDB6Ii8+PGcgY2xpcC1wYXRoPSJ1cmwoI2QpIj48cGF0aCBmaWxsPSIjMzA1NjgwIiBkPSJNMTIuMzAxIDE2LjUyNHY1LjgwMWgtLjk5MnYtNS44aC45OTJabTEuODIxIDB2Ljc5N0g5LjUwNHYtLjc5N2g0LjYxOFptMS40OTIgMi4zMTF2My40OWgtLjk2di00LjMxaC45MTdsLjA0My44MlptMS4zMi0uODQ5LS4wMDkuODkzYTIuNTAzIDIuNTAzIDAgMCAwLS4zOS0uMDMyYy0uMTY1IDAtLjMxLjAyNC0uNDM0LjA3MmEuODE4LjgxOCAwIDAgMC0uNTA2LjUxIDEuMzkzIDEuMzkzIDAgMCAwLS4wOC40MWwtLjIyLjAxNmMwLS4yNy4wMjctLjUyMi4wOC0uNzUzLjA1My0uMjMxLjEzMy0uNDM0LjI0LS42MS4xMDgtLjE3NS4yNDQtLjMxMi40MDYtLjQxYTEuMDkgMS4wOSAwIDAgMSAuNTctLjE0NyAxLjE5IDEuMTkgMCAwIDEgLjM0Mi4wNTJabTMuMDMzIDMuNDc1di0yLjA1NmEuODgyLjg4MiAwIDAgMC0uMDgzLS4zOTkuNTg2LjU4NiAwIDAgMC0uMjU1LS4yNTkuODczLjg3MyAwIDAgMC0uNDIzLS4wOTEuOTU3Ljk1NyAwIDAgMC0uNDA2LjA4LjY1Ni42NTYgMCAwIDAtLjI2Ny4yMTUuNTIuNTIgMCAwIDAtLjA5Ni4zMDZoLS45NTZjMC0uMTcuMDQxLS4zMzQuMTI0LS40OTRhMS4zMiAxLjMyIDAgMCAxIC4zNTgtLjQyNiAxLjc5IDEuNzkgMCAwIDEgLjU2Mi0uMjk1Yy4yMTgtLjA3Mi40NjItLjEwNy43MzMtLjEwNy4zMjQgMCAuNjExLjA1NC44Ni4xNjMuMjUzLjEwOS40NTEuMjc0LjU5NC40OTQuMTQ2LjIxOC4yMi40OTEuMjIuODJ2MS45MTdjMCAuMTk3LjAxMy4zNzMuMDQuNTMuMDI5LjE1NC4wNy4yODguMTIzLjQwMnYuMDY0aC0uOTg0YTEuNzA2IDEuNzA2IDAgMCAxLS4xMDgtLjM5NCAzLjIyMyAzLjIyMyAwIDAgMS0uMDM2LS40N1ptLjE0LTEuNzU3LjAwOC41OTNoLS42OWExLjkxIDEuOTEgMCAwIDAtLjQ3LjA1Mi45NjMuOTYzIDAgMCAwLS4zMzguMTQzLjYyMi42MjIgMCAwIDAtLjI3MS41MzhjMCAuMTE1LjAyNi4yMi4wOC4zMTUuMDUzLjA5My4xMy4xNjYuMjMuMjIuMTA0LjA1Mi4yMjkuMDc5LjM3NS4wNzlhMS4wNTcgMS4wNTcgMCAwIDAgLjg2NS0uNDE4LjY1LjY1IDAgMCAwIC4xMzUtLjM0bC4zMS40MjdhMS40NTYgMS40NTYgMCAwIDEtLjE2My4zNSAxLjY5NiAxLjY5NiAwIDAgMS0uMzAyLjM2IDEuNTAzIDEuNTAzIDAgMCAxLTEuMDMyLjM4MmMtLjI4MiAwLS41MzMtLjA1Ni0uNzUzLS4xNjhhMS4zNCAxLjM0IDAgMCAxLS41MTgtLjQ1OCAxLjE4OSAxLjE4OSAwIDAgMS0uMTg3LS42NTdjMC0uMjI4LjA0Mi0uNDMuMTI3LS42MDYuMDg4LS4xNzguMjE1LS4zMjYuMzgzLS40NDYuMTctLjEyLjM3Ny0uMjEuNjIxLS4yNy4yNDUtLjA2NS41MjMtLjA5Ni44MzctLjA5NmguNzUzWm0yLjkyNy0uNzd2My4zOTFoLS45NnYtNC4zMWguOTA0bC4wNTUuOTJabS0uMTcyIDEuMDc2LS4zMS0uMDA0YTIuOCAyLjggMCAwIDEgLjEyNy0uODQgMi4wNyAyLjA3IDAgMCAxIC4zNS0uNjU4Yy4xNTItLjE4My4zMzItLjMyNC41NDItLjQyMi4yMS0uMS40NDQtLjE1MS43MDEtLjE1MS4yMDggMCAuMzk1LjAyOS41NjIuMDg3LjE3LjA1Ni4zMTUuMTQ4LjQzNS4yNzUuMTIyLjEyOC4yMTUuMjk0LjI3OS40OTguMDYzLjIwMi4wOTUuNDUuMDk1Ljc0NXYyLjc4NWgtLjk2NHYtMi43ODljMC0uMjA3LS4wMy0uMzctLjA5Mi0uNDlhLjUxMy41MTMgMCAwIDAtLjI1OS0uMjU5Ljk3MS45NzEgMCAwIDAtLjQxOC0uMDguOTI5LjkyOSAwIDAgMC0uNzczLjM4N2MtLjA4OC4xMi0uMTU1LjI1OC0uMjAzLjQxNGExLjcxMiAxLjcxMiAwIDAgMC0uMDcyLjUwMlptNi4zMjUgMS4xNDhhLjQ4LjQ4IDAgMCAwLS4wNzItLjI2Yy0uMDQ3LS4wNzktLjEzOS0uMTUtLjI3NC0uMjE0YTIuNjcgMi42NyAwIDAgMC0uNTktLjE3NiA1LjA2OCA1LjA2OCAwIDAgMS0uNjMtLjE3OSAxLjk5OCAxLjk5OCAwIDAgMS0uNDg2LS4yNTkuOTkzLjk5MyAwIDAgMS0uNDI2LS44MzdjMC0uMTc1LjAzOS0uMzQuMTE2LS40OTguMDc3LS4xNTYuMTg3LS4yOTQuMzMtLjQxNGExLjYxIDEuNjEgMCAwIDEgLjUyMi0uMjgzYy4yMDctLjA2OS40MzktLjEwMy42OTQtLjEwMy4zNiAwIC42Ny4wNi45MjguMTgzLjI2LjEyLjQ2LjI4My41OTcuNDkuMTM5LjIwNC4yMDguNDM2LjIwOC42OTNoLS45NmMwLS4xMTQtLjAzLS4yMi0uMDg4LS4zMThhLjYxLjYxIDAgMCAwLS4yNTUtLjI0NC44NzQuODc0IDAgMCAwLS40My0uMDk1LjkzNC45MzQgMCAwIDAtLjQxLjA4LjU2Mi41NjIgMCAwIDAtLjI0LjE5OS41MDkuNTA5IDAgMCAwLS4wMzYuNDY2LjQ2LjQ2IDAgMCAwIC4xNDQuMTU1Yy4wNjYuMDQ1LjE1Ni4wODguMjcuMTI4LjExNy4wNC4yNjQuMDc4LjQzOS4xMTUuMzMuMDcuNjEyLjE1OC44NDkuMjY3LjIzOC4xMDcuNDIyLjI0NS41NS40MTUuMTI3LjE2Ny4xOS4zOC4xOS42MzcgMCAuMTkxLS4wNC4zNjctLjEyMy41MjYtLjA4LjE1Ny0uMTk3LjI5My0uMzUuNDFhMS43NjMgMS43NjMgMCAwIDEtLjU1NC4yNjcgMi40OTUgMi40OTUgMCAwIDEtLjcxOC4wOTZjLS4zOSAwLS43Mi0uMDctLjk5Mi0uMjA3LS4yNy0uMTQxLS40NzYtLjMyLS42MTctLjUzOGExLjI3MiAxLjI3MiAwIDAgMS0uMjA3LS42ODVoLjkyOGMuMDEuMTc3LjA2LjMyLjE0Ny40MjYuMDkuMTA0LjIwMi4xOC4zMzUuMjI3LjEzNi4wNDUuMjc1LjA2OC40MTguMDY4LjE3MyAwIC4zMTgtLjAyMy40MzUtLjA2OGEuNjI0LjYyNCAwIDAgMCAuMjY3LS4xOTEuNDU2LjQ1NiAwIDAgMCAuMDkxLS4yOFptMi44OTEtMi4zMTV2NS4xNGgtLjk2di01Ljk2OWguODg0bC4wNzYuODNabTIuODA5IDEuMjg3di4wODRjMCAuMzEzLS4wMzcuNjA0LS4xMTIuODcyLS4wNzEuMjY2LS4xNzkuNDk4LS4zMjIuNjk3LS4xNDEuMTk3LS4zMTUuMzUtLjUyMi40NThhMS41MTkgMS41MTkgMCAwIDEtLjcxNy4xNjRjLS4yNjkgMC0uNTA0LS4wNS0uNzA2LS4xNDhhMS40NDYgMS40NDYgMCAwIDEtLjUwNi0uNDI2IDIuMzE2IDIuMzE2IDAgMCAxLS4zMzQtLjY0NSA0LjEzNiA0LjEzNiAwIDAgMS0uMTc2LS44MjF2LS4zMjNjLjAzNS0uMzE2LjA5My0uNjAzLjE3Ni0uODYuMDg1LS4yNTguMTk2LS40OC4zMzQtLjY2Ni4xMzgtLjE4Ni4zMDctLjMyOS41MDYtLjQzLjItLjEuNDMyLS4xNTEuNjk3LS4xNTEuMjcyIDAgLjUxMi4wNTMuNzIyLjE1OS4yMS4xMDQuMzg2LjI1Mi41My40NDYuMTQzLjE5Mi4yNS40MjMuMzIyLjY5NC4wNzIuMjY4LjEwOC41NjcuMTA4Ljg5NlptLS45Ni4wODR2LS4wODRjMC0uMi0uMDE5LS4zODQtLjA1Ni0uNTU0YTEuNDQ1IDEuNDQ1IDAgMCAwLS4xNzUtLjQ1NC44NTguODU4IDAgMCAwLS4zMDctLjMwMy44MzUuODM1IDAgMCAwLS40NDItLjExMWMtLjE3IDAtLjMxNy4wMjktLjQzOS4wODdhLjg0Ljg0IDAgMCAwLS4zMDYuMjM1Yy0uMDgzLjEwMS0uMTQ3LjIyLS4xOTIuMzU1YTIuMTIyIDIuMTIyIDAgMCAwLS4wOTUuNDM0di43NzNjLjAzMi4xOTEuMDg2LjM2Ny4xNjMuNTI2LjA3Ny4xNi4xODYuMjg3LjMyNy4zODMuMTQzLjA5Mi4zMjYuMTM5LjU1LjEzOS4xNzIgMCAuMzItLjAzNy40NDItLjExMmEuODcxLjg3MSAwIDAgMCAuMjk5LS4zMDZjLjA4LS4xMzMuMTM4LS4yODYuMTc1LS40NTkuMDM3LS4xNzIuMDU2LS4zNTYuMDU2LS41NVptMS43MzkuMDA0di0uMDkyYzAtLjMxLjA0NS0uNTk5LjEzNi0uODY1LjA5LS4yNjguMjItLjUuMzktLjY5Ny4xNzMtLjE5OS4zODItLjM1My42My0uNDYyYTIuMDUgMi4wNSAwIDAgMSAuODQ0LS4xNjdjLjMxNiAwIC41OTguMDU2Ljg0NS4xNjcuMjUuMTA5LjQ2LjI2My42MzMuNDYyLjE3My4xOTcuMzA0LjQzLjM5NS42OTcuMDkuMjY2LjEzNS41NTQuMTM1Ljg2NXYuMDkyYzAgLjMxLS4wNDUuNTk5LS4xMzUuODY0LS4wOS4yNjYtLjIyMi40OTgtLjM5NS42OTctLjE3Mi4xOTctLjM4Mi4zNTEtLjYzLjQ2MmEyLjA2NCAyLjA2NCAwIDAgMS0uODQuMTY0Yy0uMzE2IDAtLjU5OS0uMDU1LS44NDktLjE2NGExLjgyOCAxLjgyOCAwIDAgMS0uNjMtLjQ2MiAyLjA2OSAyLjA2OSAwIDAgMS0uMzk0LS42OTcgMi42NyAyLjY3IDAgMCAxLS4xMzUtLjg2NFptLjk2LS4wOTJ2LjA5MmMwIC4xOTQuMDIuMzc3LjA2LjU1LjA0LjE3Mi4xMDIuMzIzLjE4Ny40NTQuMDg1LjEzLjE5NC4yMzIuMzI3LjMwNi4xMzMuMDc1LjI5LjExMi40NzQuMTEyYS45MTcuOTE3IDAgMCAwIC40NjItLjExMi45MjcuOTI3IDAgMCAwIC4zMjctLjMwNmMuMDg1LS4xMy4xNDctLjI4Mi4xODctLjQ1NS4wNDMtLjE3Mi4wNjQtLjM1NS4wNjQtLjU1di0uMDkxYzAtLjE5MS0uMDIxLS4zNzItLjA2NC0uNTQyYTEuMzkgMS4zOSAwIDAgMC0uMTkxLS40NTguOTEzLjkxMyAwIDAgMC0uNzkzLS40MjYuOTIuOTIgMCAwIDAtLjQ3LjExNS45MjUuOTI1IDAgMCAwLS4zMjMuMzEgMS40NDUgMS40NDUgMCAwIDAtLjE4Ny40NmMtLjA0LjE3LS4wNi4zNS0uMDYuNTQxWm00Ljk2My0xLjI5djMuNDloLS45NnYtNC4zMTJoLjkxNmwuMDQ0LjgyMVptMS4zMTktLjg1LS4wMDguODkzYTIuNTAzIDIuNTAzIDAgMCAwLS4zOS0uMDMyYy0uMTY2IDAtLjMxLjAyNC0uNDM1LjA3MmEuODE4LjgxOCAwIDAgMC0uNTA2LjUxIDEuMzkzIDEuMzkzIDAgMCAwLS4wOC40MWwtLjIxOS4wMTZjMC0uMjcuMDI3LS41MjIuMDgtLjc1My4wNTMtLjIzMS4xMzMtLjQzNC4yMzktLjYxLjEwOS0uMTc1LjI0NC0uMzEyLjQwNi0uNDFhMS4wOSAxLjA5IDAgMCAxIC41Ny0uMTQ3IDEuMTkgMS4xOSAwIDAgMSAuMzQzLjA1MlptMi45MjIuMDI4di43MDJINDMuNHYtLjcwMmgyLjQzWm0tMS43MjktMS4wNTVoLjk2djQuMTc1YS42OC42OCAwIDAgMCAuMDU2LjMwN2MuMDQuMDY5LjA5NC4xMTUuMTYzLjE0LjA3LjAyMy4xNS4wMzUuMjQzLjAzNWExLjQ5MiAxLjQ5MiAwIDAgMCAuMzM5LS4wMzZsLjAwNC43MzNjLS4wOC4wMjQtLjE3My4wNDUtLjI3OS4wNjRhMi4wNDcgMi4wNDcgMCAwIDEtLjM1OS4wMjhjLS4yMiAwLS40MTUtLjAzOS0uNTg1LS4xMTZhLjg2Mi44NjIgMCAwIDEtLjM5OS0uMzg2Yy0uMDk1LS4xNzgtLjE0My0uNDE1LS4xNDMtLjcxVjE2Ljk2Wm01Ljc1NCAxLjkzMnYzLjQzNGgtLjk2di00LjMxaC45MDRsLjA1Ni44NzZabS0uMTU2IDEuMTItLjMyNi0uMDA1YzAtLjI5Ny4wMzctLjU3Mi4xMTEtLjgyNC4wNzUtLjI1My4xODMtLjQ3Mi4zMjctLjY1OC4xNDMtLjE4OC4zMjEtLjMzMy41MzQtLjQzNC4yMTUtLjEwNC40NjMtLjE1NS43NDUtLjE1NS4xOTYgMCAuMzc2LjAyOS41MzguMDg3LjE2NC4wNTYuMzA2LjE0NS40MjYuMjY3LjEyMi4xMjIuMjE1LjI4LjI3OS40Ny4wNjYuMTkyLjEuNDIzLjEuNjk0djIuODcyaC0uOTZ2LTIuNzg5YzAtLjIxLS4wMzMtLjM3NC0uMDk2LS40OTRhLjUzLjUzIDAgMCAwLS4yNjctLjI1NS45NjcuOTY3IDAgMCAwLS40MS0uMDguOTU4Ljk1OCAwIDAgMC0uNDYzLjEwNC44NjkuODY5IDAgMCAwLS4zMDcuMjgzYy0uMDguMTItLjEzOC4yNTgtLjE3NS40MTRhMi4xNyAyLjE3IDAgMCAwLS4wNTYuNTAyWm0yLjY3NC0uMjU2LS40NS4xYzAtLjI2LjAzNS0uNTA2LjEwNy0uNzM3LjA3NC0uMjM0LjE4Mi0uNDM4LjMyMy0uNjE0LjE0My0uMTc4LjMyLS4zMTcuNTMtLjQxOC4yMS0uMS40NS0uMTUxLjcyLS4xNTEuMjIxIDAgLjQxOC4wMy41OS4wOTEuMTc2LjA1OS4zMjQuMTUyLjQ0Ny4yOC4xMjIuMTI3LjIxNS4yOTMuMjc5LjQ5Ny4wNjMuMjAyLjA5NS40NDYuMDk1LjczM3YyLjc5aC0uOTY0di0yLjc5NGMwLS4yMTgtLjAzMi0uMzg2LS4wOTYtLjUwNmEuNDk2LjQ5NiAwIDAgMC0uMjYzLS4yNDcgMS4wNiAxLjA2IDAgMCAwLS40MS0uMDcxLjg4OC44ODggMCAwIDAtLjM5NC4wODMuNzgyLjc4MiAwIDAgMC0uMjgzLjIyNyAxLjAxMyAxLjAxMyAwIDAgMC0uMTc2LjMzMWMtLjAzNy4xMjUtLjA1NS4yNi0uMDU1LjQwNlptNS42NzUgMi42NWMtLjMxOCAwLS42MDctLjA1Mi0uODY0LS4xNTVhMS45MDkgMS45MDkgMCAwIDEtLjY1NC0uNDQzIDEuOTYgMS45NiAwIDAgMS0uNDEtLjY2NSAyLjMzIDIuMzMgMCAwIDEtLjE0My0uODI1di0uMTZjMC0uMzM3LjA0OS0uNjQyLjE0Ny0uOTE2YTIuMDggMi4wOCAwIDAgMSAuNDEtLjdjLjE3Ni0uMTk3LjM4My0uMzQ3LjYyMi0uNDUuMjM5LS4xMDUuNDk4LS4xNTYuNzc3LS4xNTYuMzA4IDAgLjU3OC4wNTIuODA5LjE1NS4yMy4xMDQuNDIyLjI1LjU3My40MzguMTU0LjE4Ni4yNjkuNDA4LjM0My42NjYuMDc3LjI1Ny4xMTUuNTQxLjExNS44NTJ2LjQxaC0zLjMzdi0uNjg5aDIuMzgydi0uMDc1YTEuMzQ3IDEuMzQ3IDAgMCAwLS4xMDMtLjQ4Ni44MjUuODI1IDAgMCAwLS4yODMtLjM2N2MtLjEyOC0uMDkzLS4yOTgtLjE0LS41MS0uMTRhLjg2Ni44NjYgMCAwIDAtLjQyNy4xMDQuODQ0Ljg0NCAwIDAgMC0uMzA2LjI5MSAxLjUzIDEuNTMgMCAwIDAtLjE5MS40NjIgMi41OTcgMi41OTcgMCAwIDAtLjA2NC42MDJ2LjE2YzAgLjE4OC4wMjUuMzYzLjA3NS41MjUuMDU0LjE2LjEzLjI5OS4yMzIuNDE4LjEuMTIuMjIzLjIxNC4zNjYuMjgzLjE0NC4wNjcuMzA3LjEuNDkuMS4yMzEgMCAuNDM3LS4wNDcuNjE4LS4xNC4xOC0uMDkzLjMzNy0uMjI0LjQ3LS4zOTRsLjUwNi40OWExLjgxNCAxLjgxNCAwIDAgMS0uOTA4LjY5IDIuMTcgMi4xNyAwIDAgMS0uNzQyLjExNVptNS4wMzktMS4yNDdhLjQ4MS40ODEgMCAwIDAtLjA3Mi0uMjZjLS4wNDgtLjA3OS0uMTQtLjE1LS4yNzUtLjIxNGEyLjY3IDIuNjcgMCAwIDAtLjU5LS4xNzYgNS4wNjggNS4wNjggMCAwIDEtLjYzLS4xNzkgMS45OTggMS45OTggMCAwIDEtLjQ4NS0uMjU5Ljk5My45OTMgMCAwIDEtLjQyNi0uODM3YzAtLjE3NS4wMzgtLjM0LjExNS0uNDk4LjA3Ny0uMTU2LjE4Ny0uMjk0LjMzLS40MTRhMS42MSAxLjYxIDAgMCAxIC41MjMtLjI4M2MuMjA3LS4wNjkuNDM4LS4xMDMuNjkzLS4xMDMuMzYxIDAgLjY3LjA2LjkyOC4xODMuMjYuMTIuNDYuMjgzLjU5OC40OS4xMzguMjA0LjIwNy40MzYuMjA3LjY5M2gtLjk2YzAtLjExNC0uMDMtLjIyLS4wODgtLjMxOGEuNjEuNjEgMCAwIDAtLjI1NS0uMjQ0Ljg3NC44NzQgMCAwIDAtLjQzLS4wOTUuOTM0LjkzNCAwIDAgMC0uNDEuMDguNTYyLjU2MiAwIDAgMC0uMjQuMTk5LjUwOS41MDkgMCAwIDAtLjAzNS40NjYuNDYuNDYgMCAwIDAgLjE0My4xNTVjLjA2Ni4wNDUuMTU3LjA4OC4yNy4xMjguMTE4LjA0LjI2NC4wNzguNDQuMTE1LjMyOC4wNy42MTEuMTU4Ljg0OC4yNjcuMjM5LjEwNy40MjIuMjQ1LjU1LjQxNS4xMjcuMTY3LjE5LjM4LjE5LjYzNyAwIC4xOTEtLjA0LjM2Ny0uMTIzLjUyNi0uMDguMTU3LS4xOTYuMjkzLS4zNS40MWExLjc2MyAxLjc2MyAwIDAgMS0uNTU0LjI2NyAyLjQ5NSAyLjQ5NSAwIDAgMS0uNzE3LjA5NmMtLjM5IDAtLjcyMS0uMDctLjk5Mi0uMjA3LS4yNzEtLjE0MS0uNDc3LS4zMi0uNjE4LS41MzhhMS4yNzIgMS4yNzIgMCAwIDEtLjIwNy0uNjg1aC45MjhjLjAxLjE3Ny4wNi4zMi4xNDguNDI2LjA5LjEwNC4yMDIuMTguMzM0LjIyNy4xMzYuMDQ1LjI3NS4wNjguNDE5LjA2OC4xNzIgMCAuMzE3LS4wMjMuNDM0LS4wNjhhLjYyNC42MjQgMCAwIDAgLjI2Ny0uMTkxLjQ1Ni40NTYgMCAwIDAgLjA5Mi0uMjhabTQuMzQ0IDBhLjQ4LjQ4IDAgMCAwLS4wNzEtLjI2Yy0uMDQ4LS4wNzktLjE0LS4xNS0uMjc1LS4yMTRhMi42NyAyLjY3IDAgMCAwLS41OS0uMTc2IDUuMDYzIDUuMDYzIDAgMCAxLS42My0uMTc5IDEuOTk4IDEuOTk4IDAgMCAxLS40ODUtLjI1OS45OTMuOTkzIDAgMCAxLS40MjYtLjgzN2MwLS4xNzUuMDM4LS4zNC4xMTUtLjQ5OC4wNzctLjE1Ni4xODctLjI5NC4zMy0uNDE0YTEuNjEgMS42MSAwIDAgMSAuNTIzLS4yODNjLjIwNy0uMDY5LjQzOC0uMTAzLjY5My0uMTAzLjM2MSAwIC42Ny4wNi45MjguMTgzLjI2LjEyLjQ2LjI4My41OTguNDkuMTM4LjIwNC4yMDcuNDM2LjIwNy42OTNoLS45NmMwLS4xMTQtLjAzLS4yMi0uMDg4LS4zMThhLjYxLjYxIDAgMCAwLS4yNTUtLjI0NC44NzQuODc0IDAgMCAwLS40My0uMDk1LjkzNC45MzQgMCAwIDAtLjQxLjA4LjU2Mi41NjIgMCAwIDAtLjI0LjE5OS41MDkuNTA5IDAgMCAwLS4wMzUuNDY2Yy4wMjkuMDU2LjA3Ni4xMDguMTQzLjE1NS4wNjYuMDQ1LjE1Ny4wODguMjcuMTI4LjExOC4wNC4yNjQuMDc4LjQ0LjExNS4zMjkuMDcuNjExLjE1OC44NDguMjY3LjIzOS4xMDcuNDIyLjI0NS41NS40MTUuMTI3LjE2Ny4xOS4zOC4xOS42MzcgMCAuMTkxLS4wNC4zNjctLjEyMy41MjYtLjA4LjE1Ny0uMTk2LjI5My0uMzUuNDFhMS43NjMgMS43NjMgMCAwIDEtLjU1NC4yNjcgMi40OTUgMi40OTUgMCAwIDEtLjcxNy4wOTZjLS4zOSAwLS43MjEtLjA3LS45OTItLjIwNy0uMjcxLS4xNDEtLjQ3Ny0uMzItLjYxOC0uNTM4YTEuMjcyIDEuMjcyIDAgMCAxLS4yMDctLjY4NWguOTI4Yy4wMS4xNzcuMDYuMzIuMTQ4LjQyNi4wOS4xMDQuMjAyLjE4LjMzNC4yMjcuMTM2LjA0NS4yNzUuMDY4LjQxOS4wNjguMTcyIDAgLjMxNy0uMDIzLjQzNC0uMDY4YS42MjQuNjI0IDAgMCAwIC4yNjctLjE5MS40NTYuNDU2IDAgMCAwIC4wOTEtLjI4Wm00LjM1OC4zMDN2LTIuMDU2YS44ODIuODgyIDAgMCAwLS4wODQtLjM5OS41ODYuNTg2IDAgMCAwLS4yNTUtLjI1OS44NzMuODczIDAgMCAwLS40MjItLjA5MS45NTcuOTU3IDAgMCAwLS40MDcuMDguNjU2LjY1NiAwIDAgMC0uMjY3LjIxNS41MTkuNTE5IDAgMCAwLS4wOTUuMzA2aC0uOTU3YzAtLjE3LjA0MS0uMzM0LjEyNC0uNDk0LjA4Mi0uMTU5LjIwMi0uMzAxLjM1OC0uNDI2YTEuNzkgMS43OSAwIDAgMSAuNTYyLS4yOTVjLjIxOC0uMDcyLjQ2Mi0uMTA3LjczMy0uMTA3LjMyNCAwIC42MTEuMDU0Ljg2LjE2My4yNTMuMTA5LjQ1MS4yNzQuNTk1LjQ5NC4xNDYuMjE4LjIxOS40OTEuMjE5LjgydjEuOTE3YzAgLjE5Ny4wMTMuMzczLjA0LjUzLjAyOS4xNTQuMDcuMjg4LjEyMy40MDJ2LjA2NGgtLjk4NGExLjcwMSAxLjcwMSAwIDAgMS0uMTA4LS4zOTQgMy4yMjMgMy4yMjMgMCAwIDEtLjAzNS0uNDdabS4xMzktMS43NTcuMDA4LjU5M2gtLjY5YTEuOTEgMS45MSAwIDAgMC0uNDcuMDUyLjk2My45NjMgMCAwIDAtLjMzOC4xNDMuNjIxLjYyMSAwIDAgMC0uMjcxLjUzOGMwIC4xMTUuMDI2LjIyLjA4LjMxNS4wNTMuMDkzLjEzLjE2Ni4yMy4yMi4xMDQuMDUyLjIzLjA3OS4zNzUuMDc5YTEuMDU3IDEuMDU3IDAgMCAwIC44NjUtLjQxOC42NS42NSAwIDAgMCAuMTM1LS4zNGwuMzExLjQyN2ExLjQ1NiAxLjQ1NiAwIDAgMS0uMTYzLjM1IDEuNjk0IDEuNjk0IDAgMCAxLS4zMDMuMzYgMS41MDIgMS41MDIgMCAwIDEtMS4wMzIuMzgyYy0uMjgyIDAtLjUzMy0uMDU2LS43NTMtLjE2OGExLjMzOSAxLjMzOSAwIDAgMS0uNTE4LS40NTggMS4xODkgMS4xODkgMCAwIDEtLjE4Ny0uNjU3YzAtLjIyOC4wNDItLjQzLjEyNy0uNjA2LjA4OC0uMTc4LjIxNS0uMzI2LjM4My0uNDQ2LjE3LS4xMi4zNzctLjIxLjYyMS0uMjcuMjQ1LS4wNjUuNTI0LS4wOTYuODM3LS4wOTZoLjc1M1ptNC43MzUtMS42OWguODczdjQuMTkyYzAgLjM4Ny0uMDgyLjcxNy0uMjQ3Ljk4OGExLjU4OCAxLjU4OCAwIDAgMS0uNjkuNjE3IDIuNDA1IDIuNDA1IDAgMCAxLTIuMTU1LS4wODggMS40NDMgMS40NDMgMCAwIDEtLjQ2Ni0uNDFsLjQ1LS41NjZjLjE1NC4xODQuMzI0LjMxOC41MS40MDMuMTg2LjA4NS4zODEuMTI3LjU4Ni4xMjcuMjIgMCAuNDA4LS4wNC41NjItLjEyM2EuODM0LjgzNCAwIDAgMCAuMzYyLS4zNTUgMS4xOSAxLjE5IDAgMCAwIC4xMjgtLjU3M1YxOC45OWwuMDg3LS45NzdabS0yLjkyOCAyLjIwNHYtLjA4NGMwLS4zMjcuMDQtLjYyNC4xMi0uODkzLjA4LS4yNy4xOTMtLjUwMy4zNDItLjY5Ny4xNDktLjE5Ni4zMy0uMzQ2LjU0Mi0uNDUuMjEyLS4xMDYuNDUzLS4xNi43MjEtLjE2LjI3OSAwIC41MTcuMDUxLjcxMy4xNTIuMi4xMDEuMzY1LjI0Ni40OTguNDM0LjEzMy4xODYuMjM3LjQxLjMxMS42Ny4wNzcuMjU3LjEzNC41NDQuMTcxLjg2di4yNjdjLS4wMzQuMzA4LS4wOTMuNTktLjE3NS44NDVhMi4zMjggMi4zMjggMCAwIDEtLjMyNy42NjFjLS4xMzUuMTg2LS4zMDIuMzMtLjUwMi40My0uMTk2LjEwMS0uNDI5LjE1Mi0uNjk3LjE1Mi0uMjYzIDAtLjUtLjA1NS0uNzEzLS4xNjRhMS42MjQgMS42MjQgMCAwIDEtLjU0Mi0uNDU4IDIuMTcgMi4xNyAwIDAgMS0uMzQzLS42OTNjLS4wOC0uMjY4LS4xMTktLjU1OS0uMTE5LS44NzJabS45Ni0uMDg0di4wODRjMCAuMTk2LjAxOS4zOC4wNTYuNTUuMDQuMTcuMS4zMi4xOC40NWEuOTQuOTQgMCAwIDAgLjMxLjMwMi45MDQuOTA0IDAgMCAwIC40NS4xMDhjLjIyNiAwIC40MS0uMDQ4LjU1NC0uMTQzYS45MjguOTI4IDAgMCAwIC4zMzUtLjM4N2MuMDgtLjE2NS4xMzUtLjM0OC4xNjctLjU1di0uNzJhMS43NTcgMS43NTcgMCAwIDAtLjEtLjQ0IDEuMTcyIDEuMTcyIDAgMCAwLS4xOTUtLjM1NC44MTMuODEzIDAgMCAwLS4zMS0uMjM5IDEuMDMzIDEuMDMzIDAgMCAwLS40NDMtLjA4Ny44NzcuODc3IDAgMCAwLS40NS4xMTEuOTE1LjkxNSAwIDAgMC0uMzE1LjMwN2MtLjA4LjEzLS4xNC4yODEtLjE4LjQ1NC0uMDM5LjE3My0uMDU5LjM1Ny0uMDU5LjU1NFptNS44ODMgMi4yN2MtLjMxOSAwLS42MDctLjA1LS44NjUtLjE1NGExLjkwOSAxLjkwOSAwIDAgMS0uNjUzLS40NDMgMS45NiAxLjk2IDAgMCAxLS40MS0uNjY1IDIuMzMgMi4zMyAwIDAgMS0uMTQ0LS44MjV2LS4xNmMwLS4zMzcuMDUtLjY0Mi4xNDgtLjkxNmEyLjA4IDIuMDggMCAwIDEgLjQxLS43Yy4xNzUtLjE5Ny4zODMtLjM0Ny42MjItLjQ1LjIzOS0uMTA1LjQ5OC0uMTU2Ljc3Ni0uMTU2LjMwOSAwIC41NzguMDUyLjgxLjE1NS4yMy4xMDQuNDIyLjI1LjU3My40MzguMTU0LjE4Ni4yNjguNDA4LjM0My42NjYuMDc3LjI1Ny4xMTUuNTQxLjExNS44NTJ2LjQxaC0zLjMzdi0uNjg5aDIuMzgydi0uMDc1YTEuMzUxIDEuMzUxIDAgMCAwLS4xMDQtLjQ4Ni44MjYuODI2IDAgMCAwLS4yODItLjM2N2MtLjEyOC0uMDkzLS4yOTgtLjE0LS41MS0uMTRhLjg2Ny44NjcgMCAwIDAtLjQyNy4xMDQuODQ0Ljg0NCAwIDAgMC0uMzA3LjI5MSAxLjUzMyAxLjUzMyAwIDAgMC0uMTkuNDYyIDIuNTk3IDIuNTk3IDAgMCAwLS4wNjQuNjAydi4xNmMwIC4xODguMDI1LjM2My4wNzUuNTI1LjA1My4xNi4xMy4yOTkuMjMxLjQxOC4xMDEuMTIuMjIzLjIxNC4zNjcuMjgzLjE0My4wNjcuMzA3LjEuNDkuMS4yMyAwIC40MzctLjA0Ny42MTctLjE0LjE4MS0uMDkzLjMzOC0uMjI0LjQ3LS4zOTRsLjUwNy40OWExLjgxNCAxLjgxNCAwIDAgMS0uOTA4LjY5IDIuMTcgMi4xNyAwIDAgMS0uNzQyLjExNVptNS4wMzgtMS4yNDZhLjQ4LjQ4IDAgMCAwLS4wNzEtLjI2Yy0uMDQ4LS4wNzktLjE0LS4xNS0uMjc1LS4yMTRhMi42NyAyLjY3IDAgMCAwLS41OS0uMTc2IDUuMDY4IDUuMDY4IDAgMCAxLS42My0uMTc5IDEuOTk4IDEuOTk4IDAgMCAxLS40ODYtLjI1OS45OTMuOTkzIDAgMCAxLS40MjYtLjgzN2MwLS4xNzUuMDM5LS4zNC4xMTYtLjQ5OC4wNzctLjE1Ni4xODctLjI5NC4zMy0uNDE0YTEuNjEgMS42MSAwIDAgMSAuNTIyLS4yODNjLjIwOC0uMDY5LjQzOS0uMTAzLjY5NC0uMTAzLjM2IDAgLjY3LjA2LjkyOC4xODMuMjYuMTIuNDYuMjgzLjU5Ny40OS4xMzkuMjA0LjIwOC40MzYuMjA4LjY5M2gtLjk2YzAtLjExNC0uMDMtLjIyLS4wODgtLjMxOGEuNjEuNjEgMCAwIDAtLjI1NS0uMjQ0Ljg3My44NzMgMCAwIDAtLjQzLS4wOTUuOTM0LjkzNCAwIDAgMC0uNDEuMDguNTYyLjU2MiAwIDAgMC0uMjQuMTk5LjUwOS41MDkgMCAwIDAtLjAzNi40NjYuNDYuNDYgMCAwIDAgLjE0NC4xNTVjLjA2Ni4wNDUuMTU2LjA4OC4yNy4xMjguMTE3LjA0LjI2NC4wNzguNDM5LjExNS4zMy4wNy42MTIuMTU4Ljg0OS4yNjcuMjM5LjEwNy40MjIuMjQ1LjU1LjQxNS4xMjcuMTY3LjE5LjM4LjE5LjYzNyAwIC4xOTEtLjA0LjM2Ny0uMTIzLjUyNi0uMDguMTU3LS4xOTcuMjkzLS4zNS40MWExLjc2MSAxLjc2MSAwIDAgMS0uNTU0LjI2NyAyLjQ5NSAyLjQ5NSAwIDAgMS0uNzE4LjA5NmMtLjM5IDAtLjcyLS4wNy0uOTkyLS4yMDctLjI3LS4xNDEtLjQ3Ni0uMzItLjYxNy0uNTM4YTEuMjczIDEuMjczIDAgMCAxLS4yMDctLjY4NWguOTI4Yy4wMS4xNzcuMDYuMzIuMTQ3LjQyNi4wOS4xMDQuMjAyLjE4LjMzNS4yMjcuMTM2LjA0NS4yNzUuMDY4LjQxOC4wNjguMTczIDAgLjMxOC0uMDIzLjQzNS0uMDY4YS42MjQuNjI0IDAgMCAwIC4yNjctLjE5MS40NTYuNDU2IDAgMCAwIC4wOTEtLjI4WiIvPjwvZz48ZyBjbGlwLXBhdGg9InVybCgjZSkiPjxwYXRoIGZpbGw9IiMwMDAiIGZpbGwtb3BhY2l0eT0iLjU0IiBkPSJNMTA3LjE1MiA2LjY2M3Y1aC0uNjMyVjcuNDVsLTEuMjczLjQ2NXYtLjU3bDEuODA2LS42ODNoLjA5OVptNC4xNjcgMHY1aC0uNjMxVjcuNDVsLTEuMjc0LjQ2NXYtLjU3bDEuODA2LS42ODNoLjA5OVptMi40NjMuMDI3aC42MzlsMS42MjkgNC4wNTMgMS42MjUtNC4wNTNoLjY0MmwtMi4wMjEgNC45NzJoLS40OTlsLTIuMDE1LTQuOTcyWm0tLjIwOCAwaC41NjRsLjA5MiAzLjAzMnYxLjk0aC0uNjU2VjYuNjlabTQuMzg1IDBoLjU2M3Y0Ljk3MmgtLjY1NXYtMS45NGwuMDkyLTMuMDMyWm02LjAyNiAwLTIuMDczIDUuMzk5aC0uNTQzbDIuMDc2LTUuNGguNTRabTMuNjIxIDIuNjA2LS41MDUtLjEzLjI0OS0yLjQ3NmgyLjU1MXYuNTg0aC0yLjAxNWwtLjE1IDEuMzUyYTEuODYgMS44NiAwIDAgMSAuMzQ1LS4xNDcgMS41OCAxLjU4IDAgMCAxIC40ODUtLjA2OGMuMjMgMCAuNDM2LjA0LjYxOC4xMi4xODIuMDc3LjMzNy4xODkuNDY1LjMzNC4xMjkuMTQ2LjIyOC4zMjEuMjk3LjUyNi4wNjguMjA1LjEwMi40MzQuMTAyLjY4NyAwIC4yMzktLjAzMy40NTgtLjA5OS42NTktLjA2NC4yLS4xNi4zNzUtLjI5LjUyNmExLjMxIDEuMzEgMCAwIDEtLjQ5Mi4zNDUgMS43OSAxLjc5IDAgMCAxLS42OTMuMTIyIDEuOTQgMS45NCAwIDAgMS0uNTctLjA4MiAxLjQ3NSAxLjQ3NSAwIDAgMS0uNDc5LS4yNTYgMS40MDEgMS40MDEgMCAwIDEtLjM0MS0uNDMgMS43MjIgMS43MjIgMCAwIDEtLjE2NC0uNjA4aC42MDFjLjAyNy4xODcuMDgyLjM0NC4xNjQuNDcxYS44MDQuODA0IDAgMCAwIC4zMjEuMjljLjEzNC4wNjUuMjkuMDk2LjQ2OC4wOTZhLjk2Ljk2IDAgMCAwIC4zOTktLjA3OC43OTEuNzkxIDAgMCAwIC4yOTQtLjIyNmMuMDgtLjA5OC4xNC0uMjE2LjE4MS0uMzU1LjA0My0uMTM5LjA2NS0uMjk1LjA2NS0uNDY4IDAtLjE1Ny0uMDIyLS4zMDItLjA2NS0uNDM3YS45OS45OSAwIDAgMC0uMTk1LS4zNTEuODQ3Ljg0NyAwIDAgMC0uMzEtLjIzMy45OTguOTk4IDAgMCAwLS40MjQtLjA4NWMtLjIxMiAwLS4zNzIuMDI4LS40ODEuMDg1YTEuODUxIDEuODUxIDAgMCAwLS4zMzIuMjMzWm02LjQ5LS41MTZ2Ljc1OGMwIC40MDgtLjAzNy43NTEtLjEwOSAxLjAzMS0uMDczLjI4LS4xNzguNTA2LS4zMTUuNjc2LS4xMzYuMTcxLS4zMDEuMjk1LS40OTUuMzczYTEuNzYgMS43NiAwIDAgMS0uNjQ5LjExMiAxLjg2IDEuODYgMCAwIDEtLjUyOS0uMDcxIDEuMjU1IDEuMjU1IDAgMCAxLS40MzctLjIzIDEuMzggMS4zOCAwIDAgMS0uMzI4LS40MTYgMi4yNDUgMi4yNDUgMCAwIDEtLjIwOC0uNjIxIDQuNDYxIDQuNDYxIDAgMCAxLS4wNzItLjg1NFY4Ljc4YzAtLjQwOC4wMzYtLjc1LjEwOS0xLjAyNS4wNzUtLjI3NS4xODEtLjQ5Ni4zMTgtLjY2Mi4xMzYtLjE2OS4zLS4yOS40OTItLjM2Mi4xOTMtLjA3My40MDktLjExLjY0OC0uMTEuMTk0IDAgLjM3Mi4wMjUuNTMzLjA3MmExLjE5OSAxLjE5OSAwIDAgMSAuNzYyLjYyNWMuMDkxLjE2Ni4xNi4zNy4yMDguNjEyLjA0OC4yNC4wNzIuNTI0LjA3Mi44NVptLS42MzUuODZ2LS45NjZjMC0uMjIzLS4wMTQtLjQxOS0uMDQxLS41ODdhMS44NDcgMS44NDcgMCAwIDAtLjExMy0uNDM3Ljg2OS44NjkgMCAwIDAtLjE5MS0uMjk0LjY3Ni42NzYgMCAwIDAtLjI2My0uMTY0Ljk1Ljk1IDAgMCAwLS4zMzItLjA1NS44OTUuODk1IDAgMCAwLS4zOTkuMDg2LjcyMS43MjEgMCAwIDAtLjI5NC4yNjNjLS4wNzcuMTItLjEzNi4yNzgtLjE3Ny40NzRhMy41MzggMy41MzggMCAwIDAtLjA2Mi43MTR2Ljk2NmMwIC4yMjQuMDEzLjQyLjAzOC41OTEuMDI3LjE3MS4wNjcuMzE5LjExOS40NDQuMDUzLjEyMy4xMTYuMjI0LjE5Mi4zMDRhLjcxLjcxIDAgMCAwIC4yNTkuMTc4Yy4xLjAzNi4yMTEuMDU0LjMzMS4wNTQuMTU1IDAgLjI5MS0uMDMuNDA3LS4wODhhLjczLjczIDAgMCAwIC4yOS0uMjc3Yy4wOC0uMTI4LjEzOS0uMjkuMTc4LS40ODguMDM4LS4yLjA1OC0uNDQuMDU4LS43MThabTIuMDUzLTIuOTVoLjYzOWwxLjYyOCA0LjA1MyAxLjYyNi00LjA1M2guNjQybC0yLjAyMiA0Ljk3MmgtLjQ5OGwtMi4wMTUtNC45NzJabS0uMjA4IDBoLjU2M2wuMDkyIDMuMDMydjEuOTRoLS42NTVWNi42OVptNC4zODQgMGguNTY0djQuOTcyaC0uNjU2di0xLjk0bC4wOTItMy4wMzJaIi8+PGcgZmlsbD0iIzE5ODAzOCIgY2xpcC1wYXRoPSJ1cmwoI2YpIj48cmVjdCB3aWR0aD0iODYuMDEyIiBoZWlnaHQ9IjQuNjYzIiB4PSIxMDQuNjYzIiB5PSIxNi45OTMiIGZpbGwtb3BhY2l0eT0iLjA2IiByeD0iMi4zMzEiLz48cGF0aCBkPSJNMTA0LjY2MyAxNS44MjdoMjYuMDEydjYuOTk0aC0yNi4wMTJ6Ii8+PC9nPjxwYXRoIGZpbGw9IiMxOTgwMzgiIGQ9Ik0xMDguMzk5IDMwLjQ1MXYuNTM2aC0yLjYzM3YtLjUzNmgyLjYzM1ptLTIuNS00LjQzNnY0Ljk3MmgtLjY1OXYtNC45NzJoLjY1OVptMi4xNTEgMi4xMzh2LjUzNmgtMi4yODR2LS41MzZoMi4yODRabS4zMTQtMi4xMzh2LjU0aC0yLjU5OHYtLjU0aDIuNTk4Wm0xLjYyIDIuMDY2djIuOTA2aC0uNjMydi0zLjY5NWguNTk4bC4wMzQuNzlabS0uMTUuOTE5LS4yNjMtLjAxYTIuMjEgMi4yMSAwIDAgMSAuMTEzLS43Yy4wNzItLjIxNy4xNzUtLjQwNS4zMDctLjU2NGExLjM2OSAxLjM2OSAwIDAgMSAxLjA4Mi0uNTAyYy4xODMgMCAuMzQ2LjAyNS40OTIuMDc1LjE0Ni4wNDguMjcuMTI1LjM3Mi4yMzIuMTA1LjEwNy4xODUuMjQ2LjIzOS40MTcuMDU1LjE2OC4wODIuMzc1LjA4Mi42MTh2Mi40MjFoLS42MzVWMjguNTZjMC0uMTkzLS4wMjgtLjM0OC0uMDg1LS40NjRhLjUyNi41MjYgMCAwIDAtLjI0OS0uMjU2Ljg5OS44OTkgMCAwIDAtLjQwMy0uMDgyLjk0Ljk0IDAgMCAwLS43NjIuMzcyYy0uMDkxLjExNi0uMTYzLjI1LS4yMTUuNC0uMDUuMTQ4LS4wNzUuMzA1LS4wNzUuNDdabTUuNzk2IDEuMzU1di0xLjkwMmEuNzczLjc3MyAwIDAgMC0uMDg5LS4zNzkuNTgxLjU4MSAwIDAgMC0uMjU5LS4yNTIuOTQ2Ljk0NiAwIDAgMC0uNDMxLS4wOWMtLjE1OSAwLS4yOTkuMDI4LS40Mi4wODNhLjc0Ljc0IDAgMCAwLS4yOC4yMTUuNDcyLjQ3MiAwIDAgMC0uMDk5LjI4N2gtLjYzMmEuODQuODQgMCAwIDEgLjEwMy0uMzkzIDEuMTQgMS4xNCAwIDAgMSAuMjk0LS4zNTJjLjEyOS0uMTA3LjI4NC0uMTkuNDY0LS4yNTIuMTgyLS4wNjQuMzg1LS4wOTYuNjA4LS4wOTYuMjY4IDAgLjUwNS4wNDYuNzEuMTM3LjIwNy4wOS4zNjkuMjI4LjQ4NS40MTMuMTE4LjE4Mi4xNzguNDEuMTc4LjY4NnYxLjcyMWMwIC4xMjMuMDEuMjU0LjAzLjM5My4wMjMuMTM5LjA1Ni4yNTguMDk5LjM1OXYuMDU0aC0uNjU5YTEuMTY1IDEuMTY1IDAgMCAxLS4wNzUtLjI5IDIuMzY3IDIuMzY3IDAgMCAxLS4wMjctLjM0MVptLjEwOS0xLjYwOC4wMDcuNDQ0aC0uNjM5Yy0uMTc5IDAtLjM0LjAxNS0uNDgxLjA0NC0uMTQxLjAyOC0uMjYuMDctLjM1NS4xMjdhLjU3MS41NzEgMCAwIDAtLjI5NC41MTJjMCAuMTE2LjAyNi4yMjIuMDc5LjMxOGEuNTcyLjU3MiAwIDAgMCAuMjM1LjIyOC44NTYuODU2IDAgMCAwIC4zOTMuMDgyIDEuMDcxIDEuMDcxIDAgMCAwIC44NjQtLjQyNC42MzguNjM4IDAgMCAwIC4xNDMtLjM0NGwuMjcuMzA0YS45MS45MSAwIDAgMS0uMTMuMzE3IDEuNTExIDEuNTExIDAgMCAxLS43LjU5OCAxLjM1NCAxLjM1NCAwIDAgMS0uNTM5LjEwM2MtLjI1MSAwLS40Ny0uMDUtLjY1OS0uMTQ3YTEuMTE5IDEuMTE5IDAgMCAxLS40MzctLjM5MyAxLjAzNSAxLjAzNSAwIDAgMS0uMTU0LS41NTdjMC0uMTk4LjAzOS0uMzcyLjExNi0uNTIyLjA3Ny0uMTUzLjE4OS0uMjc5LjMzNS0uMzguMTQ1LS4xMDIuMzIxLS4xNzkuNTI2LS4yMzEuMjA0LS4wNTMuNDMzLS4wNzkuNjg2LS4wNzloLjczNFptMS43NDYtMy4wMDVoLjYzNXY0LjUyOGwtLjA1NC43MTdoLS41ODF2LTUuMjQ1Wm0zLjEzMiAzLjM2N3YuMDcyYzAgLjI2OC0uMDMyLjUxOC0uMDk2Ljc0OGExLjg0NCAxLjg0NCAwIDAgMS0uMjguNTk0Yy0uMTIzLjE2OC0uMjczLjMtLjQ1MS4zOTMtLjE3Ny4wOTMtLjM4MS4xNC0uNjExLjE0LS4yMzUgMC0uNDQxLS4wNC0uNjE4LS4xMmExLjIyIDEuMjIgMCAwIDEtLjQ0NC0uMzUyIDEuNzkyIDEuNzkyIDAgMCAxLS4yOS0uNTUzIDMuNDQgMy40NCAwIDAgMS0uMTQ3LS43M3YtLjMxNWMuMDI3LS4yNzMuMDc2LS41MTguMTQ3LS43MzQuMDcyLS4yMTYuMTY5LS40LjI5LS41NTMuMTIxLS4xNTUuMjY5LS4yNzIuNDQ0LS4zNTIuMTc1LS4wODIuMzc5LS4xMjMuNjExLS4xMjMuMjMyIDAgLjQzOC4wNDYuNjE4LjEzNy4xOC4wODguMzMuMjE2LjQ1MS4zODIuMTIzLjE2Ni4yMTYuMzY2LjI4LjU5OC4wNjQuMjMuMDk2LjQ4Ni4wOTYuNzY4Wm0tLjYzNi4wNzJ2LS4wNzJhMi41MiAyLjUyIDAgMCAwLS4wNTEtLjUxOSAxLjM0NCAxLjM0NCAwIDAgMC0uMTY0LS40My44MTUuODE1IDAgMCAwLS4yOTctLjI5NC44NzcuODc3IDAgMCAwLS40NTQtLjExLjk5MS45OTEgMCAwIDAtLjQxNy4wODMuOS45IDAgMCAwLS4yOTcuMjIyIDEuMTY2IDEuMTY2IDAgMCAwLS4yMDEuMzE0Yy0uMDUuMTE2LS4wODguMjM3LS4xMTMuMzYydi44MjNjLjAzNy4xNi4wOTYuMzEzLjE3OC40Ni4wODQuMTQ3LjE5Ni4yNjYuMzM0LjM2YS45My45MyAwIDAgMCAuNTIzLjE0Ljg3NC44NzQgMCAwIDAgLjQzNy0uMTAzLjgyMy44MjMgMCAwIDAgLjI5Ny0uMjljLjA3OC0uMTIzLjEzNC0uMjY1LjE3MS0uNDI3LjAzNi0uMTYyLjA1NC0uMzM1LjA1NC0uNTJabTIuMzU0LTMuNDR2NS4yNDZoLS42MzV2LTUuMjQ1aC42MzVabTIuNzgxIDUuMzE1Yy0uMjU3IDAtLjQ5MS0uMDQ0LS43LS4xM2ExLjU4NSAxLjU4NSAwIDAgMS0uNTM2LS4zNzIgMS42NjEgMS42NjEgMCAwIDEtLjM0Mi0uNTY3IDIuMDk1IDIuMDk1IDAgMCAxLS4xMTktLjcxN3YtLjE0NGMwLS4zLjA0NC0uNTY4LjEzMy0uODAyLjA4OS0uMjM3LjIwOS0uNDM4LjM2Mi0uNjAxYTEuNTQgMS41NCAwIDAgMSAuNTE5LS4zNzNjLjE5NC0uMDg0LjM5NC0uMTI2LjYwMS0uMTI2LjI2NCAwIC40OTIuMDQ2LjY4My4xMzcuMTk0LjA5LjM1Mi4yMTguNDc1LjM4Mi4xMjMuMTYyLjIxNC4zNTMuMjczLjU3NC4wNTkuMjE4LjA4OS40NTcuMDg5LjcxN3YuMjgzaC0yLjc2di0uNTE1aDIuMTI4di0uMDQ4YTEuNTUgMS41NSAwIDAgMC0uMTAzLS40NzguODQ3Ljg0NyAwIDAgMC0uMjczLS4zODNjLS4xMjUtLjEtLjI5Ni0uMTUtLjUxMi0uMTVhLjg2MS44NjEgMCAwIDAtLjcwNy4zNTkgMS4zMzMgMS4zMzMgMCAwIDAtLjIwMS40MzMgMi4xOSAyLjE5IDAgMCAwLS4wNzIuNTkxdi4xNDRjMCAuMTc1LjAyNC4zNC4wNzIuNDk1LjA1LjE1Mi4xMjEuMjg3LjIxNS40MDMuMDk1LjExNi4yMS4yMDcuMzQ1LjI3My4xMzYuMDY2LjI5MS4wOTkuNDY0LjA5OS4yMjMgMCAuNDEyLS4wNDYuNTY3LS4xMzcuMTU1LS4wOS4yOS0uMjEyLjQwNi0uMzY1bC4zODMuMzA0Yy0uMDguMTItLjE4MS4yMzYtLjMwNC4zNDUtLjEyMy4xMS0uMjc0LjE5OC0uNDU0LjI2NmExLjc2IDEuNzYgMCAwIDEtLjYzMi4xMDNabTQuNzM3LS43ODZ2LTQuNTI4aC42MzZ2NS4yNDVoLS41ODFsLS4wNTUtLjcxN1ptLTIuNDg2LTEuMDl2LS4wN2MwLS4yODMuMDM1LS41NC4xMDMtLjc3LjA3LS4yMzIuMTY5LS40My4yOTctLjU5N2ExLjMxIDEuMzEgMCAwIDEgMS4wNjItLjUyYy4yMzIuMDAxLjQzNS4wNDIuNjA4LjEyNC4xNzUuMDguMzIzLjE5Ny40NDQuMzUyLjEyMy4xNTIuMjIuMzM3LjI5LjU1My4wNzEuMjE2LjEyLjQ2LjE0Ny43MzR2LjMxNGMtLjAyNS4yNzEtLjA3NC41MTUtLjE0Ny43MzEtLjA3LjIxNi0uMTY3LjQtLjI5LjU1M2ExLjIyIDEuMjIgMCAwIDEtLjQ0NC4zNTJjLS4xNzUuMDgtLjM4LjEyLS42MTUuMTItLjIxNiAwLS40MTQtLjA0Ny0uNTk0LS4xNGExLjQwMiAxLjQwMiAwIDAgMS0uNDYxLS4zOTMgMS44OTEgMS44OTEgMCAwIDEtLjI5Ny0uNTk0IDIuNjI3IDIuNjI3IDAgMCAxLS4xMDMtLjc0OFptLjYzNi0uMDd2LjA3YzAgLjE4NS4wMTguMzU4LjA1NC41Mi4wMzkuMTYyLjA5OC4zMDQuMTc4LjQyNy4wNzkuMTIzLjE4MS4yMi4zMDQuMjkuMTIzLjA2OC4yNy4xMDIuNDQuMTAyLjIxIDAgLjM4Mi0uMDQ0LjUxNi0uMTMzYS45OS45OSAwIDAgMCAuMzI4LS4zNTFjLjA4Mi0uMTQ2LjE0NS0uMzA0LjE5MS0uNDc1di0uODIzYTEuNzY1IDEuNzY1IDAgMCAwLS4xMi0uMzYyIDEuMTEgMS4xMSAwIDAgMC0uMTk4LS4zMTQuODQzLjg0MyAwIDAgMC0uMjk3LS4yMjIuOTYzLjk2MyAwIDAgMC0uNDEzLS4wODIuODczLjg3MyAwIDAgMC0uNDQ3LjEwOS44NjYuODY2IDAgMCAwLS4zMDQuMjk0Yy0uMDguMTIzLS4xMzkuMjY2LS4xNzguNDNhMi4zODQgMi4zODQgMCAwIDAtLjA1NC41MlpNMTg2LjAxMiAyNS4xMDJhMy44ODYgMy44ODYgMCAwIDAgMCA3Ljc3IDMuODg3IDMuODg3IDAgMCAwIDMuODg2LTMuODg1IDMuODg3IDMuODg3IDAgMCAwLTMuODg2LTMuODg1Wm0tLjc3NyA1LjgyOC0xLjk0My0xLjk0My41NDgtLjU0OCAxLjM5NSAxLjM5MSAyLjk0OS0yLjk0OS41NDguNTUyLTMuNDk3IDMuNDk3WiIvPjwvZz48cGF0aCBmaWxsPSIjMzA1NjgwIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yMDAgMzkuMjMzSDB2LS41ODNoMjAwdi41ODNaIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiLz48ZyBjbGlwLXBhdGg9InVybCgjZykiPjxwYXRoIGZpbGw9IiMzMDU2ODAiIGQ9Ik0xMi4zMDEgNDkuNzU4djUuOGgtLjk5MnYtNS44aC45OTJabTEuODIxIDB2Ljc5Nkg5LjUwNHYtLjc5Nmg0LjYxOFptMS40OTIgMi4zMXYzLjQ5aC0uOTZ2LTQuMzFoLjkxN2wuMDQzLjgyWm0xLjMyLS44NDgtLjAwOS44OTJhMi41MDMgMi41MDMgMCAwIDAtLjM5LS4wMzJjLS4xNjUgMC0uMzEuMDI0LS40MzQuMDcyYS44MTguODE4IDAgMCAwLS41MDYuNTEgMS4zOTMgMS4zOTMgMCAwIDAtLjA4LjQxbC0uMjIuMDE2YzAtLjI3LjAyNy0uNTIyLjA4LS43NTMuMDUzLS4yMy4xMzMtLjQzNC4yNC0uNjEuMTA4LS4xNzQuMjQ0LS4zMTEuNDA2LS40MWExLjA5IDEuMDkgMCAwIDEgLjU3LS4xNDcgMS4xOSAxLjE5IDAgMCAxIC4zNDIuMDUyWm0zLjAzMyAzLjQ3NHYtMi4wNTZhLjg4Mi44ODIgMCAwIDAtLjA4My0uMzk4LjU4Ni41ODYgMCAwIDAtLjI1NS0uMjYuODcyLjg3MiAwIDAgMC0uNDIzLS4wOS45NTcuOTU3IDAgMCAwLS40MDYuMDc5LjY1Ni42NTYgMCAwIDAtLjI2Ny4yMTUuNTIuNTIgMCAwIDAtLjA5Ni4zMDdoLS45NTZjMC0uMTcuMDQxLS4zMzUuMTI0LS40OTRhMS4zMiAxLjMyIDAgMCAxIC4zNTgtLjQyNiAxLjc5IDEuNzkgMCAwIDEgLjU2Mi0uMjk1Yy4yMTgtLjA3Mi40NjItLjEwOC43MzMtLjEwOC4zMjQgMCAuNjExLjA1NC44Ni4xNjMuMjUzLjExLjQ1MS4yNzQuNTk0LjQ5NC4xNDYuMjE4LjIyLjQ5Mi4yMi44MjF2MS45MTdjMCAuMTk2LjAxMy4zNzMuMDQuNTMuMDI5LjE1My4wNy4yODguMTIzLjQwMnYuMDY0aC0uOTg0YTEuNzA2IDEuNzA2IDAgMCAxLS4xMDgtLjM5NSAzLjIyMyAzLjIyMyAwIDAgMS0uMDM2LS40N1ptLjE0LTEuNzU3LjAwOC41OTRoLS42OWExLjkxIDEuOTEgMCAwIDAtLjQ3LjA1Mi45NjMuOTYzIDAgMCAwLS4zMzguMTQzLjYyMi42MjIgMCAwIDAtLjI3MS41MzhjMCAuMTE0LjAyNi4yMTkuMDguMzE0LjA1My4wOTMuMTMuMTY2LjIzLjIyLjEwNC4wNTMuMjI5LjA4LjM3NS4wOGExLjA1NyAxLjA1NyAwIDAgMCAuODY1LS40MTkuNjUuNjUgMCAwIDAgLjEzNS0uMzM5bC4zMS40MjdhMS40NTYgMS40NTYgMCAwIDEtLjE2My4zNSAxLjY5NiAxLjY5NiAwIDAgMS0uMzAyLjM1OSAxLjUwMyAxLjUwMyAwIDAgMS0xLjAzMi4zODJjLS4yODIgMC0uNTMzLS4wNTUtLjc1My0uMTY3YTEuMzQgMS4zNCAwIDAgMS0uNTE4LS40NTggMS4xODkgMS4xODkgMCAwIDEtLjE4Ny0uNjU4YzAtLjIyOC4wNDItLjQzLjEyNy0uNjA1LjA4OC0uMTc4LjIxNS0uMzI3LjM4My0uNDQ2LjE3LS4xMi4zNzctLjIxLjYyMS0uMjcxLjI0NS0uMDY0LjUyMy0uMDk2LjgzNy0uMDk2aC43NTNabTIuOTI3LS43Njl2My4zOWgtLjk2di00LjMxaC45MDRsLjA1NS45MlptLS4xNzIgMS4wNzYtLjMxLS4wMDRhMi44IDIuOCAwIDAgMSAuMTI3LS44NCAyLjA3IDIuMDcgMCAwIDEgLjM1LS42NThjLjE1Mi0uMTgzLjMzMi0uMzI0LjU0Mi0uNDIyLjIxLS4xMDIuNDQ0LS4xNTIuNzAxLS4xNTIuMjA4IDAgLjM5NS4wMy41NjIuMDg4LjE3LjA1Ni4zMTUuMTQ3LjQzNS4yNzUuMTIyLjEyNy4yMTUuMjkzLjI3OS40OTguMDYzLjIwMS4wOTUuNDUuMDk1Ljc0NXYyLjc4NWgtLjk2NHYtMi43OWMwLS4yMDYtLjAzLS4zNy0uMDkyLS40OWEuNTEzLjUxMyAwIDAgMC0uMjU5LS4yNTguOTcxLjk3MSAwIDAgMC0uNDE4LS4wOC45MjkuOTI5IDAgMCAwLS43NzMuMzg2Yy0uMDg4LjEyLS4xNTUuMjU4LS4yMDMuNDE1YTEuNzEyIDEuNzEyIDAgMCAwLS4wNzIuNTAyWm02LjMyNSAxLjE0N2EuNDguNDggMCAwIDAtLjA3Mi0uMjU5Yy0uMDQ3LS4wOC0uMTM5LS4xNTEtLjI3NC0uMjE1YTIuNjY0IDIuNjY0IDAgMCAwLS41OS0uMTc1IDUuMDY4IDUuMDY4IDAgMCAxLS42My0uMTggMS45OTggMS45OTggMCAwIDEtLjQ4Ni0uMjU4Ljk5My45OTMgMCAwIDEtLjQyNi0uODM3YzAtLjE3NS4wMzktLjM0MS4xMTYtLjQ5OC4wNzctLjE1Ny4xODctLjI5NS4zMy0uNDE1YTEuNjEgMS42MSAwIDAgMSAuNTIyLS4yODJjLjIwNy0uMDcuNDM5LS4xMDQuNjk0LS4xMDQuMzYgMCAuNjcuMDYxLjkyOC4xODMuMjYuMTIuNDYuMjgzLjU5Ny40OS4xMzkuMjA1LjIwOC40MzYuMjA4LjY5NGgtLjk2YzAtLjExNS0uMDMtLjIyLS4wODgtLjMyYS42MS42MSAwIDAgMC0uMjU1LS4yNDIuODc0Ljg3NCAwIDAgMC0uNDMtLjA5Ni45MzQuOTM0IDAgMCAwLS40MS4wOC41NjIuNTYyIDAgMCAwLS4yNC4yLjUwOS41MDkgMCAwIDAtLjAzNi40NjUuNDYuNDYgMCAwIDAgLjE0NC4xNTZjLjA2Ni4wNDUuMTU2LjA4Ny4yNy4xMjcuMTE3LjA0LjI2NC4wNzguNDM5LjExNi4zMy4wNjkuNjEyLjE1OC44NDkuMjY3LjIzOC4xMDYuNDIyLjI0NC41NS40MTQuMTI3LjE2Ny4xOS4zOC4xOS42MzcgMCAuMTkyLS4wNC4zNjctLjEyMy41MjYtLjA4LjE1Ny0uMTk3LjI5NC0uMzUuNDFhMS43NjMgMS43NjMgMCAwIDEtLjU1NC4yNjggMi40OTUgMi40OTUgMCAwIDEtLjcxOC4wOTVjLS4zOSAwLS43Mi0uMDY5LS45OTItLjIwNy0uMjctLjE0LS40NzYtLjMyLS42MTctLjUzOGExLjI3MiAxLjI3MiAwIDAgMS0uMjA3LS42ODVoLjkyOGMuMDEuMTc4LjA2LjMyLjE0Ny40MjYuMDkuMTA0LjIwMi4xOC4zMzUuMjI3LjEzNi4wNDYuMjc1LjA2OC40MTguMDY4LjE3MyAwIC4zMTgtLjAyMy40MzUtLjA2OGEuNjI0LjYyNCAwIDAgMCAuMjY3LS4xOS40NTYuNDU2IDAgMCAwIC4wOTEtLjI4Wm0yLjg5MS0yLjMxNHY1LjEzOWgtLjk2di01Ljk2OGguODg0bC4wNzYuODI5Wm0yLjgwOSAxLjI4NnYuMDg0YzAgLjMxMy0uMDM3LjYwNC0uMTEyLjg3Mi0uMDcxLjI2Ni0uMTc5LjQ5OC0uMzIyLjY5OC0uMTQxLjE5Ni0uMzE1LjM0OS0uNTIyLjQ1OGExLjUxOSAxLjUxOSAwIDAgMS0uNzE3LjE2M2MtLjI2OSAwLS41MDQtLjA0OS0uNzA2LS4xNDdhMS40NDYgMS40NDYgMCAwIDEtLjUwNi0uNDI2IDIuMzE2IDIuMzE2IDAgMCAxLS4zMzQtLjY0NiA0LjEzNiA0LjEzNiAwIDAgMS0uMTc2LS44MnYtLjMyM2MuMDM1LS4zMTYuMDkzLS42MDMuMTc2LS44Ni4wODUtLjI1OC4xOTYtLjQ4LjMzNC0uNjY2LjEzOC0uMTg2LjMwNy0uMzMuNTA2LS40My4yLS4xMDIuNDMyLS4xNTIuNjk3LS4xNTIuMjcyIDAgLjUxMi4wNTMuNzIyLjE2LjIxLjEwMy4zODYuMjUyLjUzLjQ0Ni4xNDMuMTkuMjUuNDIyLjMyMi42OTMuMDcyLjI2OC4xMDguNTY3LjEwOC44OTZabS0uOTYuMDg0di0uMDg0YzAtLjE5OS0uMDE5LS4zODMtLjA1Ni0uNTUzYTEuNDQ3IDEuNDQ3IDAgMCAwLS4xNzUtLjQ1NS44NTkuODU5IDAgMCAwLS4zMDctLjMwMi44MzUuODM1IDAgMCAwLS40NDItLjExMmMtLjE3IDAtLjMxNy4wMy0uNDM5LjA4OGEuODQxLjg0MSAwIDAgMC0uMzA2LjIzNWMtLjA4My4xLS4xNDcuMjE5LS4xOTIuMzU0YTIuMTIyIDIuMTIyIDAgMCAwLS4wOTUuNDM1di43NzNjLjAzMi4xOS4wODYuMzY2LjE2My41MjUuMDc3LjE2LjE4Ni4yODcuMzI3LjM4My4xNDMuMDkzLjMyNi4xNC41NS4xNC4xNzIgMCAuMzItLjAzOC40NDItLjExMmEuODcxLjg3MSAwIDAgMCAuMjk5LS4zMDdjLjA4LS4xMzMuMTM4LS4yODUuMTc1LS40NTguMDM3LS4xNzMuMDU2LS4zNTYuMDU2LS41NVptMS43MzkuMDA0di0uMDkyYzAtLjMxLjA0NS0uNTk5LjEzNi0uODY0LjA5LS4yNjguMjItLjUuMzktLjY5Ny4xNzMtLjIuMzgyLS4zNTQuNjMtLjQ2M2EyLjA1IDIuMDUgMCAwIDEgLjg0NC0uMTY3Yy4zMTYgMCAuNTk4LjA1Ni44NDUuMTY3LjI1LjExLjQ2LjI2My42MzMuNDYzLjE3My4xOTYuMzA0LjQyOC4zOTUuNjk3LjA5LjI2NS4xMzUuNTU0LjEzNS44NjR2LjA5MmMwIC4zMS0uMDQ1LjU5OS0uMTM1Ljg2NC0uMDkuMjY2LS4yMjIuNDk5LS4zOTUuNjk4LS4xNzIuMTk2LS4zODIuMzUtLjYzLjQ2MmEyLjA2NCAyLjA2NCAwIDAgMS0uODQuMTYzYy0uMzE2IDAtLjU5OS0uMDU0LS44NDktLjE2M2ExLjgyOCAxLjgyOCAwIDAgMS0uNjMtLjQ2MiAyLjA2OSAyLjA2OSAwIDAgMS0uMzk0LS42OTcgMi42NyAyLjY3IDAgMCAxLS4xMzUtLjg2NVptLjk2LS4wOTJ2LjA5MmMwIC4xOTQuMDIuMzc3LjA2LjU1LjA0LjE3Mi4xMDIuMzI0LjE4Ny40NTRzLjE5NC4yMzIuMzI3LjMwN2MuMTMzLjA3NC4yOS4xMTEuNDc0LjExMWEuOTE3LjkxNyAwIDAgMCAuNDYyLS4xMTEuOTI3LjkyNyAwIDAgMCAuMzI3LS4zMDdjLjA4NS0uMTMuMTQ3LS4yODIuMTg3LS40NTQuMDQzLS4xNzMuMDY0LS4zNTYuMDY0LS41NXYtLjA5MmMwLS4xOS0uMDIxLS4zNzItLjA2NC0uNTQxYTEuMzkgMS4zOSAwIDAgMC0uMTkxLS40NTkuOTEzLjkxMyAwIDAgMC0uNzkzLS40MjYuOTIuOTIgMCAwIDAtLjQ3LjExNi45MjUuOTI1IDAgMCAwLS4zMjMuMzEgMS40NDUgMS40NDUgMCAwIDAtLjE4Ny40NTljLS4wNC4xNy0uMDYuMzUtLjA2LjU0MVptNC45NjMtMS4yOXYzLjQ5aC0uOTZ2LTQuMzExaC45MTZsLjA0NC44MlptMS4zMTktLjg1LS4wMDguODkzYTIuNTAzIDIuNTAzIDAgMCAwLS4zOS0uMDMyYy0uMTY2IDAtLjMxLjAyNC0uNDM1LjA3MmEuODE4LjgxOCAwIDAgMC0uNTA2LjUxIDEuMzkzIDEuMzkzIDAgMCAwLS4wOC40MWwtLjIxOS4wMTZjMC0uMjcuMDI3LS41MjIuMDgtLjc1My4wNTMtLjIzLjEzMy0uNDM0LjIzOS0uNjEuMTA5LS4xNzQuMjQ0LS4zMTEuNDA2LS40MWExLjA5IDEuMDkgMCAwIDEgLjU3LS4xNDcgMS4xOSAxLjE5IDAgMCAxIC4zNDMuMDUyWm0yLjkyMi4wMjl2LjcwMUg0My40di0uNzAxaDIuNDNabS0xLjcyOS0xLjA1NmguOTZ2NC4xNzVhLjY4LjY4IDAgMCAwIC4wNTYuMzA3Yy4wNC4wNy4wOTQuMTE2LjE2My4xNC4wNy4wMjMuMTUuMDM1LjI0My4wMzVhMS40OTIgMS40OTIgMCAwIDAgLjMzOS0uMDM1bC4wMDQuNzMzYy0uMDguMDI0LS4xNzMuMDQ1LS4yNzkuMDYzYTIuMDQ3IDIuMDQ3IDAgMCAxLS4zNTkuMDI4Yy0uMjIgMC0uNDE1LS4wMzgtLjU4NS0uMTE1YS44NjIuODYyIDAgMCAxLS4zOTktLjM4N2MtLjA5NS0uMTc4LS4xNDMtLjQxNC0uMTQzLS43MDl2LTQuMjM1Wm03LjQyMyA0LjQ3NFY0OS40NGguOTY0djYuMTJoLS44NzJsLS4wOTItLjg5M1ptLTIuODA1LTEuMjE1di0uMDg0YzAtLjMyNi4wMzktLjYyNC4xMTYtLjg5Mi4wNzctLjI3MS4xODgtLjUwNC4zMzQtLjY5N2ExLjQ3IDEuNDcgMCAwIDEgLjUzNC0uNDVjLjIxLS4xMDcuNDQ3LS4xNi43MS0uMTYuMjYgMCAuNDg4LjA1LjY4NS4xNTEuMTk2LjEwMS4zNjQuMjQ2LjUwMi40MzUuMTM4LjE4Ni4yNDguNDA5LjMzLjY3LjA4My4yNTcuMTQxLjU0NC4xNzYuODZ2LjI2N2MtLjAzNS4zMDgtLjA5My41OS0uMTc2Ljg0NGEyLjI2OCAyLjI2OCAwIDAgMS0uMzMuNjYyIDEuNDMgMS40MyAwIDAgMS0uNTA2LjQzIDEuNDkgMS40OSAwIDAgMS0uNjkuMTUxYy0uMjYgMC0uNDk1LS4wNTQtLjcwNS0uMTYzYTEuNTYgMS41NiAwIDAgMS0uNTMtLjQ1OCAyLjE2IDIuMTYgMCAwIDEtLjMzNC0uNjk0IDMuMTUyIDMuMTUyIDAgMCAxLS4xMTYtLjg3MlptLjk2LS4wODR2LjA4NGMwIC4xOTcuMDE4LjM4LjA1Mi41NS4wMzcuMTcuMDk0LjMyLjE3Mi40NWEuODgxLjg4MSAwIDAgMCAuMjk4LjMwMy44ODEuODgxIDAgMCAwIC40NDcuMTA3LjkzNy45MzcgMCAwIDAgLjUzNy0uMTQzLjk4Ljk4IDAgMCAwIC4zMzEtLjM4N2MuMDgyLS4xNjQuMTM4LS4zNDguMTY3LS41NXYtLjcyYTEuNzYxIDEuNzYxIDAgMCAwLS4xLS40MzkgMS4xNzIgMS4xNzIgMCAwIDAtLjE5NC0uMzU0LjgyMy44MjMgMCAwIDAtLjMwNy0uMjQuOTYyLjk2MiAwIDAgMC0uNDI2LS4wODcuODQyLjg0MiAwIDAgMC0uNDQ3LjExMi45MDMuOTAzIDAgMCAwLS4zMDMuMzA2IDEuNTEgMS41MSAwIDAgMC0uMTcuNDU0IDIuNjMzIDIuNjMzIDAgMCAwLS4wNTcuNTU0Wm02LjM5IDEuMzI3di0yLjA1NmEuODgyLjg4MiAwIDAgMC0uMDg1LS4zOTguNTg2LjU4NiAwIDAgMC0uMjU0LS4yNi44NzIuODcyIDAgMCAwLS40MjMtLjA5Ljk1Ni45NTYgMCAwIDAtLjQwNi4wNzkuNjU3LjY1NyAwIDAgMC0uMjY3LjIxNS41Mi41MiAwIDAgMC0uMDk2LjMwN2gtLjk1NmMwLS4xNy4wNDEtLjMzNS4xMjQtLjQ5NC4wODItLjE2LjIwMS0uMzAyLjM1OC0uNDI2LjE1Ny0uMTI1LjM0NC0uMjI0LjU2Mi0uMjk1LjIxOC0uMDcyLjQ2Mi0uMTA4LjczMy0uMTA4LjMyNCAwIC42MS4wNTQuODYuMTYzLjI1My4xMS40NS4yNzQuNTk0LjQ5NC4xNDYuMjE4LjIyLjQ5Mi4yMi44MjF2MS45MTdjMCAuMTk2LjAxMy4zNzMuMDQuNTMuMDI4LjE1My4wNy4yODguMTIzLjQwMnYuMDY0aC0uOTg0YTEuNzAyIDEuNzAyIDAgMCAxLS4xMDgtLjM5NSAzLjIyNCAzLjIyNCAwIDAgMS0uMDM2LS40N1ptLjEzOS0xLjc1Ny4wMDguNTk0aC0uNjlhMS45MSAxLjkxIDAgMCAwLS40Ny4wNTIuOTYzLjk2MyAwIDAgMC0uMzM4LjE0My42MjMuNjIzIDAgMCAwLS4yNzEuNTM4YzAgLjExNC4wMjYuMjE5LjA4LjMxNC4wNTMuMDkzLjEzLjE2Ni4yMy4yMi4xMDQuMDUzLjIyOS4wOC4zNzUuMDhhMS4wNTcgMS4wNTcgMCAwIDAgLjg2NC0uNDE5LjY1Mi42NTIgMCAwIDAgLjEzNi0uMzM5bC4zMS40MjdhMS40NiAxLjQ2IDAgMCAxLS4xNjMuMzUgMS43IDEuNyAwIDAgMS0uMzAyLjM1OSAxLjUwMyAxLjUwMyAwIDAgMS0xLjAzMi4zODJjLS4yODIgMC0uNTMzLS4wNTUtLjc1My0uMTY3YTEuMzQgMS4zNCAwIDAgMS0uNTE4LS40NTggMS4xODkgMS4xODkgMCAwIDEtLjE4OC0uNjU4YzAtLjIyOC4wNDMtLjQzLjEyOC0uNjA1LjA4OC0uMTc4LjIxNS0uMzI3LjM4My0uNDQ2LjE3LS4xMi4zNzctLjIxLjYyMS0uMjcxLjI0NC0uMDY0LjUyMy0uMDk2LjgzNy0uMDk2aC43NTNabTMuOTUtMS42OXYuNzAyaC0yLjQzdi0uNzAxaDIuNDNabS0xLjcyOS0xLjA1NWguOTZ2NC4xNzVhLjY4LjY4IDAgMCAwIC4wNTYuMzA3Yy4wNC4wNy4wOTQuMTE2LjE2My4xNC4wNy4wMjMuMTUuMDM1LjI0My4wMzVhMS40OTIgMS40OTIgMCAwIDAgLjM0LS4wMzVsLjAwMy43MzNjLS4wOC4wMjQtLjE3My4wNDUtLjI3OS4wNjNhMi4wNDcgMi4wNDcgMCAwIDEtLjM1OC4wMjhjLS4yMiAwLS40MTYtLjAzOC0uNTg2LS4xMTVhLjg2Mi44NjIgMCAwIDEtLjM5OC0uMzg3Yy0uMDk2LS4xNzgtLjE0NC0uNDE0LS4xNDQtLjcwOXYtNC4yMzVabTUuMDQ2IDQuNTAydi0yLjA1NmEuODgyLjg4MiAwIDAgMC0uMDgzLS4zOTguNTg2LjU4NiAwIDAgMC0uMjU1LS4yNi44NzIuODcyIDAgMCAwLS40MjMtLjA5Ljk1Ny45NTcgMCAwIDAtLjQwNi4wNzkuNjU2LjY1NiAwIDAgMC0uMjY3LjIxNS41Mi41MiAwIDAgMC0uMDk2LjMwN2gtLjk1NmMwLS4xNy4wNDEtLjMzNS4xMjQtLjQ5NGExLjMyIDEuMzIgMCAwIDEgLjM1OC0uNDI2IDEuNzkgMS43OSAwIDAgMSAuNTYyLS4yOTVjLjIxOC0uMDcyLjQ2Mi0uMTA4LjczMy0uMTA4LjMyNCAwIC42MTEuMDU0Ljg2LjE2My4yNTMuMTEuNDUuMjc0LjU5NC40OTQuMTQ2LjIxOC4yMi40OTIuMjIuODIxdjEuOTE3YzAgLjE5Ni4wMTMuMzczLjA0LjUzLjAyOC4xNTMuMDcuMjg4LjEyMy40MDJ2LjA2NGgtLjk4NGExLjcwNiAxLjcwNiAwIDAgMS0uMTA4LS4zOTUgMy4yMjMgMy4yMjMgMCAwIDEtLjAzNi0uNDdabS4xNC0xLjc1Ny4wMDguNTk0aC0uNjlhMS45MSAxLjkxIDAgMCAwLS40Ny4wNTIuOTYzLjk2MyAwIDAgMC0uMzM4LjE0My42MjIuNjIyIDAgMCAwLS4yNzEuNTM4YzAgLjExNC4wMjYuMjE5LjA4LjMxNC4wNTMuMDkzLjEzLjE2Ni4yMy4yMi4xMDQuMDUzLjIyOS4wOC4zNzUuMDhhMS4wNTcgMS4wNTcgMCAwIDAgLjg2NS0uNDE5LjY1LjY1IDAgMCAwIC4xMzUtLjMzOWwuMzEuNDI3YTEuNDU2IDEuNDU2IDAgMCAxLS4xNjMuMzUgMS42OTYgMS42OTYgMCAwIDEtLjMwMi4zNTkgMS41MDMgMS41MDMgMCAwIDEtMS4wMzIuMzgyYy0uMjgyIDAtLjUzMy0uMDU1LS43NTMtLjE2N2ExLjM0IDEuMzQgMCAwIDEtLjUxOC0uNDU4IDEuMTg5IDEuMTg5IDAgMCAxLS4xODctLjY1OGMwLS4yMjguMDQyLS40My4xMjctLjYwNS4wODgtLjE3OC4yMTUtLjMyNy4zODMtLjQ0Ni4xNy0uMTIuMzc3LS4yMS42MjEtLjI3MS4yNDQtLjA2NC41MjMtLjA5Ni44MzctLjA5NmguNzUzWm0tNTIuODMyIDEwLjE0djUuMTM5aC0uOTZ2LTUuOTY4aC44ODVsLjA3NS44MjlabTIuODEgMS4yODZ2LjA4NGMwIC4zMTMtLjAzOC42MDQtLjExMi44NzMtLjA3Mi4yNjUtLjE4LjQ5OC0uMzIzLjY5Ny0uMTQuMTk2LS4zMTUuMzQ5LS41MjIuNDU4YTEuNTE5IDEuNTE5IDAgMCAxLS43MTcuMTYzYy0uMjY4IDAtLjUwNC0uMDQ5LS43MDUtLjE0N2ExLjQ0NiAxLjQ0NiAwIDAgMS0uNTA2LS40MjYgMi4zMTcgMi4zMTcgMCAwIDEtLjMzNS0uNjQ2IDQuMTUgNC4xNSAwIDAgMS0uMTc1LS44MnYtLjMyM2MuMDM0LS4zMTYuMDkzLS42MDMuMTc1LS44Ni4wODUtLjI1OC4xOTctLjQ4LjMzNS0uNjY2LjEzOC0uMTg2LjMwNi0uMzMuNTA2LS40My4xOTktLjEwMi40MzEtLjE1Mi42OTctLjE1Mi4yNyAwIC41MTEuMDUzLjcyMS4xNi4yMS4xMDMuMzg2LjI1Mi41My40NDYuMTQzLjE5LjI1LjQyMi4zMjMuNjkzLjA3MS4yNjguMTA3LjU2Ny4xMDcuODk2Wm0tLjk2MS4wODR2LS4wODRjMC0uMTk5LS4wMTktLjM4My0uMDU2LS41NTNhMS40NDUgMS40NDUgMCAwIDAtLjE3NS0uNDU1Ljg1OC44NTggMCAwIDAtLjMwNy0uMzAyLjgzNS44MzUgMCAwIDAtLjQ0Mi0uMTEyYy0uMTcgMC0uMzE2LjAzLS40MzguMDg4YS44NC44NCAwIDAgMC0uMzA3LjIzNWMtLjA4Mi4xLS4xNDYuMjE5LS4xOTEuMzU0YTIuMTIyIDIuMTIyIDAgMCAwLS4wOTYuNDM1di43NzNjLjAzMi4xOS4wODYuMzY2LjE2My41MjUuMDc3LjE2LjE4Ni4yODcuMzI3LjM4My4xNDQuMDkzLjMyNy4xNC41NS4xNC4xNzIgMCAuMzItLjAzOC40NDItLjExMmEuODcxLjg3MSAwIDAgMCAuMjk5LS4zMDdjLjA4LS4xMzMuMTM4LS4yODUuMTc1LS40NTguMDM3LS4xNzMuMDU2LS4zNTYuMDU2LS41NVptMS43NC4wMDR2LS4wOTJjMC0uMzEuMDQ0LS41OTkuMTM1LS44NjQuMDktLjI2OC4yMi0uNS4zOS0uNjk3LjE3My0uMi4zODMtLjM1NC42My0uNDYzYTIuMDUgMi4wNSAwIDAgMSAuODQ0LS4xNjdjLjMxNyAwIC41OTguMDU2Ljg0NS4xNjcuMjUuMTEuNDYuMjYzLjYzMy40NjMuMTczLjE5Ni4zMDUuNDI4LjM5NS42OTcuMDkuMjY1LjEzNS41NTQuMTM1Ljg2NHYuMDkyYzAgLjMxLS4wNDUuNTk5LS4xMzUuODY1LS4wOS4yNjUtLjIyMi40OTgtLjM5NS42OTctLjE3Mi4xOTYtLjM4Mi4zNS0uNjI5LjQ2MmEyLjA2NCAyLjA2NCAwIDAgMS0uODQuMTYzYy0uMzE3IDAtLjYtLjA1NC0uODUtLjE2M2ExLjgyNyAxLjgyNyAwIDAgMS0uNjI5LS40NjIgMi4wNjkgMi4wNjkgMCAwIDEtLjM5NC0uNjk4IDIuNjcgMi42NyAwIDAgMS0uMTM2LS44NjRabS45Ni0uMDkydi4wOTJjMCAuMTk0LjAyLjM3Ny4wNi41NWExLjQgMS40IDAgMCAwIC4xODcuNDU0Yy4wODUuMTMuMTk0LjIzMi4zMjYuMzA3LjEzMy4wNzQuMjkxLjExMS40NzQuMTExYS45MTcuOTE3IDAgMCAwIC40NjMtLjExMS45MjcuOTI3IDAgMCAwIC4zMjYtLjMwNyAxLjQgMS40IDAgMCAwIC4xODgtLjQ1NGMuMDQyLS4xNzMuMDYzLS4zNTYuMDYzLS41NXYtLjA5MmMwLS4xOS0uMDIxLS4zNzEtLjA2NC0uNTQxYTEuMzkyIDEuMzkyIDAgMCAwLS4xOS0uNDU5LjkxMy45MTMgMCAwIDAtLjc5NC0uNDI2LjkyLjkyIDAgMCAwLS40Ny4xMTYuOTI0LjkyNCAwIDAgMC0uMzIyLjMxIDEuNDQ3IDEuNDQ3IDAgMCAwLS4xODguNDU5Yy0uMDQuMTctLjA2LjM1LS4wNi41NDFabTUuMDI2LTIuMTExdjQuMzFoLS45NjR2LTQuMzFoLjk2NFptLTEuMDI4LTEuMTMyYzAtLjE0Ni4wNDgtLjI2Ny4xNDMtLjM2MmEuNTQ5LjU0OSAwIDAgMSAuNDA3LS4xNDhjLjE3IDAgLjMwNC4wNS40MDIuMTQ4YS40ODQuNDg0IDAgMCAxIC4xNDguMzYyLjQ4LjQ4IDAgMCAxLS4xNDguMzU5LjU1Mi41NTIgMCAwIDEtLjQwMi4xNDMuNTU3LjU1NyAwIDAgMS0uNDA3LS4xNDMuNDg2LjQ4NiAwIDAgMS0uMTQzLS4zNTlabTMuMTc4IDIuMDUydjMuMzloLS45NnYtNC4zMWguOTA0bC4wNTYuOTJabS0uMTcyIDEuMDc2LS4zMS0uMDA0YTIuOCAyLjggMCAwIDEgLjEyNy0uODQgMi4wNyAyLjA3IDAgMCAxIC4zNS0uNjU4Yy4xNTItLjE4My4zMzMtLjMyNC41NDItLjQyMi4yMS0uMTAyLjQ0NC0uMTUyLjcwMi0uMTUyLjIwNyAwIC4zOTQuMDMuNTYxLjA4OC4xNy4wNTYuMzE1LjE0Ny40MzUuMjc1LjEyMi4xMjcuMjE1LjI5My4yNzkuNDk4LjA2My4yMDEuMDk1LjQ1LjA5NS43NDV2Mi43ODVoLS45NjR2LTIuNzljMC0uMjA2LS4wMy0uMzctLjA5Mi0uNDlhLjUxMy41MTMgMCAwIDAtLjI1OS0uMjU4Ljk3Mi45NzIgMCAwIDAtLjQxOC0uMDguOTI5LjkyOSAwIDAgMC0uNDQyLjEwNC45OTUuOTk1IDAgMCAwLS4zMy4yODIgMS4zNyAxLjM3IDAgMCAwLS4yMDQuNDE1IDEuNzA5IDEuNzA5IDAgMCAwLS4wNzIuNTAyWm01Ljg4My0xLjk5NnYuNzAxaC0yLjQzdi0uNzAxaDIuNDNabS0xLjcyOS0xLjA1NmguOTZ2NC4xNzVjMCAuMTMzLjAxOS4yMzUuMDU2LjMwNy4wNC4wNy4wOTQuMTE2LjE2My4xNC4wNy4wMjQuMTUuMDM1LjI0My4wMzVhMS40OTIgMS40OTIgMCAwIDAgLjMzOS0uMDM1bC4wMDQuNzMzYy0uMDguMDIzLS4xNzMuMDQ1LS4yNzkuMDYzYTIuMDQ3IDIuMDQ3IDAgMCAxLS4zNTguMDI4Yy0uMjIxIDAtLjQxNi0uMDM4LS41ODYtLjExNWEuODYyLjg2MiAwIDAgMS0uMzk5LS4zODdjLS4wOTUtLjE3OC0uMTQzLS40MTQtLjE0My0uNzA5di00LjIzNVptNS4wMzQgNC4yYS40OC40OCAwIDAgMC0uMDcyLS4yNmMtLjA0Ny0uMDgtLjEzOS0uMTUxLS4yNzQtLjIxNWEyLjY2NCAyLjY2NCAwIDAgMC0uNTktLjE3NSA1LjA2MyA1LjA2MyAwIDAgMS0uNjMtLjE4IDEuOTk4IDEuOTk4IDAgMCAxLS40ODYtLjI1OC45OTMuOTkzIDAgMCAxLS40MjYtLjgzN2MwLS4xNzUuMDM5LS4zNDEuMTE2LS40OTguMDc3LS4xNTcuMTg3LS4yOTUuMzMtLjQxNWExLjYxIDEuNjEgMCAwIDEgLjUyMi0uMjgyYy4yMDctLjA3LjQzOS0uMTA0LjY5NC0uMTA0LjM2IDAgLjY3LjA2MS45MjguMTgzLjI2LjEyLjQ2LjI4My41OTcuNDkuMTM5LjIwNS4yMDguNDM2LjIwOC42OTRoLS45NmMwLS4xMTUtLjAzLS4yMi0uMDg4LS4zMmEuNjEuNjEgMCAwIDAtLjI1NS0uMjQyLjg3NC44NzQgMCAwIDAtLjQzLS4wOTYuOTM0LjkzNCAwIDAgMC0uNDEuMDguNTYyLjU2MiAwIDAgMC0uMjQuMi41MDkuNTA5IDAgMCAwLS4wMzYuNDY1Yy4wMy4wNTYuMDc3LjEwOC4xNDQuMTU2LjA2Ni4wNDUuMTU2LjA4Ny4yNy4xMjcuMTE3LjA0LjI2NC4wNzguNDM5LjExNmE0IDQgMCAwIDEgLjg0OC4yNjdjLjI0LjEwNi40MjMuMjQ0LjU1LjQxNC4xMjguMTY3LjE5MS4zOC4xOTEuNjM3IDAgLjE5Mi0uMDQuMzY3LS4xMjMuNTI2LS4wOC4xNTctLjE5Ny4yOTQtLjM1LjQxYTEuNzYzIDEuNzYzIDAgMCAxLS41NTQuMjY4IDIuNDk1IDIuNDk1IDAgMCAxLS43MTguMDk1Yy0uMzkgMC0uNzItLjA2OS0uOTkyLS4yMDctLjI3LS4xNC0uNDc2LS4zMi0uNjE3LS41MzhhMS4yNzMgMS4yNzMgMCAwIDEtLjIwNy0uNjg1aC45MjhjLjAxLjE3OC4wNi4zMi4xNDcuNDI2LjA5LjEwNC4yMDIuMTguMzM1LjIyNy4xMzYuMDQ1LjI3NS4wNjguNDE4LjA2OC4xNzMgMCAuMzE4LS4wMjMuNDM1LS4wNjhhLjYyNC42MjQgMCAwIDAgLjI2Ny0uMTkuNDU2LjQ1NiAwIDAgMCAuMDkxLS4yOFoiLz48L2c+PGcgY2xpcC1wYXRoPSJ1cmwoI2gpIj48cGF0aCBmaWxsPSIjMDAwIiBmaWxsLW9wYWNpdHk9Ii41NCIgZD0iTTEwNy41IDQ1LjkxNmguMDU1di41MzdoLS4wNTVjLS4zMzQgMC0uNjE0LjA1NC0uODQuMTYzYTEuMzc3IDEuMzc3IDAgMCAwLS41MzYuNDM0Yy0uMTMyLjE4LS4yMjcuMzgzLS4yODcuNjA4YTIuNzg5IDIuNzg5IDAgMCAwLS4wODUuNjg2di43MzFjMCAuMjIxLjAyNi40MTcuMDc5LjU4OC4wNTIuMTY4LjEyNC4zMS4yMTUuNDI3YS45NDYuOTQ2IDAgMCAwIC4zMDcuMjYyYy4xMTYuMDYuMjM3LjA5LjM2Mi4wOWEuODg4Ljg4OCAwIDAgMCAuMzg5LS4wODMuODIuODIgMCAwIDAgLjI4Ny0uMjM1Yy4wOC0uMTAzLjE0LS4yMjMuMTgxLS4zNjJhMS42MSAxLjYxIDAgMCAwIC4wNjItLjQ1OGMwLS4xNDgtLjAxOS0uMjktLjA1NS0uNDI3YTEuMTMxIDEuMTMxIDAgMCAwLS4xNjctLjM2OS44MDQuODA0IDAgMCAwLS42ODMtLjM1MS45NzguOTc4IDAgMCAwLS40OTIuMTNjLS4xNS4wODQtLjI3NC4xOTUtLjM3Mi4zMzRhLjg5Ljg5IDAgMCAwLS4xNjQuNDQ3bC0uMzM1LS4wMDNjLjAzMi0uMjU1LjA5MS0uNDcyLjE3OC0uNjUyYTEuMzkgMS4zOSAwIDAgMSAuMzI3LS40NDRjLjEzMy0uMTE2LjI3OS0uMi40NDEtLjI1M2ExLjYzIDEuNjMgMCAwIDEgLjUxOS0uMDgyYy4yNDggMCAuNDYyLjA0Ny42NDIuMTQuMTguMDk0LjMyOC4yMTkuNDQ0LjM3Ni4xMTYuMTU1LjIwMi4zMy4yNTYuNTI2LjA1Ny4xOTMuMDg2LjM5Mi4wODYuNTk3IDAgLjIzNS0uMDMzLjQ1NS0uMDk5LjY2YTEuNTYgMS41NiAwIDAgMS0uMjk4LjU0Yy0uMTI5LjE1NC0uMjkuMjc1LS40ODEuMzYxLS4xOTEuMDg3LS40MTMuMTMtLjY2Ni4xMy0uMjY5IDAtLjUwMy0uMDU1LS43MDMtLjE2NGExLjUwMSAxLjUwMSAwIDAgMS0uNDk5LS40NDQgMi4wMjUgMi4wMjUgMCAwIDEtLjI5Ny0uNjE1IDIuNDMgMi40MyAwIDAgMS0uMDk5LS42ODZ2LS4yOTdjMC0uMzUuMDM1LS42OTUuMTA2LTEuMDMyLjA3LS4zMzcuMTkyLS42NDIuMzY1LS45MTUuMTc1LS4yNzMuNDE4LS40OS43MjctLjY1Mi4zMS0uMTYyLjcwNS0uMjQyIDEuMTg1LS4yNDJabTIuMTE1LjAwN2guNjM5bDEuNjI5IDQuMDU0IDEuNjI1LTQuMDU0aC42NDJsLTIuMDIxIDQuOTcyaC0uNDk5bC0yLjAxNS00Ljk3MlptLS4yMDggMGguNTYzbC4wOTMgMy4wMzN2MS45NGgtLjY1NnYtNC45NzNabTQuMzg1IDBoLjU2M3Y0Ljk3MmgtLjY1NXYtMS45NGwuMDkyLTMuMDMyWm02LjAyNiAwLTIuMDczIDUuNGgtLjU0M2wyLjA3Ni01LjRoLjU0Wm00Ljg5OC0uMDI3djVoLS42MzF2LTQuMjExbC0xLjI3NC40NjR2LS41N2wxLjgwNi0uNjgzaC4wOTlabTUuMjEzIDIuMTE3di43NThjMCAuNDA4LS4wMzcuNzUyLS4xMSAxLjAzMi0uMDczLjI4LS4xNzcuNTA1LS4zMTQuNjc2LS4xMzYuMTctLjMwMS4yOTUtLjQ5NS4zNzJhMS43NjUgMS43NjUgMCAwIDEtLjY0OS4xMTNjLS4xOTEgMC0uMzY4LS4wMjQtLjUyOS0uMDcyYTEuMjQ3IDEuMjQ3IDAgMCAxLS40MzctLjIyOSAxLjM4IDEuMzggMCAwIDEtLjMyOC0uNDE2IDIuMjEzIDIuMjEzIDAgMCAxLS4yMDgtLjYyMiA0LjQ2IDQuNDYgMCAwIDEtLjA3Mi0uODU0di0uNzU4YzAtLjQwNy4wMzYtLjc0OS4xMDktMS4wMjQuMDc1LS4yNzYuMTgxLS40OTYuMzE4LS42NjMuMTM2LS4xNjguMy0uMjg5LjQ5MS0uMzYyLjE5NC0uMDczLjQxLS4xMDkuNjQ5LS4xMDkuMTk0IDAgLjM3MS4wMjQuNTMzLjA3MmExLjIgMS4yIDAgMCAxIC43NjIuNjI1Yy4wOTEuMTY2LjE2LjM3LjIwOC42MS4wNDguMjQyLjA3Mi41MjYuMDcyLjg1MVptLS42MzYuODZ2LS45NjZjMC0uMjIzLS4wMTMtLjQxOC0uMDQxLS41ODdhMS44NSAxLjg1IDAgMCAwLS4xMTItLjQzNy44Ny44NyAwIDAgMC0uMTkxLS4yOTQuNjg0LjY4NCAwIDAgMC0uMjYzLS4xNjQuOTUuOTUgMCAwIDAtLjMzMi0uMDU0Ljg5NS44OTUgMCAwIDAtLjM5OS4wODUuNzIyLjcyMiAwIDAgMC0uMjk0LjI2M2MtLjA3Ny4xMi0uMTM3LjI3OS0uMTc4LjQ3NWEzLjYyIDMuNjIgMCAwIDAtLjA2MS43MTN2Ljk2N2MwIC4yMjMuMDEzLjQyLjAzOC41OS4wMjcuMTcxLjA2Ny4zMi4xMTkuNDQ1LjA1Mi4xMjMuMTE2LjIyNC4xOTEuMzAzLjA3NS4wOC4xNjIuMTQuMjYuMTc4LjEuMDM2LjIxLjA1NS4zMzEuMDU1LjE1NSAwIC4yOS0uMDMuNDA3LS4wOWEuNzM3LjczNyAwIDAgMCAuMjktLjI3NiAxLjQ0IDEuNDQgMCAwIDAgLjE3Ny0uNDg4Yy4wMzktLjIuMDU4LS40NC4wNTgtLjcxN1ptNC44MDMtLjg2di43NThjMCAuNDA4LS4wMzcuNzUyLS4xMDkgMS4wMzItLjA3My4yOC0uMTc4LjUwNS0uMzE1LjY3Ni0uMTM2LjE3LS4zMDEuMjk1LS40OTUuMzcyYTEuNzYgMS43NiAwIDAgMS0uNjQ5LjExMyAxLjg2IDEuODYgMCAwIDEtLjUyOS0uMDcyIDEuMjU1IDEuMjU1IDAgMCAxLS40MzctLjIyOSAxLjM4IDEuMzggMCAwIDEtLjMyOC0uNDE2IDIuMjQ1IDIuMjQ1IDAgMCAxLS4yMDgtLjYyMiA0LjQ2IDQuNDYgMCAwIDEtLjA3Mi0uODU0di0uNzU4YzAtLjQwNy4wMzYtLjc0OS4xMDktMS4wMjQuMDc1LS4yNzYuMTgxLS40OTYuMzE4LS42NjMuMTM2LS4xNjguMy0uMjg5LjQ5Mi0uMzYyLjE5My0uMDczLjQwOS0uMTA5LjY0OC0uMTA5LjE5NCAwIC4zNzIuMDI0LjUzMy4wNzJhMS4yIDEuMiAwIDAgMSAuNzYyLjYyNWMuMDkxLjE2Ni4xNi4zNy4yMDguNjEuMDQ4LjI0Mi4wNzIuNTI2LjA3Mi44NTFabS0uNjM1Ljg2di0uOTY2YTMuNzUgMy43NSAwIDAgMC0uMDQxLS41ODcgMS44NDggMS44NDggMCAwIDAtLjExMy0uNDM3Ljg3Ljg3IDAgMCAwLS4xOTEtLjI5NC42NzcuNjc3IDAgMCAwLS4yNjMtLjE2NC45NS45NSAwIDAgMC0uMzMyLS4wNTQuODk1Ljg5NSAwIDAgMC0uMzk5LjA4NS43MjIuNzIyIDAgMCAwLS4yOTQuMjYzYy0uMDc3LjEyLS4xMzYuMjc5LS4xNzcuNDc1YTMuNTM4IDMuNTM4IDAgMCAwLS4wNjIuNzEzdi45NjdjMCAuMjIzLjAxMy40Mi4wMzguNTkuMDI3LjE3MS4wNjcuMzIuMTE5LjQ0NS4wNTMuMTIzLjExNi4yMjQuMTkyLjMwM2EuNzEuNzEgMCAwIDAgLjI1OS4xNzhjLjEuMDM2LjIxMS4wNTUuMzMxLjA1NS4xNTUgMCAuMjkxLS4wMy40MDctLjA5YS43My43MyAwIDAgMCAuMjktLjI3NmMuMDgtLjEyNy4xMzktLjI5LjE3OC0uNDg4LjAzOC0uMi4wNTgtLjQ0LjA1OC0uNzE3Wm0yLjA1My0yLjk1aC42MzlsMS42MjggNC4wNTQgMS42MjYtNC4wNTRoLjY0MmwtMi4wMjIgNC45NzJoLS40OThsLTIuMDE1LTQuOTcyWm0tLjIwOCAwaC41NjNsLjA5MiAzLjAzM3YxLjk0aC0uNjU1di00Ljk3M1ptNC4zODQgMGguNTY0djQuOTcyaC0uNjU2di0xLjk0bC4wOTItMy4wMzJaIi8+PGcgZmlsbD0iIzE5ODAzOCIgY2xpcC1wYXRoPSJ1cmwoI2kpIj48cmVjdCB3aWR0aD0iODYuMDEyIiBoZWlnaHQ9IjQuNjYzIiB4PSIxMDQuNjYzIiB5PSI1Ni4yMjciIGZpbGwtb3BhY2l0eT0iLjA2IiByeD0iMi4zMzEiLz48cGF0aCBkPSJNMTA0LjY2MyA1NS4wNmg4LjAxMnY2Ljk5NGgtOC4wMTJ6Ii8+PC9nPjxwYXRoIGZpbGw9IiMxOTgwMzgiIGQ9Ik0xMDguMzk5IDY5LjY4NXYuNTM2aC0yLjYzM3YtLjUzNmgyLjYzM1ptLTIuNS00LjQzNnY0Ljk3MmgtLjY1OXYtNC45NzJoLjY1OVptMi4xNTEgMi4xMzd2LjUzNmgtMi4yODR2LS41MzZoMi4yODRabS4zMTQtMi4xMzd2LjU0aC0yLjU5OHYtLjU0aDIuNTk4Wm0xLjYyIDIuMDY2djIuOTA2aC0uNjMydi0zLjY5NWguNTk4bC4wMzQuNzg5Wm0tLjE1LjkxOC0uMjYzLS4wMWEyLjIxIDIuMjEgMCAwIDEgLjExMy0uN2MuMDcyLS4yMTYuMTc1LS40MDQuMzA3LS41NjRhMS4zNjkgMS4zNjkgMCAwIDEgMS4wODItLjUwMmMuMTgzIDAgLjM0Ni4wMjUuNDkyLjA3Ni4xNDYuMDQ3LjI3LjEyNS4zNzIuMjMyLjEwNS4xMDcuMTg1LjI0Ni4yMzkuNDE2LjA1NS4xNjkuMDgyLjM3NS4wODIuNjE4djIuNDIyaC0uNjM1di0yLjQyOGMwLS4xOTQtLjAyOC0uMzQ5LS4wODUtLjQ2NWEuNTI2LjUyNiAwIDAgMC0uMjQ5LS4yNTYuODk5Ljg5OSAwIDAgMC0uNDAzLS4wODIuOTM4LjkzOCAwIDAgMC0uNzYyLjM3MmMtLjA5MS4xMTctLjE2My4yNS0uMjE1LjQtLjA1LjE0OC0uMDc1LjMwNS0uMDc1LjQ3MVptNS43OTYgMS4zNTZ2LTEuOTAyYS43NzMuNzczIDAgMCAwLS4wODktLjM4LjU4MS41ODEgMCAwIDAtLjI1OS0uMjUyLjk0Ni45NDYgMCAwIDAtLjQzMS0uMDg5Yy0uMTU5IDAtLjI5OS4wMjgtLjQyLjA4MmEuNzQuNzQgMCAwIDAtLjI4LjIxNS40NzIuNDcyIDAgMCAwLS4wOTkuMjg3aC0uNjMyYS44NC44NCAwIDAgMSAuMTAzLS4zOTIgMS4xNCAxLjE0IDAgMCAxIC4yOTQtLjM1MmMuMTI5LS4xMDcuMjg0LS4xOTEuNDY0LS4yNTMuMTgyLS4wNjQuMzg1LS4wOTYuNjA4LS4wOTYuMjY4IDAgLjUwNS4wNDYuNzEuMTM3LjIwNy4wOTEuMzY5LjIyOS40ODUuNDEzLjExOC4xODIuMTc4LjQxMS4xNzguNjg3djEuNzJjMCAuMTI0LjAxLjI1NS4wMy4zOTQuMDIzLjEzOC4wNTYuMjU4LjA5OS4zNTh2LjA1NWgtLjY1OWExLjE2NSAxLjE2NSAwIDAgMS0uMDc1LS4yOSAyLjM2NyAyLjM2NyAwIDAgMS0uMDI3LS4zNDJabS4xMDktMS42MDguMDA3LjQ0M2gtLjYzOWMtLjE3OSAwLS4zNC4wMTUtLjQ4MS4wNDUtLjE0MS4wMjctLjI2LjA3LS4zNTUuMTI2YS41NzEuNTcxIDAgMCAwLS4yOTQuNTEyYzAgLjExNy4wMjYuMjIyLjA3OS4zMThhLjU3Mi41NzIgMCAwIDAgLjIzNS4yMjkuODU2Ljg1NiAwIDAgMCAuMzkzLjA4MiAxLjA3MSAxLjA3MSAwIDAgMCAuODY0LS40MjQuNjM4LjYzOCAwIDAgMCAuMTQzLS4zNDVsLjI3LjMwNGEuOTEuOTEgMCAwIDEtLjEzLjMxOCAxLjUxMSAxLjUxMSAwIDAgMS0uNy41OTggMS4zNTQgMS4zNTQgMCAwIDEtLjUzOS4xMDJjLS4yNTEgMC0uNDctLjA0OS0uNjU5LS4xNDdhMS4xMTkgMS4xMTkgMCAwIDEtLjQzNy0uMzkzIDEuMDM1IDEuMDM1IDAgMCAxLS4xNTQtLjU1NmMwLS4xOTguMDM5LS4zNzIuMTE2LS41MjMuMDc3LS4xNTIuMTg5LS4yNzkuMzM1LS4zNzkuMTQ1LS4xMDIuMzIxLS4xOC41MjYtLjIzMi4yMDQtLjA1Mi40MzMtLjA3OC42ODYtLjA3OGguNzM0Wm0xLjc0Ni0zLjAwNmguNjM1djQuNTI5bC0uMDU0LjcxN2gtLjU4MXYtNS4yNDZabTMuMTMyIDMuMzY4di4wNzFjMCAuMjY5LS4wMzIuNTE4LS4wOTYuNzQ4YTEuODQ0IDEuODQ0IDAgMCAxLS4yOC41OTRjLS4xMjMuMTY5LS4yNzMuMy0uNDUxLjM5My0uMTc3LjA5My0uMzgxLjE0LS42MTEuMTQtLjIzNSAwLS40NDEtLjA0LS42MTgtLjEyYTEuMjIgMS4yMiAwIDAgMS0uNDQ0LS4zNTEgMS43OTIgMS43OTIgMCAwIDEtLjI5LS41NTMgMy40NCAzLjQ0IDAgMCAxLS4xNDctLjczMXYtLjMxNWMuMDI3LS4yNzMuMDc2LS41MTcuMTQ3LS43MzQuMDcyLS4yMTYuMTY5LS40LjI5LS41NTMuMTIxLS4xNTUuMjY5LS4yNzIuNDQ0LS4zNTIuMTc1LS4wODIuMzc5LS4xMjMuNjExLS4xMjMuMjMyIDAgLjQzOC4wNDYuNjE4LjEzNy4xOC4wODkuMzMuMjE2LjQ1MS4zODMuMTIzLjE2Ni4yMTYuMzY1LjI4LjU5Ny4wNjQuMjMuMDk2LjQ4Ni4wOTYuNzY5Wm0tLjYzNi4wNzF2LS4wNzJhMi41MiAyLjUyIDAgMCAwLS4wNTEtLjUxOSAxLjM0NCAxLjM0NCAwIDAgMC0uMTY0LS40My44MTUuODE1IDAgMCAwLS4yOTctLjI5NC44NzcuODc3IDAgMCAwLS40NTQtLjEwOS45OTEuOTkxIDAgMCAwLS40MTcuMDgyLjkuOSAwIDAgMC0uMjk3LjIyMiAxLjE2NyAxLjE2NyAwIDAgMC0uMjAxLjMxNCAxLjgxIDEuODEgMCAwIDAtLjExMy4zNjJ2LjgyM2MuMDM3LjE2LjA5Ni4zMTMuMTc4LjQ2MS4wODQuMTQ2LjE5Ni4yNjUuMzM0LjM1OWEuOTMuOTMgMCAwIDAgLjUyMy4xNC44NzQuODc0IDAgMCAwIC40MzctLjEwMy44MjMuODIzIDAgMCAwIC4yOTctLjI5Yy4wNzgtLjEyMy4xMzQtLjI2NS4xNzEtLjQyNy4wMzYtLjE2MS4wNTQtLjMzNC4wNTQtLjUxOVptMi4zNTQtMy40Mzl2NS4yNDZoLS42MzV2LTUuMjQ2aC42MzVabTIuNzgxIDUuMzE0Yy0uMjU3IDAtLjQ5MS0uMDQzLS43LS4xM2ExLjU4NSAxLjU4NSAwIDAgMS0uNTM2LS4zNzIgMS42NjEgMS42NjEgMCAwIDEtLjM0Mi0uNTY3IDIuMDk1IDIuMDk1IDAgMCAxLS4xMTktLjcxN3YtLjE0NGMwLS4zLjA0NC0uNTY3LjEzMy0uODAyLjA4OS0uMjM3LjIwOS0uNDM3LjM2Mi0uNjAxYTEuNTQgMS41NCAwIDAgMSAuNTE5LS4zNzJjLjE5NC0uMDg1LjM5NC0uMTI3LjYwMS0uMTI3LjI2NCAwIC40OTIuMDQ2LjY4My4xMzcuMTk0LjA5MS4zNTIuMjE5LjQ3NS4zODMuMTIzLjE2MS4yMTQuMzUyLjI3My41NzMuMDU5LjIxOS4wODkuNDU4LjA4OS43MTd2LjI4NGgtMi43NnYtLjUxNmgyLjEyOHYtLjA0OGExLjU1IDEuNTUgMCAwIDAtLjEwMy0uNDc4Ljg0Ny44NDcgMCAwIDAtLjI3My0uMzgyYy0uMTI1LS4xLS4yOTYtLjE1LS41MTItLjE1YS44NjEuODYxIDAgMCAwLS43MDcuMzU4IDEuMzMzIDEuMzMzIDAgMCAwLS4yMDEuNDM0IDIuMTkgMi4xOSAwIDAgMC0uMDcyLjU5di4xNDRjMCAuMTc1LjAyNC4zNC4wNzIuNDk1LjA1LjE1My4xMjEuMjg3LjIxNS40MDMuMDk1LjExNi4yMS4yMDcuMzQ1LjI3My4xMzYuMDY2LjI5MS4xLjQ2NC4xLjIyMyAwIC40MTItLjA0Ni41NjctLjEzNy4xNTUtLjA5MS4yOS0uMjEzLjQwNi0uMzY2bC4zODMuMzA0Yy0uMDguMTItLjE4MS4yMzYtLjMwNC4zNDUtLjEyMy4xMS0uMjc0LjE5OC0uNDU0LjI2N2ExLjc2IDEuNzYgMCAwIDEtLjYzMi4xMDJabTQuNzM3LS43ODV2LTQuNTI5aC42MzZ2NS4yNDZoLS41ODFsLS4wNTUtLjcxN1ptLTIuNDg2LTEuMDl2LS4wNzJjMC0uMjgyLjAzNS0uNTM4LjEwMy0uNzY4LjA3LS4yMzIuMTY5LS40MzEuMjk3LS41OTdhMS4zMSAxLjMxIDAgMCAxIDEuMDYyLS41MmMuMjMyIDAgLjQzNS4wNDEuNjA4LjEyMy4xNzUuMDguMzIzLjE5Ny40NDQuMzUyLjEyMy4xNTMuMjIuMzM3LjI5LjU1My4wNzEuMjE3LjEyLjQ2MS4xNDcuNzM0di4zMTVjLS4wMjUuMjctLjA3NC41MTQtLjE0Ny43My0uMDcuMjE3LS4xNjcuNDAxLS4yOS41NTRhMS4yMiAxLjIyIDAgMCAxLS40NDQuMzUyYy0uMTc1LjA4LS4zOC4xMTktLjYxNS4xMTktLjIxNiAwLS40MTQtLjA0Ny0uNTk0LS4xNGExLjQwMiAxLjQwMiAwIDAgMS0uNDYxLS4zOTMgMS44OTEgMS44OTEgMCAwIDEtLjI5Ny0uNTk0IDIuNjI3IDIuNjI3IDAgMCAxLS4xMDMtLjc0OFptLjYzNi0uMDcydi4wNzJjMCAuMTg1LjAxOC4zNTguMDU0LjUyLjAzOS4xNi4wOTguMzAzLjE3OC40MjYuMDc5LjEyMy4xODEuMjIuMzA0LjI5LjEyMy4wNjkuMjcuMTAzLjQ0LjEwMy4yMSAwIC4zODItLjA0NC41MTYtLjEzM2EuOTkuOTkgMCAwIDAgLjMyOC0uMzUyYy4wODItLjE0Ni4xNDUtLjMwNC4xOTEtLjQ3NXYtLjgyM2ExLjc2NyAxLjc2NyAwIDAgMC0uMTItLjM2MiAxLjExMiAxLjExMiAwIDAgMC0uMTk4LS4zMTQuODQzLjg0MyAwIDAgMC0uMjk3LS4yMjIuOTYzLjk2MyAwIDAgMC0uNDEzLS4wODIuODczLjg3MyAwIDAgMC0uNDQ3LjExLjg2Ni44NjYgMCAwIDAtLjMwNC4yOTNjLS4wOC4xMjMtLjEzOS4yNjYtLjE3OC40M2EyLjM4NCAyLjM4NCAwIDAgMC0uMDU0LjUyWiIvPjxnIGNsaXAtcGF0aD0idXJsKCNqKSI+PHBhdGggZmlsbD0iIzE5ODAzOCIgZD0iTTE4Ni4wMTIgNjQuMzM2YTMuODg2IDMuODg2IDAgMCAwIDAgNy43NyAzLjg4NyAzLjg4NyAwIDAgMCAzLjg4Ni0zLjg4NSAzLjg4NyAzLjg4NyAwIDAgMC0zLjg4Ni0zLjg4NVptLS43NzcgNS44MjgtMS45NDMtMS45NDMuNTQ4LS41NDcgMS4zOTUgMS4zOSAyLjk0OS0yLjk0OC41NDguNTUxLTMuNDk3IDMuNDk3WiIvPjwvZz48L2c+PHBhdGggZmlsbD0iIzAwMCIgZmlsbC1vcGFjaXR5PSIuMTIiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTIwMCA3OC40NjdIMHYtLjU4M2gyMDB2LjU4M1oiIGNsaXAtcnVsZT0iZXZlbm9kZCIvPjxnIGNsaXAtcGF0aD0idXJsKCNrKSI+PHBhdGggZmlsbD0iIzMwNTY4MCIgZD0iTTkuOTE1IDg4Ljk5aDIuMDUyYy40NCAwIC44MTYuMDY3IDEuMTI3LjIuMzExLjEzMi41NDkuMzI5LjcxMy41ODkuMTY4LjI1OC4yNTEuNTc2LjI1MS45NTYgMCAuMjktLjA1My41NDUtLjE1OS43NjUtLjEwNi4yMi0uMjU2LjQwNi0uNDUuNTU4YTIuMTc2IDIuMTc2IDAgMCAxLS42OTQuMzQ2bC0uMzAyLjE0OGgtMS44NDVsLS4wMDgtLjc5M2gxLjM4M2MuMjM5IDAgLjQzOC0uMDQyLjU5Ny0uMTI3LjE2LS4wODUuMjgtLjIuMzU5LS4zNDdhMS4wMiAxLjAyIDAgMCAwIC4xMjMtLjUwMmMwLS4yMDItLjA0LS4zNzctLjEyLS41MjZhLjc3Ljc3IDAgMCAwLS4zNTgtLjM0NyAxLjM2IDEuMzYgMCAwIDAtLjYxNy0uMTIzaC0xLjA1MnY1LjAwNGgtMXYtNS44Wm0zLjMxIDUuODAxLTEuMzYyLTIuNjA2IDEuMDQ4LS4wMDQgMS4zODIgMi41NTh2LjA1MmgtMS4wNjdabTQuNDczLTEuMDE2VjkwLjQ4aC45NjR2NC4zMTFoLS45MDhsLS4wNTYtMS4wMTZabS4xMzYtLjg5Ni4zMjItLjAwOGMwIC4yOS0uMDMyLjU1Ni0uMDk1LjhhMS44NTYgMS44NTYgMCAwIDEtLjI5NS42MzQgMS4zNzkgMS4zNzkgMCAwIDEtLjUxLjQxOCAxLjcyNSAxLjcyNSAwIDAgMS0uNzQ1LjE0OGMtLjIxIDAtLjQwMi0uMDMtLjU3OC0uMDkyYTEuMTg0IDEuMTg0IDAgMCAxLS40NTQtLjI4MyAxLjI4NyAxLjI4NyAwIDAgMS0uMjktLjQ5OCAyLjMwMyAyLjMwMyAwIDAgMS0uMTA0LS43MzNWOTAuNDhoLjk2djIuNzkzYzAgLjE1Ny4wMTguMjg4LjA1NS4zOTVhLjY2OS42NjkgMCAwIDAgLjE1Mi4yNS41NC41NCAwIDAgMCAuMjIzLjEzNmMuMDg1LjAyNi4xNzUuMDQuMjcuMDQuMjc0IDAgLjQ5LS4wNTMuNjQ2LS4xNmEuODgyLjg4MiAwIDAgMCAuMzM5LS40MzhjLjA3LS4xODMuMTA0LS4zODkuMTA0LS42MTdabTIuOTg2LTQuMjA4djYuMTJoLS45NjR2LTYuMTJoLjk2NFptMy4wOTggNi4yYy0uMzE5IDAtLjYwNy0uMDUyLS44NjUtLjE1NmExLjkwOCAxLjkwOCAwIDAgMS0uNjUzLS40NDIgMS45NiAxLjk2IDAgMCAxLS40MS0uNjY1IDIuMzMgMi4zMyAwIDAgMS0uMTQ0LS44MjV2LS4xNmMwLS4zMzcuMDUtLjY0Mi4xNDgtLjkxNmEyLjA4IDIuMDggMCAwIDEgLjQxLS43Yy4xNzUtLjE5OC4zODMtLjM0OC42MjItLjQ1MS4yMzktLjEwNC40OTctLjE1Ni43NzYtLjE1Ni4zMDggMCAuNTc4LjA1Mi44MS4xNTYuMjMuMTAzLjQyMi4yNS41NzMuNDM4LjE1NC4xODYuMjY4LjQwOC4zNDMuNjY1LjA3Ny4yNTguMTE1LjU0Mi4xMTUuODUzdi40MWgtMy4zM3YtLjY4OWgyLjM4MnYtLjA3NmExLjM1MiAxLjM1MiAwIDAgMC0uMTA0LS40ODYuODI2LjgyNiAwIDAgMC0uMjgzLS4zNjZjLS4xMjctLjA5My0uMjk3LS4xNC0uNTEtLjE0YS44NjcuODY3IDAgMCAwLS40MjYuMTA0Ljg0NC44NDQgMCAwIDAtLjMwNy4yOSAxLjUzMyAxLjUzMyAwIDAgMC0uMTkuNDYzIDIuNTk3IDIuNTk3IDAgMCAwLS4wNjUuNjAydi4xNTljMCAuMTg5LjAyNi4zNjQuMDc2LjUyNi4wNTMuMTYuMTMuMjk5LjIzMS40MTguMTAxLjEyLjIyMy4yMTQuMzY3LjI4My4xNDMuMDY2LjMwNi4xLjQ5LjEuMjMgMCAuNDM3LS4wNDcuNjE3LS4xNC4xOC0uMDkzLjMzOC0uMjI0LjQ3LS4zOTRsLjUwNi40OWExLjgxNyAxLjgxNyAwIDAgMS0uOTA4LjY5Yy0uMjEyLjA3Ni0uNDYuMTE1LS43NDEuMTE1Wm02LjY5OCAwYy0uMzE5IDAtLjYwNy0uMDUyLS44NjUtLjE1NmExLjkwOCAxLjkwOCAwIDAgMS0uNjUzLS40NDIgMS45NiAxLjk2IDAgMCAxLS40MS0uNjY1IDIuMzMgMi4zMyAwIDAgMS0uMTQ0LS44MjV2LS4xNmMwLS4zMzcuMDUtLjY0Mi4xNDgtLjkxNmEyLjA4IDIuMDggMCAwIDEgLjQxLS43Yy4xNzUtLjE5OC4zODItLjM0OC42MjEtLjQ1MS4yNC0uMTA0LjQ5OC0uMTU2Ljc3Ny0uMTU2LjMwOCAwIC41NzguMDUyLjgxLjE1Ni4yMy4xMDMuNDIxLjI1LjU3My40MzguMTU0LjE4Ni4yNjguNDA4LjM0Mi42NjUuMDc4LjI1OC4xMTYuNTQyLjExNi44NTN2LjQxaC0zLjMzdi0uNjg5aDIuMzgydi0uMDc2YTEuMzUyIDEuMzUyIDAgMCAwLS4xMDQtLjQ4Ni44MjYuODI2IDAgMCAwLS4yODMtLjM2NmMtLjEyNy0uMDkzLS4yOTctLjE0LS41MS0uMTRhLjg2Ny44NjcgMCAwIDAtLjQyNi4xMDQuODQ0Ljg0NCAwIDAgMC0uMzA3LjI5IDEuNTMzIDEuNTMzIDAgMCAwLS4xOTEuNDYzIDIuNTk3IDIuNTk3IDAgMCAwLS4wNjQuNjAydi4xNTljMCAuMTg5LjAyNi4zNjQuMDc2LjUyNi4wNTMuMTYuMTMuMjk5LjIzMS40MTguMTAxLjEyLjIyMy4yMTQuMzY3LjI4My4xNDMuMDY2LjMwNi4xLjQ5LjEuMjMgMCAuNDM3LS4wNDcuNjE3LS4xNC4xOC0uMDkzLjMzNy0uMjI0LjQ3LS4zOTRsLjUwNi40OWExLjgxNyAxLjgxNyAwIDAgMS0uOTA4LjY5Yy0uMjEzLjA3Ni0uNDYuMTE1LS43NDEuMTE1Wm0zLjU3Mi0zLjQ3djMuMzloLS45NnYtNC4zMWguOTA0bC4wNTYuOTJabS0uMTcxIDEuMDc1LS4zMTEtLjAwNGMuMDAyLS4zMDUuMDQ1LS41ODUuMTI3LS44NC4wODUtLjI1NS4yMDItLjQ3NS4zNS0uNjU4LjE1Mi0uMTgzLjMzMy0uMzI0LjU0My0uNDIyLjIxLS4xMDEuNDQzLS4xNTIuNzAxLS4xNTIuMjA3IDAgLjM5NC4wMy41NjIuMDg4LjE3LjA1Ni4zMTUuMTQ4LjQzNC4yNzUuMTIyLjEyNy4yMTUuMjk0LjI3OS40OTguMDY0LjIwMi4wOTYuNDUuMDk2Ljc0NXYyLjc4NWgtLjk2NXYtMi43ODljMC0uMjA3LS4wMy0uMzctLjA5MS0uNDlhLjUxMy41MTMgMCAwIDAtLjI2LS4yNTkuOTcyLjk3MiAwIDAgMC0uNDE4LS4wOC45MjguOTI4IDAgMCAwLS40NDIuMTA0Ljk5NS45OTUgMCAwIDAtLjMzLjI4M2MtLjA4OC4xMi0uMTU2LjI1Ny0uMjA0LjQxNGExLjcxMiAxLjcxMiAwIDAgMC0uMDcxLjUwMlptNi42NjctMS45OTZoLjg3M3Y0LjE5MWMwIC4zODgtLjA4My43MTgtLjI0Ny45ODktLjE2NS4yNy0uMzk1LjQ3Ni0uNjkuNjE3YTIuNDA3IDIuNDA3IDAgMCAxLTIuMTU1LS4wODggMS40NCAxLjQ0IDAgMCAxLS40NjYtLjQxbC40NS0uNTY2Yy4xNTQuMTg0LjMyNC4zMTguNTEuNDAzLjE4Ni4wODUuMzgxLjEyNy41ODYuMTI3LjIyIDAgLjQwOC0uMDQuNTYyLS4xMjNhLjgzNS44MzUgMCAwIDAgLjM2Mi0uMzU1IDEuMTkgMS4xOSAwIDAgMCAuMTI4LS41NzR2LTMuMjM1bC4wODctLjk3NlptLTIuOTI4IDIuMjAzVjkyLjZjMC0uMzI3LjA0LS42MjQuMTItLjg5My4wOC0uMjcuMTkzLS41MDMuMzQyLS42OTcuMTQ5LS4xOTcuMzMtLjM0Ny41NDItLjQ1YTEuNTkgMS41OSAwIDAgMSAuNzIxLS4xNmMuMjc5IDAgLjUxNy4wNTEuNzEzLjE1Mi4yLjEuMzY1LjI0Ni40OTguNDM0LjEzMy4xODYuMjM3LjQxLjMxMS42Ny4wNzcuMjU3LjEzNC41NDQuMTcxLjg2di4yNjdjLS4wMzQuMzA4LS4wOTMuNTktLjE3NS44NDVhMi4zMzMgMi4zMzMgMCAwIDEtLjMyNy42NjFjLS4xMzUuMTg2LS4zMDIuMzMtLjUwMi40My0uMTk2LjEwMS0uNDI5LjE1Mi0uNjk3LjE1Mi0uMjYzIDAtLjUtLjA1NS0uNzEzLS4xNjRhMS42MjQgMS42MjQgMCAwIDEtLjU0Mi0uNDU4IDIuMTcyIDIuMTcyIDAgMCAxLS4zNDMtLjY5M2MtLjA4LS4yNjgtLjExOS0uNTYtLjExOS0uODczWm0uOTYtLjA4M3YuMDgzYzAgLjE5Ny4wMTkuMzguMDU2LjU1LjA0LjE3LjEuMzIuMTguNDVhLjk0Ljk0IDAgMCAwIC4zMS4zMDMuOTA0LjkwNCAwIDAgMCAuNDUuMTA4Yy4yMjYgMCAuNDEtLjA0OC41NTQtLjE0NGEuOTMuOTMgMCAwIDAgLjMzNS0uMzg2Yy4wOC0uMTY1LjEzNS0uMzQ4LjE2Ny0uNTV2LS43MjFhMS43NTggMS43NTggMCAwIDAtLjEtLjQzOCAxLjE3NCAxLjE3NCAwIDAgMC0uMTk1LS4zNTUuODE1LjgxNSAwIDAgMC0uMzEtLjIzOSAxLjAzMyAxLjAzMyAwIDAgMC0uNDQzLS4wODguODc3Ljg3NyAwIDAgMC0uNDUuMTEyLjkxMy45MTMgMCAwIDAtLjMxNS4zMDdjLS4wOC4xMy0uMTQuMjgxLS4xOC40NTQtLjAzOS4xNzMtLjA1OS4zNTctLjA1OS41NTRabTUuMDEtMi4xMnY0LjMxMWgtLjk2NHYtNC4zMWguOTY1Wk00Mi43IDg5LjM1YzAtLjE0Ni4wNDctLjI2Ny4xNDMtLjM2M2EuNTUuNTUgMCAwIDEgLjQwNi0uMTQ3Yy4xNyAwIC4zMDUuMDQ5LjQwMy4xNDdhLjQ4NC40ODQgMCAwIDEgLjE0Ny4zNjMuNDguNDggMCAwIDEtLjE0Ny4zNTguNTUzLjU1MyAwIDAgMS0uNDAzLjE0NC41NTguNTU4IDAgMCAxLS40MDYtLjE0NC40ODcuNDg3IDAgMCAxLS4xNDMtLjM1OFptMy4xNzcgMi4wNTF2My4zOTFoLS45NnYtNC4zMWguOTA0bC4wNTYuOTJabS0uMTcxIDEuMDc2LS4zMS0uMDA0Yy4wMDItLjMwNS4wNDQtLjU4NS4xMjctLjg0LjA4NS0uMjU1LjIwMi0uNDc1LjM1LS42NTguMTUyLS4xODMuMzMyLS4zMjQuNTQyLS40MjIuMjEtLjEwMS40NDQtLjE1Mi43MDEtLjE1Mi4yMDcgMCAuMzk1LjAzLjU2Mi4wODguMTcuMDU2LjMxNS4xNDguNDM0LjI3NS4xMjMuMTI3LjIxNi4yOTQuMjguNDk4YTIuNSAyLjUgMCAwIDEgLjA5NS43NDV2Mi43ODVoLS45NjR2LTIuNzg5YzAtLjIwNy0uMDMtLjM3LS4wOTItLjQ5YS41MTMuNTEzIDAgMCAwLS4yNTktLjI1OS45NzIuOTcyIDAgMCAwLS40MTgtLjA4LjkyOC45MjggMCAwIDAtLjQ0My4xMDQuOTk1Ljk5NSAwIDAgMC0uMzMuMjgzIDEuMzcgMS4zNyAwIDAgMC0uMjAzLjQxNCAxLjcxMiAxLjcxMiAwIDAgMC0uMDcyLjUwMlptNS44MDcgMi4zOTVhMi4zIDIuMyAwIDAgMS0uODY0LS4xNTYgMS45MDggMS45MDggMCAwIDEtLjY1NC0uNDQyIDEuOTYzIDEuOTYzIDAgMCAxLS40MS0uNjY1IDIuMzMgMi4zMyAwIDAgMS0uMTQ0LS44MjV2LS4xNmMwLS4zMzcuMDUtLjY0Mi4xNDgtLjkxNmEyLjA4IDIuMDggMCAwIDEgLjQxLS43Yy4xNzUtLjE5OC4zODMtLjM0OC42MjItLjQ1MS4yMzktLjEwNC40OTgtLjE1Ni43NzctLjE1Ni4zMDggMCAuNTc3LjA1Mi44MDguMTU2LjIzMS4xMDMuNDIzLjI1LjU3NC40MzguMTU0LjE4Ni4yNjguNDA4LjM0My42NjUuMDc3LjI1OC4xMTUuNTQyLjExNS44NTN2LjQxaC0zLjMzdi0uNjg5aDIuMzgydi0uMDc2YTEuMzUgMS4zNSAwIDAgMC0uMTA0LS40ODYuODI2LjgyNiAwIDAgMC0uMjgyLS4zNjZjLS4xMjgtLjA5My0uMjk4LS4xNC0uNTEtLjE0YS44NjcuODY3IDAgMCAwLS40MjcuMTA0Ljg0My44NDMgMCAwIDAtLjMwNi4yOSAxLjUzIDEuNTMgMCAwIDAtLjE5Mi40NjMgMi41OTcgMi41OTcgMCAwIDAtLjA2NC42MDJ2LjE1OWMwIC4xODkuMDI2LjM2NC4wNzYuNTI2LjA1My4xNi4xMy4yOTkuMjMxLjQxOC4xMDEuMTIuMjIzLjIxNC4zNjcuMjgzLjE0My4wNjYuMzA3LjEuNDkuMS4yMyAwIC40MzctLjA0Ny42MTctLjE0LjE4MS0uMDkzLjMzOC0uMjI0LjQ3LS4zOTRsLjUwNi40OWExLjgxNSAxLjgxNSAwIDAgMS0uOTA4LjY5Yy0uMjEyLjA3Ni0uNDYuMTE1LS43NC4xMTVabS0zOS43OTIgMTFjLS4zMiAwLS42MDctLjA1Mi0uODY1LS4xNTZhMS44OTkgMS44OTkgMCAwIDEtLjY1My0uNDQyIDEuOTYyIDEuOTYyIDAgMCAxLS40MS0uNjY1IDIuMzMgMi4zMyAwIDAgMS0uMTQ0LS44MjV2LS4xNTljMC0uMzM4LjA1LS42NDMuMTQ3LS45MTcuMDk5LS4yNzMuMjM1LS41MDcuNDEtLjcwMS4xNzYtLjE5Ni4zODMtLjM0Ny42MjItLjQ1LjI0LS4xMDQuNDk4LS4xNTYuNzc3LS4xNTYuMzA4IDAgLjU3OC4wNTIuODA5LjE1Ni4yMzEuMTAzLjQyMi4yNS41NzQuNDM4LjE1NC4xODYuMjY4LjQwOC4zNDIuNjY1LjA3Ny4yNTguMTE2LjU0Mi4xMTYuODUzdi40MWgtMy4zM3YtLjY4OWgyLjM4MnYtLjA3NWExLjM1NSAxLjM1NSAwIDAgMC0uMTA0LS40ODcuODI3LjgyNyAwIDAgMC0uMjgzLS4zNjZjLS4xMjctLjA5My0uMjk3LS4xNC0uNTEtLjE0YS44MzYuODM2IDAgMCAwLS43MzMuMzk1IDEuNTIgMS41MiAwIDAgMC0uMTkxLjQ2MiAyLjYwMSAyLjYwMSAwIDAgMC0uMDY0LjYwMnYuMTU5YzAgLjE4OS4wMjUuMzY0LjA3Ni41MjYuMDUzLjE1OS4xMy4yOTkuMjMuNDE4LjEwMi4xMi4yMjQuMjE0LjM2Ny4yODMuMTQ0LjA2Ny4zMDcuMS40OS4xLjIzMiAwIC40MzctLjA0Ny42MTgtLjE0LjE4LS4wOTMuMzM3LS4yMjQuNDctLjM5NGwuNTA2LjQ5YTEuNzk2IDEuNzk2IDAgMCAxLS45MDguNjg5Yy0uMjEzLjA3Ny0uNDYuMTE2LS43NDEuMTE2Wm0zLjM1My00LjM5MS44MiAxLjQzLjgzNy0xLjQzaDEuMDU2bC0xLjMwNyAyLjExNiAxLjM1OSAyLjE5NWgtMS4wNTZsLS44NzctMS40OS0uODc2IDEuNDloLTEuMDZsMS4zNTUtMi4xOTUtMS4zMDMtMi4xMTZoMS4wNTJabTUuMzM3IDQuMzkxYTIuMjkgMi4yOSAwIDAgMS0uODY1LS4xNTYgMS44OTkgMS44OTkgMCAwIDEtLjY1My0uNDQyIDEuOTYyIDEuOTYyIDAgMCAxLS40MS0uNjY1IDIuMzMxIDIuMzMxIDAgMCAxLS4xNDQtLjgyNXYtLjE1OWMwLS4zMzguMDQ5LS42NDMuMTQ3LS45MTcuMDk5LS4yNzMuMjM1LS41MDcuNDEtLjcwMS4xNzYtLjE5Ni4zODMtLjM0Ny42MjItLjQ1LjI0LS4xMDQuNDk4LS4xNTYuNzc3LS4xNTYuMzA4IDAgLjU3OC4wNTIuODA5LjE1Ni4yMy4xMDMuNDIyLjI1LjU3NC40MzguMTU0LjE4Ni4yNjguNDA4LjM0Mi42NjUuMDc3LjI1OC4xMTYuNTQyLjExNi44NTN2LjQxaC0zLjMzMXYtLjY4OWgyLjM4M3YtLjA3NWExLjM1MyAxLjM1MyAwIDAgMC0uMTA0LS40ODcuODI2LjgyNiAwIDAgMC0uMjgzLS4zNjZjLS4xMjctLjA5My0uMjk3LS4xNC0uNTEtLjE0YS44MzcuODM3IDAgMCAwLS43MzMuMzk1IDEuNTIgMS41MiAwIDAgMC0uMTkxLjQ2MiAyLjYwMSAyLjYwMSAwIDAgMC0uMDY0LjYwMnYuMTU5YzAgLjE4OS4wMjUuMzY0LjA3Ni41MjYuMDUzLjE1OS4xMy4yOTkuMjMuNDE4LjEwMi4xMi4yMjQuMjE0LjM2Ny4yODMuMTQ0LjA2Ny4zMDcuMS40OS4xLjIzMiAwIC40MzctLjA0Ny42MTgtLjE0LjE4LS4wOTMuMzM3LS4yMjQuNDctLjM5NGwuNTA2LjQ5YTEuNzk3IDEuNzk3IDAgMCAxLS45MDguNjg5Yy0uMjEzLjA3Ny0uNDYuMTE2LS43NDEuMTE2Wm00LjM4LS43NjVhLjk1Ljk1IDAgMCAwIC40MjMtLjA5Mi43OTkuNzk5IDAgMCAwIC4zMDctLjI2My43MTQuNzE0IDAgMCAwIC4xMzEtLjM4NmguOTA0YTEuMzQ3IDEuMzQ3IDAgMCAxLS4yNDcuNzYxYy0uMTU5LjIyOC0uMzcuNDEtLjYzMy41NDUtLjI2My4xMzMtLjU1NC4yLS44NzMuMi0uMzI5IDAtLjYxNi0uMDU2LS44Ni0uMTY4YTEuNzAxIDEuNzAxIDAgMCAxLS42MS0uNDcgMi4wNyAyLjA3IDAgMCAxLS4zNjYtLjY4OWMtLjA4LS4yNi0uMTItLjUzOS0uMTItLjgzN3YtLjEzOWMwLS4yOTguMDQtLjU3Ny4xMi0uODM3LjA4Mi0uMjYzLjIwNC0uNDk0LjM2Ni0uNjkzLjE2Mi0uMTk5LjM2Ni0uMzU1LjYxLS40NjYuMjQ0LS4xMTQuNTMtLjE3Mi44NTYtLjE3Mi4zNDYgMCAuNjQ5LjA3LjkwOS4yMDguMjYuMTM1LjQ2NS4zMjUuNjEzLjU2OS4xNTIuMjQyLjIzLjUyNC4yMzUuODQ1aC0uOTA0YS45Ni45NiAwIDAgMC0uMTItLjQzLjc5Ljc5IDAgMCAwLS4yOTQtLjMxMS44NDEuODQxIDAgMCAwLS40NS0uMTE2Ljg5Mi44OTIgMCAwIDAtLjQ4My4xMi44LjggMCAwIDAtLjI5OC4zMTkgMS41NTggMS41NTggMCAwIDAtLjE1Ni40NWMtLjAyOS4xNjUtLjA0NC4zMzYtLjA0NC41MTR2LjEzOWMwIC4xNzguMDE1LjM1MS4wNDQuNTE4LjAzLjE2OC4wOC4zMTguMTUyLjQ1YS44Ny44NyAwIDAgMCAuMzAyLjMxNWMuMTI4LjA3Ny4yOS4xMTYuNDg3LjExNlptNS4yNDItLjMzMXYtMy4yOTVoLjk2NHY0LjMxMWgtLjkwOGwtLjA1Ni0xLjAxNlptLjEzNS0uODk2LjMyMy0uMDA4YzAgLjI4OS0uMDMyLjU1Ni0uMDk2LjhhMS44NTkgMS44NTkgMCAwIDEtLjI5NC42MzQgMS4zNzggMS4zNzggMCAwIDEtLjUxLjQxOCAxLjcxNSAxLjcxNSAwIDAgMS0uNzQ1LjE0OGMtLjIxIDAtLjQwMy0uMDMxLS41NzgtLjA5MmExLjE3NyAxLjE3NyAwIDAgMS0uNDU0LS4yODMgMS4yOCAxLjI4IDAgMCAxLS4yOTEtLjQ5OCAyLjI5OCAyLjI5OCAwIDAgMS0uMTA0LS43MzN2LTIuNzg1aC45NnYyLjc5M2MwIC4xNTcuMDIuMjg4LjA1Ni4zOTRhLjY2LjY2IDAgMCAwIC4xNTIuMjUxLjU0Mi41NDIgMCAwIDAgLjIyMy4xMzYuODkuODkgMCAwIDAgLjI3LjA0Yy4yNzQgMCAuNDktLjA1My42NDYtLjE2YS44NzguODc4IDAgMCAwIC4zMzktLjQzOCAxLjc0IDEuNzQgMCAwIDAgLjEwMy0uNjE3Wm0zLjkzNS0yLjM5OXYuNzAxaC0yLjQzdi0uNzAxaDIuNDNabS0xLjczLTEuMDU2aC45NjF2NC4xNzZjMCAuMTMzLjAxOS4yMzUuMDU2LjMwNy4wNC4wNjkuMDk0LjExNS4xNjMuMTM5YS43NC43NCAwIDAgMCAuMjQzLjAzNiAxLjkwMSAxLjkwMSAwIDAgMCAuMzM5LS4wMzZsLjAwNC43MzNhMi4xMDYgMi4xMDYgMCAwIDEtLjYzNy4wOTJjLS4yMjEgMC0uNDE2LS4wMzktLjU4Ni0uMTE2YS44NjIuODYyIDAgMCAxLS4zOTktLjM4NmMtLjA5NS0uMTc4LS4xNDMtLjQxNS0uMTQzLS43MDl2LTQuMjM2Wm0zLjY0NSAxLjA1NnY0LjMxMWgtLjk2NXYtNC4zMTFoLjk2NVptLTEuMDI4LTEuMTMxYzAtLjE0Ni4wNDctLjI2Ny4xNDMtLjM2M2EuNTUuNTUgMCAwIDEgLjQwNy0uMTQ3Yy4xNyAwIC4zMDQuMDQ5LjQwMi4xNDdhLjQ4Ni40ODYgMCAwIDEgLjE0Ny4zNjNjMCAuMTQzLS4wNDkuMjYzLS4xNDcuMzU4YS41NTEuNTUxIDAgMCAxLS40MDMuMTQ0LjU1Ny41NTcgMCAwIDEtLjQwNi0uMTQ0LjQ4NS40ODUgMCAwIDEtLjE0My0uMzU4Wm0yLjA0MiAzLjMzNHYtLjA5MWMwLS4zMTEuMDQ1LS41OTkuMTM1LS44NjUuMDktLjI2OC4yMi0uNS4zOS0uNjk3LjE3My0uMTk5LjM4My0uMzUzLjYzLS40NjIuMjUtLjExMi41MzItLjE2OC44NDUtLjE2OC4zMTYgMCAuNTk4LjA1Ni44NDUuMTY4LjI1LjEwOS40Ni4yNjMuNjMzLjQ2Mi4xNzMuMTk3LjMwNC40MjkuMzk1LjY5Ny4wOS4yNjYuMTM1LjU1NC4xMzUuODY1di4wOTFjMCAuMzExLS4wNDUuNTk5LS4xMzYuODY1LS4wOS4yNjYtLjIyMS40OTgtLjM5NC42OTdhMS44MTQgMS44MTQgMCAwIDEtLjYzLjQ2MiAyLjA2MiAyLjA2MiAwIDAgMS0uODQuMTY0IDIuMSAyLjEgMCAwIDEtLjg0OS0uMTY0IDEuODEzIDEuODEzIDAgMCAxLS42My0uNDYyIDIuMDYxIDIuMDYxIDAgMCAxLS4zOTQtLjY5NyAyLjY3MyAyLjY3MyAwIDAgMS0uMTM1LS44NjVabS45Ni0uMDkxdi4wOTFjMCAuMTk0LjAyLjM3OC4wNi41NS4wNC4xNzMuMTAyLjMyNC4xODcuNDU0YS45MTYuOTE2IDAgMCAwIC4zMjcuMzA3Yy4xMzMuMDc1LjI5LjExMi40NzQuMTEyYS45MS45MSAwIDAgMCAuNzg5LS40MTljLjA4NS0uMTMuMTQ3LS4yODEuMTg3LS40NTRhMi4yOSAyLjI5IDAgMCAwIC4wNjQtLjU1di0uMDkxYTIuMjMgMi4yMyAwIDAgMC0uMDY0LS41NDIgMS4zOSAxLjM5IDAgMCAwLS4xOTEtLjQ1OC45MS45MSAwIDAgMC0uNzkzLS40MjcuOTIxLjkyMSAwIDAgMC0uNDcuMTE2LjkyLjkyIDAgMCAwLS4zMjMuMzExIDEuNDQ0IDEuNDQ0IDAgMCAwLS4xODcuNDU4Yy0uMDQuMTctLjA2LjM1MS0uMDYuNTQyWm00Ljk1LTEuMTkxdjMuMzloLS45NnYtNC4zMTFoLjkwNWwuMDU2LjkyMVptLS4xNyAxLjA3NS0uMzEyLS4wMDRjLjAwMy0uMzA1LjA0Ni0uNTg1LjEyOC0uODQuMDg1LS4yNTUuMjAyLS40NzUuMzUtLjY1OGExLjU5NiAxLjU5NiAwIDAgMSAxLjI0My0uNTc0Yy4yMDggMCAuMzk1LjAzLjU2My4wODguMTcuMDU2LjMxNC4xNDguNDM0LjI3NS4xMjIuMTI4LjIxNS4yOTQuMjc5LjQ5OGEyLjUgMi41IDAgMCAxIC4wOTUuNzQ1djIuNzg1aC0uOTY0di0yLjc4OWMwLS4yMDctLjAzLS4zNy0uMDkyLS40OWEuNTE1LjUxNSAwIDAgMC0uMjU4LS4yNTkuOTczLjk3MyAwIDAgMC0uNDE5LS4wOC45My45MyAwIDAgMC0uNDQyLjEwNC45ODguOTg4IDAgMCAwLS4zMy4yODMgMS4zNzEgMS4zNzEgMCAwIDAtLjIwNC40MTQgMS43MTMgMS43MTMgMCAwIDAtLjA3Mi41MDJabTYuMzI0IDEuMTQ4YS40OC40OCAwIDAgMC0uMDcxLS4yNTljLS4wNDgtLjA4LS4xNC0uMTUyLS4yNzUtLjIxNWEyLjYzOCAyLjYzOCAwIDAgMC0uNTktLjE3NiA1LjAyMiA1LjAyMiAwIDAgMS0uNjMtLjE3OSAyLjAwNSAyLjAwNSAwIDAgMS0uNDg1LS4yNTkuOTkyLjk5MiAwIDAgMS0uNDI2LS44MzdjMC0uMTc1LjAzOC0uMzQxLjExNS0uNDk4LjA3Ny0uMTU2LjE4Ny0uMjk1LjMzLS40MTRhMS42IDEuNiAwIDAgMSAuNTIyLS4yODNjLjIwOC0uMDY5LjQzOS0uMTA0LjY5NC0uMTA0LjM2IDAgLjY3LjA2Mi45MjguMTg0LjI2LjExOS40Ni4yODMuNTk4LjQ5LjEzOC4yMDQuMjA3LjQzNS4yMDcuNjkzaC0uOTZjMC0uMTE0LS4wMy0uMjItLjA4OC0uMzE5YS42MDkuNjA5IDAgMCAwLS4yNTUtLjI0My44ODIuODgyIDAgMCAwLS40My0uMDk1LjkzNi45MzYgMCAwIDAtLjQxLjA3OS41NjIuNTYyIDAgMCAwLS4yNC4yLjUwNi41MDYgMCAwIDAtLjAzNi40NjZjLjAzLjA1NS4wNzcuMTA3LjE0NC4xNTUuMDY2LjA0NS4xNTcuMDg4LjI3LjEyOC4xMTguMDM5LjI2NC4wNzguNDM5LjExNS4zMy4wNjkuNjEyLjE1OC44NDkuMjY3LjIzOS4xMDYuNDIyLjI0NC41NS40MTQuMTI3LjE2OC4xOS4zOC4xOS42MzhhMS4xMzIgMS4xMzIgMCAwIDEtLjQ3My45MzYgMS43NyAxLjc3IDAgMCAxLS41NTQuMjY3IDIuNDg0IDIuNDg0IDAgMCAxLS43MTcuMDk2Yy0uMzkgMC0uNzIxLS4wNjktLjk5Mi0uMjA3YTEuNTkgMS41OSAwIDAgMS0uNjE4LS41MzggMS4yNzYgMS4yNzYgMCAwIDEtLjIwNy0uNjg2aC45MjhjLjAxLjE3OC4wNi4zMi4xNDguNDI3LjA5LjEwMy4yMDEuMTc5LjMzNC4yMjcuMTM2LjA0NS4yNzUuMDY4LjQxOS4wNjguMTcyIDAgLjMxNy0uMDIzLjQzNC0uMDY4YS42MzEuNjMxIDAgMCAwIC4yNjctLjE5MS40Ni40NiAwIDAgMCAuMDkxLS4yNzlaIi8+PC9nPjxnIGNsaXAtcGF0aD0idXJsKCNsKSI+PHBhdGggZmlsbD0iIzAwMCIgZmlsbC1vcGFjaXR5PSIuNTQiIGQ9Ik0xMDcuMTUyIDg1LjEyOHY1aC0uNjMydi00LjIxbC0xLjI3My40NjR2LS41N2wxLjgwNi0uNjg0aC4wOTlabTUuMjEyIDIuMTE4di43NThjMCAuNDA3LS4wMzYuNzUxLS4xMDkgMS4wMzEtLjA3My4yOC0uMTc4LjUwNi0uMzE0LjY3NmExLjIgMS4yIDAgMCAxLS40OTUuMzczIDEuNzY2IDEuNzY2IDAgMCAxLS42NDkuMTEyYy0uMTkxIDAtLjM2OC0uMDI0LS41My0uMDcyYTEuMjY0IDEuMjY0IDAgMCAxLS40MzctLjIyOCAxLjM5MyAxLjM5MyAwIDAgMS0uMzI3LS40MTcgMi4yMTEgMi4yMTEgMCAwIDEtLjIwOS0uNjIxIDQuNDYyIDQuNDYyIDAgMCAxLS4wNzItLjg1NHYtLjc1OGMwLS40MDguMDM3LS43NS4xMS0xLjAyNS4wNzUtLjI3NS4xODEtLjQ5Ni4zMTctLjY2Mi4xMzctLjE2OS4zMDEtLjI5LjQ5Mi0uMzYyLjE5NC0uMDczLjQxLS4xMS42NDktLjExLjE5MyAwIC4zNzEuMDI0LjUzMy4wNzIuMTY0LjA0Ni4zMDkuMTIuNDM3LjIyMi4xMjcuMS4yMzUuMjM1LjMyNC40MDMuMDkxLjE2Ni4xNjEuMzcuMjA5LjYxMS4wNDcuMjQyLjA3MS41MjUuMDcxLjg1Wm0tLjYzNS44NnYtLjk2NmMwLS4yMjMtLjAxNC0uNDItLjA0MS0uNTg4YTEuOCAxLjggMCAwIDAtLjExMy0uNDM3Ljg1NS44NTUgMCAwIDAtLjE5MS0uMjkzLjY3Ny42NzcgMCAwIDAtLjI2My0uMTY0Ljk0NC45NDQgMCAwIDAtLjMzMS0uMDU1LjkuOSAwIDAgMC0uNC4wODYuNzEzLjcxMyAwIDAgMC0uMjkzLjI2MmMtLjA3OC4xMjEtLjEzNy4yOC0uMTc4LjQ3NWEzLjU0NSAzLjU0NSAwIDAgMC0uMDYxLjcxNHYuOTY2YzAgLjIyMy4wMTIuNDIuMDM3LjU5MS4wMjguMTcuMDY3LjMxOS4xMi40NDQuMDUyLjEyMy4xMTYuMjI0LjE5MS4zMDQuMDc1LjA4LjE2Mi4xMzkuMjYuMTc4LjEuMDM2LjIxLjA1NC4zMzEuMDU0YS44OC44OCAwIDAgMCAuNDA2LS4wODkuNzMuNzMgMCAwIDAgLjI5LS4yNzZjLjA4LS4xMjguMTM5LS4yOS4xNzgtLjQ4OS4wMzktLjIuMDU4LS40MzkuMDU4LS43MTdabTQuOTM5IDEuNTAzdi41MTloLTMuMjU0di0uNDU0bDEuNjI5LTEuODE0Yy4yLS4yMjMuMzU1LS40MTIuNDY0LS41NjdhMS43NCAxLjc0IDAgMCAwIC4yMzItLjQyIDEuMSAxLjEgMCAwIDAgLjA2OC0uMzgyLjk1Ni45NTYgMCAwIDAtLjEwMi0uNDQ0Ljc2Ny43NjcgMCAwIDAtLjI5NC0uMzIxLjg4Mi44ODIgMCAwIDAtLjQ3MS0uMTIgMS4wOCAxLjA4IDAgMCAwLS41NTMuMTMuNzk4Ljc5OCAwIDAgMC0uMzI4LjM1NSAxLjIwNSAxLjIwNSAwIDAgMC0uMTA5LjUyNmgtLjYzMmMwLS4yOC4wNjEtLjUzNi4xODQtLjc2OC4xMjMtLjIzMi4zMDUtLjQxNy41NDctLjU1My4yNDEtLjE0LjUzOC0uMjA5Ljg5MS0uMjA5LjMxNCAwIC41ODMuMDU2LjgwNi4xNjguMjIzLjEwOS4zOTQuMjY0LjUxMi40NjQuMTIxLjE5OC4xODEuNDMuMTgxLjY5N2ExLjQgMS40IDAgMCAxLS4wNzUuNDQ0IDIuMjg5IDIuMjg5IDAgMCAxLS4yMDEuNDQ0IDMuNjI4IDMuNjI4IDAgMCAxLS4yOTcuNDM3IDcuMTYgNy4xNiAwIDAgMS0uMzU5LjQyM2wtMS4zMzIgMS40NDVoMi40OTNabTEuMjgyLTQuNDUzaC42MzhsMS42MjkgNC4wNTMgMS42MjYtNC4wNTNoLjY0MmwtMi4wMjIgNC45NzJoLS40OTlsLTIuMDE0LTQuOTcyWm0tLjIwOSAwaC41NjRsLjA5MiAzLjAzMnYxLjk0aC0uNjU2di00Ljk3MlptNC4zODUgMGguNTY0djQuOTcyaC0uNjU2di0xLjk0bC4wOTItMy4wMzJabTYuMDI2IDAtMi4wNzMgNS4zOTloLS41NDNsMi4wNzYtNS40aC41NFptNi4wOCA0LjQ1M3YuNTE5aC0zLjI1NHYtLjQ1NGwxLjYyOS0xLjgxNGMuMi0uMjIzLjM1NS0uNDEyLjQ2NC0uNTY3LjExMi0uMTU3LjE4OS0uMjk3LjIzMy0uNDIuMDQ1LS4xMjUuMDY4LS4yNTIuMDY4LS4zODJhLjk0NC45NDQgMCAwIDAtLjEwMy0uNDQ0Ljc3MS43NzEgMCAwIDAtLjI5My0uMzIxLjg4NS44ODUgMCAwIDAtLjQ3Mi0uMTJjLS4yMiAwLS40MDUuMDQ0LS41NTMuMTNhLjc5OC43OTggMCAwIDAtLjMyOC4zNTUgMS4yMiAxLjIyIDAgMCAwLS4xMDkuNTI2aC0uNjMyYzAtLjI4LjA2Mi0uNTM2LjE4NS0uNzY4YTEuMzYgMS4zNiAwIDAgMSAuNTQ2LS41NTNjLjI0MS0uMTQuNTM5LS4yMDkuODkxLS4yMDkuMzE1IDAgLjU4My4wNTYuODA2LjE2OC4yMjMuMTA5LjM5NC4yNjQuNTEzLjQ2NC4xMi4xOTguMTgxLjQzLjE4MS42OTcgMCAuMTQ2LS4wMjUuMjk0LS4wNzYuNDQ0YTIuMjMgMi4yMyAwIDAgMS0uMjAxLjQ0NCAzLjQxIDMuNDEgMCAwIDEtLjI5Ny40MzcgNi43OTggNi43OTggMCAwIDEtLjM1OS40MjNsLTEuMzMyIDEuNDQ1aDIuNDkzWm0xLjcwOS0xLjg0OC0uNTA2LS4xMy4yNS0yLjQ3NWgyLjU1MXYuNTg0aC0yLjAxNWwtLjE1IDEuMzUyYTEuODggMS44OCAwIDAgMSAuMzQ0LS4xNDdjLjE0Mi0uMDQ1LjMwMy0uMDY4LjQ4NS0uMDY4LjIzIDAgLjQzNi4wNC42MTkuMTIuMTgyLjA3Ny4zMzYuMTg4LjQ2NC4zMzQuMTMuMTQ2LjIyOS4zMjEuMjk3LjUyNi4wNjguMjA1LjEwMi40MzQuMTAyLjY4NiAwIC4yNC0uMDMzLjQ2LS4wOTkuNjYtLjA2My4yLS4xNi4zNzUtLjI5LjUyNS0uMTMuMTQ4LS4yOTMuMjYzLS40OTIuMzQ1YTEuNzgzIDEuNzgzIDAgMCAxLS42OTMuMTIzYy0uMiAwLS4zOS0uMDI3LS41Ny0uMDgyYTEuNDYgMS40NiAwIDAgMS0uNDc4LS4yNTYgMS4zOSAxLjM5IDAgMCAxLS4zNDItLjQzIDEuNzQ0IDEuNzQ0IDAgMCAxLS4xNjQtLjYwOGguNjAxYy4wMjguMTg3LjA4Mi4zNDQuMTY0LjQ3MWEuODAzLjgwMyAwIDAgMCAuMzIxLjI5Yy4xMzUuMDY0LjI5MS4wOTYuNDY4LjA5Ni4xNSAwIC4yODQtLjAyNi40LS4wNzhhLjc4OC43ODggMCAwIDAgLjI5My0uMjI2Yy4wOC0uMDk4LjE0LS4yMTYuMTgxLS4zNTUuMDQ0LS4xMzkuMDY1LS4yOTUuMDY1LS40NjggMC0uMTU3LS4wMjEtLjMwMy0uMDY1LS40MzdhMS4wMDMgMS4wMDMgMCAwIDAtLjE5NC0uMzUyLjg1MS44NTEgMCAwIDAtLjMxMS0uMjMyLjk5Ny45OTcgMCAwIDAtLjQyMy0uMDg1Yy0uMjEyIDAtLjM3My4wMjgtLjQ4Mi4wODVhMS44NSAxLjg1IDAgMCAwLS4zMzEuMjMyWm02LjQ4OS0uNTE1di43NThjMCAuNDA3LS4wMzYuNzUxLS4xMDkgMS4wMzEtLjA3My4yOC0uMTc4LjUwNi0uMzE0LjY3NmExLjIgMS4yIDAgMCAxLS40OTUuMzczIDEuNzY2IDEuNzY2IDAgMCAxLS42NDkuMTEyYy0uMTkyIDAtLjM2OC0uMDI0LS41My0uMDcyYTEuMjY0IDEuMjY0IDAgMCAxLS40MzctLjIyOCAxLjM5NSAxLjM5NSAwIDAgMS0uMzI4LS40MTcgMi4yNDQgMi4yNDQgMCAwIDEtLjIwOC0uNjIxIDQuNDYyIDQuNDYyIDAgMCAxLS4wNzItLjg1NHYtLjc1OGMwLS40MDguMDM3LS43NS4xMS0xLjAyNS4wNzUtLjI3NS4xODEtLjQ5Ni4zMTctLjY2Mi4xMzctLjE2OS4zMDEtLjI5LjQ5Mi0uMzYyLjE5NC0uMDczLjQxLS4xMS42NDktLjExLjE5MyAwIC4zNzEuMDI0LjUzMy4wNzIuMTY0LjA0Ni4zMDkuMTIuNDM3LjIyMi4xMjcuMS4yMzUuMjM1LjMyNC40MDMuMDkxLjE2Ni4xNjEuMzcuMjA4LjYxMS4wNDguMjQyLjA3Mi41MjUuMDcyLjg1Wm0tLjYzNS44NnYtLjk2NmMwLS4yMjMtLjAxNC0uNDItLjA0MS0uNTg4YTEuODQ4IDEuODQ4IDAgMCAwLS4xMTMtLjQzNy44Ny44NyAwIDAgMC0uMTkxLS4yOTMuNjc3LjY3NyAwIDAgMC0uMjYzLS4xNjQuOTQ0Ljk0NCAwIDAgMC0uMzMxLS4wNTUuODk4Ljg5OCAwIDAgMC0uNC4wODYuNzEzLjcxMyAwIDAgMC0uMjkzLjI2MmMtLjA3OC4xMjEtLjEzNy4yOC0uMTc4LjQ3NWEzLjU0NSAzLjU0NSAwIDAgMC0uMDYxLjcxNHYuOTY2YzAgLjIyMy4wMTIuNDIuMDM3LjU5MS4wMjcuMTcuMDY3LjMxOS4xMi40NDQuMDUyLjEyMy4xMTYuMjI0LjE5MS4zMDRhLjcyLjcyIDAgMCAwIC4yNTkuMTc4Ljk3Ljk3IDAgMCAwIC4zMzIuMDU0Yy4xNTQgMCAuMjktLjAzLjQwNi0uMDg5YS43My43MyAwIDAgMCAuMjktLjI3NmMuMDgtLjEyOC4xMzktLjI5LjE3OC0uNDg5LjAzOS0uMi4wNTgtLjQzOS4wNTgtLjcxN1ptMi4wNTMtMi45NWguNjM5bDEuNjI5IDQuMDUzIDEuNjI1LTQuMDUzaC42NDJsLTIuMDIxIDQuOTcyaC0uNDk5bC0yLjAxNS00Ljk3MlptLS4yMDggMGguNTYzbC4wOTMgMy4wMzJ2MS45NGgtLjY1NnYtNC45NzJabTQuMzg1IDBoLjU2M3Y0Ljk3MmgtLjY1NXYtMS45NGwuMDkyLTMuMDMyWiIvPjxnIGZpbGw9IiMxOTgwMzgiIGNsaXAtcGF0aD0idXJsKCNtKSI+PHJlY3Qgd2lkdGg9Ijg2LjAxMiIgaGVpZ2h0PSI0LjY2MyIgeD0iMTA0LjY2MyIgeT0iOTUuNDU5IiBmaWxsLW9wYWNpdHk9Ii4wNiIgcng9IjIuMzMxIi8+PHBhdGggZD0iTTEwNC42NjMgOTQuMjkzaDM5LjAxMnY2Ljk5NGgtMzkuMDEyeiIvPjwvZz48cGF0aCBmaWxsPSIjMTk4MDM4IiBkPSJNMTA4LjM5OSAxMDguOTE3di41MzZoLTIuNjMzdi0uNTM2aDIuNjMzWm0tMi41LTQuNDM2djQuOTcyaC0uNjU5di00Ljk3MmguNjU5Wm0yLjE1MSAyLjEzOHYuNTM2aC0yLjI4NHYtLjUzNmgyLjI4NFptLjMxNC0yLjEzOHYuNTM5aC0yLjU5OHYtLjUzOWgyLjU5OFptMS42MiAyLjA2NnYyLjkwNmgtLjYzMnYtMy42OTVoLjU5OGwuMDM0Ljc4OVptLS4xNS45MTktLjI2My0uMDExYy4wMDItLjI1Mi4wNC0uNDg2LjExMy0uNy4wNzItLjIxNi4xNzUtLjQwNC4zMDctLjU2M2ExLjM2OCAxLjM2OCAwIDAgMSAxLjA4Mi0uNTAyYy4xODMgMCAuMzQ2LjAyNS40OTIuMDc1LjE0Ni4wNDguMjcuMTI1LjM3Mi4yMzIuMTA1LjEwNy4xODUuMjQ2LjIzOS40MTcuMDU1LjE2OC4wODIuMzc0LjA4Mi42MTh2Mi40MjFoLS42MzV2LTIuNDI4YzAtLjE5My0uMDI4LS4zNDgtLjA4NS0uNDY0YS41MjIuNTIyIDAgMCAwLS4yNDktLjI1Ni44OTQuODk0IDAgMCAwLS40MDMtLjA4Mi45NC45NCAwIDAgMC0uNzYyLjM3MiAxLjM2NiAxLjM2NiAwIDAgMC0uMjE1LjM5OWMtLjA1LjE0OC0uMDc1LjMwNS0uMDc1LjQ3MlptNS43OTYgMS4zNTV2LTEuOTAyYS43NzUuNzc1IDAgMCAwLS4wODktLjM3OS41ODcuNTg3IDAgMCAwLS4yNTktLjI1My45NDguOTQ4IDAgMCAwLS40MzEtLjA4OGMtLjE1OSAwLS4yOTkuMDI3LS40Mi4wODJhLjczMS43MzEgMCAwIDAtLjI4LjIxNS40NzEuNDcxIDAgMCAwLS4wOTkuMjg3aC0uNjMyYzAtLjEzMi4wMzUtLjI2My4xMDMtLjM5M2ExLjE0IDEuMTQgMCAwIDEgLjI5NC0uMzUyYy4xMjktLjEwNy4yODQtLjE5MS40NjQtLjI1My4xODItLjA2My4zODUtLjA5NS42MDgtLjA5NS4yNjggMCAuNTA1LjA0NS43MS4xMzYuMjA3LjA5MS4zNjkuMjI5LjQ4NS40MTQuMTE4LjE4Mi4xNzguNDExLjE3OC42ODZ2MS43MjFjMCAuMTIzLjAxLjI1NC4wMy4zOTMuMDIzLjEzOS4wNTYuMjU4LjA5OS4zNTh2LjA1NWgtLjY1OWExLjE2IDEuMTYgMCAwIDEtLjA3NS0uMjkgMi4zNzMgMi4zNzMgMCAwIDEtLjAyNy0uMzQyWm0uMTA5LTEuNjA4LjAwNy40NDRoLS42MzljLS4xNzkgMC0uMzQuMDE1LS40ODEuMDQ0YTEuMTEgMS4xMSAwIDAgMC0uMzU1LjEyNy41Ny41NyAwIDAgMC0uMjk0LjUxMi42NC42NCAwIDAgMCAuMDc5LjMxNy41Ny41NyAwIDAgMCAuMjM1LjIyOS44NTIuODUyIDAgMCAwIC4zOTMuMDgyYy4xOTMgMCAuMzY0LS4wNDEuNTEyLS4xMjMuMTQ4LS4wODIuMjY1LS4xODIuMzUyLS4zYS42NDIuNjQyIDAgMCAwIC4xNDMtLjM0NWwuMjcuMzA0YS45MTMuOTEzIDAgMCAxLS4xMy4zMTcgMS41MTUgMS41MTUgMCAwIDEtLjcuNTk4IDEuMzYgMS4zNiAwIDAgMS0uNTM5LjEwMmMtLjI1MSAwLS40Ny0uMDQ5LS42NTktLjE0NmExLjEyIDEuMTIgMCAwIDEtLjQzNy0uMzkzIDEuMDM1IDEuMDM1IDAgMCAxLS4xNTQtLjU1N2MwLS4xOTguMDM5LS4zNzIuMTE2LS41MjJhLjk5OS45OTkgMCAwIDEgLjMzNS0uMzc5Yy4xNDUtLjEwMy4zMjEtLjE4LjUyNi0uMjMzLjIwNC0uMDUyLjQzMy0uMDc4LjY4Ni0uMDc4aC43MzRabTEuNzQ2LTMuMDA1aC42MzV2NC41MjhsLS4wNTQuNzE3aC0uNTgxdi01LjI0NVptMy4xMzIgMy4zNjd2LjA3MmMwIC4yNjgtLjAzMi41MTgtLjA5Ni43NDdhMS44NDkgMS44NDkgMCAwIDEtLjI4LjU5NSAxLjMwMSAxLjMwMSAwIDAgMS0uNDUxLjM5MmMtLjE3Ny4wOTQtLjM4MS4xNC0uNjExLjE0LS4yMzUgMC0uNDQxLS4wMzktLjYxOC0uMTE5YTEuMjE3IDEuMjE3IDAgMCAxLS40NDQtLjM1MiAxLjc4OCAxLjc4OCAwIDAgMS0uMjktLjU1MyAzLjQzNiAzLjQzNiAwIDAgMS0uMTQ3LS43MzF2LS4zMTRhMy40NCAzLjQ0IDAgMCAxIC4xNDctLjczNGMuMDcyLS4yMTcuMTY5LS40MDEuMjktLjU1My4xMjEtLjE1NS4yNjktLjI3My40NDQtLjM1Mi4xNzUtLjA4Mi4zNzktLjEyMy42MTEtLjEyMy4yMzIgMCAuNDM4LjA0NS42MTguMTM2LjE4LjA4OS4zMy4yMTcuNDUxLjM4My4xMjMuMTY2LjIxNi4zNjUuMjguNTk4LjA2NC4yMjkuMDk2LjQ4Ni4wOTYuNzY4Wm0tLjYzNi4wNzJ2LS4wNzJhMi41MSAyLjUxIDAgMCAwLS4wNTEtLjUxOSAxLjMzMyAxLjMzMyAwIDAgMC0uMTY0LS40My44MDUuODA1IDAgMCAwLS4yOTctLjI5NC44NzUuODc1IDAgMCAwLS40NTQtLjEwOS44OTUuODk1IDAgMCAwLS43MTQuMzAzIDEuMTc2IDEuMTc2IDAgMCAwLS4yMDEuMzE1IDEuNzkgMS43OSAwIDAgMC0uMTEzLjM2MnYuODIzYy4wMzcuMTU5LjA5Ni4zMTMuMTc4LjQ2MS4wODQuMTQ1LjE5Ni4yNjUuMzM0LjM1OC4xNDIuMDk0LjMxNi4xNC41MjMuMTRhLjg3Ny44NzcgMCAwIDAgLjQzNy0uMTAyLjgzMy44MzMgMCAwIDAgLjI5Ny0uMjkgMS4zNiAxLjM2IDAgMCAwIC4xNzEtLjQyN2MuMDM2LS4xNjIuMDU0LS4zMzUuMDU0LS41MTlabTIuMzU0LTMuNDM5djUuMjQ1aC0uNjM1di01LjI0NWguNjM1Wm0yLjc4MSA1LjMxM2MtLjI1NyAwLS40OTEtLjA0My0uNy0uMTI5YTEuNTgyIDEuNTgyIDAgMCAxLS44NzgtLjkzOSAyLjEgMi4xIDAgMCAxLS4xMTktLjcxOHYtLjE0M2MwLS4zMDEuMDQ0LS41NjguMTMzLS44MDNhMS44IDEuOCAwIDAgMSAuMzYyLS42MDFjLjE1Mi0uMTY0LjMyNS0uMjg4LjUxOS0uMzcyLjE5NC0uMDg0LjM5NC0uMTI2LjYwMS0uMTI2LjI2NCAwIC40OTIuMDQ1LjY4My4xMzYuMTk0LjA5MS4zNTIuMjE5LjQ3NS4zODMuMTIzLjE2Mi4yMTQuMzUzLjI3My41NzQuMDU5LjIxOC4wODkuNDU3LjA4OS43MTd2LjI4M2gtMi43NnYtLjUxNWgyLjEyOHYtLjA0OGExLjU0NCAxLjU0NCAwIDAgMC0uMTAzLS40NzguODQ2Ljg0NiAwIDAgMC0uMjczLS4zODNjLS4xMjUtLjEtLjI5Ni0uMTUtLjUxMi0uMTVhLjg2Ny44NjcgMCAwIDAtLjcwNy4zNTggMS4zNCAxLjM0IDAgMCAwLS4yMDEuNDM0IDIuMTk0IDIuMTk0IDAgMCAwLS4wNzIuNTkxdi4xNDNjMCAuMTc2LjAyNC4zNDEuMDcyLjQ5Ni4wNS4xNTIuMTIxLjI4Ni4yMTUuNDAzLjA5NS4xMTYuMjEuMjA3LjM0NS4yNzMuMTM2LjA2Ni4yOTEuMDk5LjQ2NC4wOTkuMjIzIDAgLjQxMi0uMDQ2LjU2Ny0uMTM3LjE1NS0uMDkxLjI5LS4yMTMuNDA2LS4zNjVsLjM4My4zMDRjLS4wOC4xMi0uMTgxLjIzNS0uMzA0LjM0NWExLjQ0NyAxLjQ0NyAwIDAgMS0uNDU0LjI2NiAxLjc2NyAxLjc2NyAwIDAgMS0uNjMyLjEwMlptNC43MzctLjc4NXYtNC41MjhoLjYzNnY1LjI0NWgtLjU4MWwtLjA1NS0uNzE3Wm0tMi40ODYtMS4wODl2LS4wNzJjMC0uMjgyLjAzNS0uNTM5LjEwMy0uNzY4YTEuODMgMS44MyAwIDAgMSAuMjk3LS41OThjLjEzLS4xNjYuMjgzLS4yOTQuNDYxLS4zODMuMTgtLjA5MS4zOC0uMTM2LjYwMS0uMTM2LjIzMiAwIC40MzUuMDQxLjYwOC4xMjMuMTc1LjA3OS4zMjMuMTk3LjQ0NC4zNTIuMTIzLjE1Mi4yMi4zMzYuMjkuNTUzLjA3MS4yMTYuMTIuNDYxLjE0Ny43MzR2LjMxNGEzLjIwNSAzLjIwNSAwIDAgMS0uMTQ3LjczMWMtLjA3LjIxNi0uMTY3LjQwMS0uMjkuNTUzLS4xMjEuMTUzLS4yNjkuMjctLjQ0NC4zNTItLjE3NS4wOC0uMzguMTE5LS42MTUuMTE5LS4yMTYgMC0uNDE0LS4wNDYtLjU5NC0uMTRhMS40IDEuNCAwIDAgMS0uNDYxLS4zOTIgMS44OTcgMS44OTcgMCAwIDEtLjI5Ny0uNTk1IDIuNjE5IDIuNjE5IDAgMCAxLS4xMDMtLjc0N1ptLjYzNi0uMDcydi4wNzJjMCAuMTg0LjAxOC4zNTcuMDU0LjUxOS4wMzkuMTYxLjA5OC4zMDQuMTc4LjQyN2EuODg3Ljg4NyAwIDAgMCAuMzA0LjI5Yy4xMjMuMDY4LjI3LjEwMi40NC4xMDIuMjEgMCAuMzgyLS4wNDQuNTE2LS4xMzNhLjk5Ljk5IDAgMCAwIC4zMjgtLjM1MmMuMDgyLS4xNDUuMTQ1LS4zMDQuMTkxLS40NzR2LS44MjNhMS43NDggMS43NDggMCAwIDAtLjEyLS4zNjIgMS4xMiAxLjEyIDAgMCAwLS4xOTgtLjMxNS44MzguODM4IDAgMCAwLS4yOTctLjIyMS45NTcuOTU3IDAgMCAwLS40MTMtLjA4Mi44NzEuODcxIDAgMCAwLS40NDcuMTA5Ljg1NS44NTUgMCAwIDAtLjMwNC4yOTRjLS4wOC4xMjItLjEzOS4yNjYtLjE3OC40M2EyLjM3NSAyLjM3NSAwIDAgMC0uMDU0LjUxOVpNMTg2LjAxMiAxMDMuNTY4YTMuODg3IDMuODg3IDAgMCAwIDAgNy43NzEgMy44ODcgMy44ODcgMCAwIDAgMC03Ljc3MVptLS43NzcgNS44MjktMS45NDMtMS45NDMuNTQ4LS41NDggMS4zOTUgMS4zOTEgMi45NDktMi45NDkuNTQ4LjU1Mi0zLjQ5NyAzLjQ5N1oiLz48L2c+PHBhdGggZmlsbD0iIzAwMCIgZmlsbC1vcGFjaXR5PSIuMTIiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTTIwMCAxMTcuNjk5SDB2LS41ODNoMjAwdi41ODNaIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiLz48ZyBjbGlwLXBhdGg9InVybCgjbikiPjxwYXRoIGZpbGw9IiMzMDU2ODAiIGQ9Ik0xMi4zMDEgMTMyLjI4M3YtNC4wNTloLjk5NnY0LjA1OWMwIC4zODUtLjA4Mi43MTQtLjI0Ny45ODQtLjE2NC4yNzEtLjM4OS40NzktLjY3My42MjJhMi4xMjQgMi4xMjQgMCAwIDEtLjk2OC4yMTVjLS4zNyAwLS42OTctLjA2Mi0uOTg0LS4xODdhMS40NyAxLjQ3IDAgMCAxLS42Ny0uNTc0Yy0uMTYyLS4yNTgtLjI0My0uNTg0LS4yNDMtLjk4aDEuMDA1YzAgLjIyOC4wMzUuNDEzLjEwNy41NTRhLjY4Ny42ODcgMCAwIDAgLjMxLjMwM2MuMTM2LjA2MS4yOTQuMDkxLjQ3NS4wOTFhLjg5Ny44OTcgMCAwIDAgLjQ1OC0uMTE1LjgzLjgzIDAgMCAwIC4zMTktLjM0N2MuMDc3LS4xNTQuMTE1LS4zNDMuMTE1LS41NjZabTQuNjI4Ljg3N3YtMi4wNTZhLjg3OC44NzggMCAwIDAtLjA4My0uMzk4LjU4My41ODMgMCAwIDAtLjI1NS0uMjU5Ljg3My44NzMgMCAwIDAtLjQyMy0uMDkyLjk1OC45NTggMCAwIDAtLjQwNi4wOC42NTguNjU4IDAgMCAwLS4yNjcuMjE1LjUxOC41MTggMCAwIDAtLjA5Ni4zMDdoLS45NTZjMC0uMTcuMDQxLS4zMzUuMTI0LS40OTQuMDgyLS4xNi4yMDEtLjMwMi4zNTgtLjQyNy4xNTctLjEyNS4zNDQtLjIyMy41NjItLjI5NS4yMTgtLjA3MS40NjItLjEwNy43MzMtLjEwNy4zMjQgMCAuNjEuMDU0Ljg2LjE2My4yNTMuMTA5LjQ1LjI3NC41OTQuNDk0LjE0Ni4yMTguMjIuNDkyLjIyLjgyMXYxLjkxNmMwIC4xOTcuMDEzLjM3NC4wNC41My4wMjguMTU0LjA3LjI4OC4xMjMuNDAzdi4wNjNoLS45ODRhMS42OTMgMS42OTMgMCAwIDEtLjEwOC0uMzk0IDMuMjI0IDMuMjI0IDAgMCAxLS4wMzYtLjQ3Wm0uMTQtMS43NTcuMDA4LjU5M2gtLjY5YTEuOTQgMS45NCAwIDAgMC0uNDcuMDUyLjk2NC45NjQgMCAwIDAtLjMzOC4xNDQuNjExLjYxMSAwIDAgMC0uMjA0LjIzMS42NzQuNjc0IDAgMCAwLS4wNjcuMzA3YzAgLjExNC4wMjYuMjE5LjA4LjMxNC4wNTMuMDkzLjEzLjE2Ni4yMy4yMTkuMTA0LjA1NC4yMjkuMDguMzc1LjA4LjE5NiAwIC4zNjgtLjA0LjUxNC0uMTE5LjE0OS0uMDgzLjI2NS0uMTgyLjM1LS4yOTlhLjY1NC42NTQgMCAwIDAgLjEzNi0uMzM5bC4zMS40MjZhMS40NjMgMS40NjMgMCAwIDEtLjE2My4zNTEgMS43MiAxLjcyIDAgMCAxLS4zMDIuMzU5Yy0uMTIzLjExMS0uMjcuMjAzLS40NDMuMjc1YTEuNTIgMS41MiAwIDAgMS0uNTkuMTA3Yy0uMjggMC0uNTMyLS4wNTYtLjc1Mi0uMTY3YTEuMzM5IDEuMzM5IDAgMCAxLS41MTgtLjQ1OCAxLjE5IDEuMTkgMCAwIDEtLjE4OC0uNjU4YzAtLjIyOC4wNDMtLjQzLjEyOC0uNjA1LjA4OC0uMTc4LjIxNS0uMzI3LjM4My0uNDQ3LjE3LS4xMTkuMzc3LS4yMDkuNjIxLS4yNzFhMy4zNiAzLjM2IDAgMCAxIC44MzctLjA5NWguNzUzWm0zLjMxMyAxLjg2IDEuMDU1LTMuNTQ5aC45OTdsLTEuNDk4IDQuMzFoLS42MjJsLjA2OC0uNzYxWm0tLjgwOS0zLjU0OSAxLjA3NiAzLjU2NS4wNTIuNzQ1aC0uNjIybC0xLjUwNi00LjMxaDFabTUuOTY2IDMuNDQ2di0yLjA1NmEuODc4Ljg3OCAwIDAgMC0uMDgzLS4zOTguNTgzLjU4MyAwIDAgMC0uMjU1LS4yNTkuODczLjg3MyAwIDAgMC0uNDIyLS4wOTIuOTU4Ljk1OCAwIDAgMC0uNDA3LjA4LjY1OC42NTggMCAwIDAtLjI2Ny4yMTUuNTE4LjUxOCAwIDAgMC0uMDk1LjMwN2gtLjk1N2MwLS4xNy4wNDItLjMzNS4xMjQtLjQ5NC4wODItLjE2LjIwMi0uMzAyLjM1OC0uNDI3LjE1Ny0uMTI1LjM0NC0uMjIzLjU2Mi0uMjk1LjIxOC0uMDcxLjQ2Mi0uMTA3LjczMy0uMTA3LjMyNCAwIC42MTEuMDU0Ljg2LjE2My4yNTMuMTA5LjQ1MS4yNzQuNTk1LjQ5NC4xNDYuMjE4LjIxOS40OTIuMjE5LjgyMXYxLjkxNmMwIC4xOTcuMDEzLjM3NC4wNC41My4wMjkuMTU0LjA3LjI4OC4xMjMuNDAzdi4wNjNoLS45ODRhMS42OTggMS42OTggMCAwIDEtLjEwOC0uMzk0IDMuMjI0IDMuMjI0IDAgMCAxLS4wMzUtLjQ3Wm0uMTQtMS43NTcuMDA4LjU5M2gtLjY5YTEuOTQgMS45NCAwIDAgMC0uNDcuMDUyLjk2NC45NjQgMCAwIDAtLjMzOC4xNDQuNjExLjYxMSAwIDAgMC0uMjAzLjIzMS42NzQuNjc0IDAgMCAwLS4wNjguMzA3YzAgLjExNC4wMjcuMjE5LjA4LjMxNC4wNTMuMDkzLjEzLjE2Ni4yMy4yMTkuMTA0LjA1NC4yMy4wOC4zNzUuMDguMTk3IDAgLjM2OC0uMDQuNTE0LS4xMTkuMTQ5LS4wODMuMjY2LS4xODIuMzUtLjI5OWEuNjU0LjY1NCAwIDAgMCAuMTM2LS4zMzlsLjMxMS40MjZhMS40NiAxLjQ2IDAgMCAxLS4xNjMuMzUxIDEuNzE2IDEuNzE2IDAgMCAxLS4zMDMuMzU5Yy0uMTIyLjExMS0uMjcuMjAzLS40NDIuMjc1YTEuNTIgMS41MiAwIDAgMS0uNTkuMTA3Yy0uMjgyIDAtLjUzMy0uMDU2LS43NTMtLjE2N2ExLjMzOSAxLjMzOSAwIDAgMS0uNTE4LS40NTggMS4xOSAxLjE5IDAgMCAxLS4xODctLjY1OGMwLS4yMjguMDQyLS40My4xMjctLjYwNS4wODgtLjE3OC4yMTUtLjMyNy4zODMtLjQ0N2ExLjg4IDEuODggMCAwIDEgLjYyMS0uMjcxIDMuMzYgMy4zNiAwIDAgMSAuODM3LS4wOTVoLjc1M1ptNS4xMjIgMS4xMjdhLjg1Ljg1IDAgMCAwLS4wNTYtLjMxOC42MjEuNjIxIDAgMCAwLS4xODctLjI1NSAxLjUyNCAxLjUyNCAwIDAgMC0uMzgzLS4yMjMgNC45NTggNC45NTggMCAwIDAtLjYyMS0uMjI4IDcuMDIyIDcuMDIyIDAgMCAxLS43NjUtLjI4MiAyLjk1MyAyLjk1MyAwIDAgMS0uNjA2LS4zNjcgMS41NjcgMS41NjcgMCAwIDEtLjQwMi0uNDgyIDEuMzQ5IDEuMzQ5IDAgMCAxLS4xNDQtLjYzNGMwLS4yMzYuMDUtLjQ1MS4xNDgtLjY0NS4xLS4xOTQuMjQzLS4zNjEuNDI2LS41MDJhMi4wNSAyLjA1IDAgMCAxIC42NTgtLjMzMSAyLjc5IDIuNzkgMCAwIDEgLjgzNi0uMTE5Yy40MyAwIC44MDEuMDggMS4xMTIuMjM5LjMxMy4xNTkuNTU0LjM3My43Mi42NDEuMTcuMjY5LjI1Ni41NjUuMjU2Ljg4OUgzMC44YzAtLjE5MS0uMDQxLS4zNi0uMTI0LS41MDZhLjgzNy44MzcgMCAwIDAtLjM2Ni0uMzUxYy0uMTYyLS4wODUtLjM2OC0uMTI3LS42MTgtLjEyN2ExLjQzIDEuNDMgMCAwIDAtLjU5LjEwNy43OS43OSAwIDAgMC0uMzUuMjkxLjc2Ljc2IDAgMCAwLS4xMTYuNDE0YzAgLjEwOS4wMjYuMjA5LjA3Ni4yOTlhLjgzLjgzIDAgMCAwIC4yMzEuMjQ3Yy4xMDQuMDc1LjIzNC4xNDUuMzkuMjExLjE1Ny4wNjcuMzQyLjEzMS41NTUuMTkyLjMyLjA5NS42MDEuMjAyLjg0LjMxOC4yNC4xMTUuNDM4LjI0NS41OTguMzkxYTEuNDQyIDEuNDQyIDAgMCAxIC40NzggMS4xMjNjMCAuMjQ1LS4wNS40NjUtLjE0OC42NjItLjA5OC4xOTQtLjIzOS4zNi0uNDIyLjQ5OC0uMTguMTM1LS4zOTguMjQtLjY1My4zMTVhMy4xMTUgMy4xMTUgMCAwIDEtLjg0NS4xMDdjLS4yNzkgMC0uNTU0LS4wMzctLjgyNS0uMTExYTIuNDU4IDIuNDU4IDAgMCAxLS43MzMtLjMzOSAxLjc0NCAxLjc0NCAwIDAgMS0uNTI2LS41NzRjLS4xMy0uMjMxLS4xOTUtLjUtLjE5NS0uODA5aDFjMCAuMTg5LjAzMi4zNS4wOTYuNDgyYS44ODguODg4IDAgMCAwIC4yNzUuMzI3Yy4xMTYuMDgzLjI1Mi4xNDQuNDA2LjE4My4xNTcuMDQuMzI0LjA2LjUwMi4wNi4yMzQgMCAuNDI5LS4wMzMuNTg2LS4wOTlhLjc4Ljc4IDAgMCAwIC4zNTgtLjI3OWMuMDgtLjEyLjEyLS4yNTguMTItLjQxNVptMy43LjgwOWMuMTU2IDAgLjI5Ny0uMDMuNDIyLS4wOTFhLjgwNy44MDcgMCAwIDAgLjMwNi0uMjYzLjcyMS43MjEgMCAwIDAgLjEzMi0uMzg3aC45MDRhMS4zNDYgMS4zNDYgMCAwIDEtLjI0Ny43NjFjLS4xNTkuMjI4LS4zNy40MS0uNjMzLjU0NmExLjkwNSAxLjkwNSAwIDAgMS0uODczLjE5OWMtLjMyOSAwLS42MTYtLjA1Ni0uODYtLjE2N2ExLjcgMS43IDAgMCAxLS42MS0uNDcgMi4wNzUgMi4wNzUgMCAwIDEtLjM2Ni0uNjljLS4wOC0uMjYtLjEyLS41MzktLjEyLS44MzZ2LS4xNGMwLS4yOTcuMDQtLjU3Ni4xMi0uODM2LjA4Mi0uMjYzLjIwNC0uNDk0LjM2Ni0uNjk0YTEuNjcgMS42NyAwIDAgMSAuNjEtLjQ2NmMuMjQ0LS4xMTQuNTMtLjE3MS44NTYtLjE3MS4zNDYgMCAuNjQ4LjA2OS45MDkuMjA3LjI2LjEzNi40NjUuMzI1LjYxMy41Ny4xNTIuMjQyLjIzLjUyMy4yMzUuODQ0aC0uOTA0YS45NjYuOTY2IDAgMCAwLS4xMi0uNDMuNzkuNzkgMCAwIDAtLjI5NC0uMzExLjg0Ljg0IDAgMCAwLS40NS0uMTE1LjkuOSAwIDAgMC0uNDgzLjExOS44MDcuODA3IDAgMCAwLS4yOTkuMzE5IDEuNTU4IDEuNTU4IDAgMCAwLS4xNTUuNDVjLS4wMy4xNjUtLjA0NC4zMzYtLjA0NC41MTR2LjE0YzAgLjE3OC4wMTUuMzUuMDQ0LjUxOC4wMy4xNjcuMDguMzE3LjE1MS40NS4wNzUuMTMuMTc2LjIzNS4zMDMuMzE1LjEyOC4wNzcuMjkuMTE1LjQ4Ni4xMTVabTMuNjExLTIuODA1djMuNDloLS45NnYtNC4zMWguOTE2bC4wNDQuODJabTEuMzE5LS44NDgtLjAwOC44OTJhMi4zNCAyLjM0IDAgMCAwLS4zOS0uMDMyYy0uMTY1IDAtLjMxLjAyNC0uNDM1LjA3MmEuODI0LjgyNCAwIDAgMC0uMzE0LjE5OS44NzkuODc5IDAgMCAwLS4xOTIuMzExIDEuMzg2IDEuMzg2IDAgMCAwLS4wOC40MWwtLjIxOC4wMTZjMC0uMjcxLjAyNi0uNTIyLjA4LS43NTMuMDUyLS4yMzEuMTMyLS40MzQuMjM4LS42MDkuMTEtLjE3Ni4yNDUtLjMxMi40MDctLjQxMS4xNjQtLjA5OC4zNTQtLjE0Ny41Ny0uMTQ3LjA1OCAwIC4xMi4wMDUuMTg3LjAxNmEuNzEuNzEgMCAwIDEgLjE1NS4wMzZabTEuNzc1LjAyOHY0LjMxaC0uOTY0di00LjMxaC45NjRabS0xLjAyOC0xLjEzMmEuNDkuNDkgMCAwIDEgLjE0NC0uMzYyLjU0Ni41NDYgMCAwIDEgLjQwNi0uMTQ4Yy4xNyAwIC4zMDQuMDQ5LjQwMy4xNDhhLjQ4My40ODMgMCAwIDEgLjE0Ny4zNjIuNDguNDggMCAwIDEtLjE0Ny4zNTkuNTU1LjU1NSAwIDAgMS0uNDAzLjE0My41Ni41NiAwIDAgMS0uNDA2LS4xNDMuNDg3LjQ4NyAwIDAgMS0uMTQ0LS4zNTlabTMuMTkgMS45NnY1LjE0aC0uOTZ2LTUuOTY4aC44ODRsLjA3Ni44MjhabTIuODA5IDEuMjg3di4wODRjMCAuMzEzLS4wMzcuNjA0LS4xMTIuODcyYTIuMTQgMi4xNCAwIDAgMS0uMzIyLjY5OGMtLjE0MS4xOTYtLjMxNS4zNDktLjUyMy40NThhMS41MTcgMS41MTcgMCAwIDEtLjcxNy4xNjNjLS4yNjggMC0uNTAzLS4wNDktLjcwNS0uMTQ3YTEuNDQgMS40NCAwIDAgMS0uNTA2LS40MjcgMi4zMSAyLjMxIDAgMCAxLS4zMzQtLjY0NSA0LjEzNyA0LjEzNyAwIDAgMS0uMTc2LS44MjF2LS4zMjJjLjAzNS0uMzE3LjA5My0uNjAzLjE3Ni0uODYxLjA4NS0uMjU4LjE5Ni0uNDc5LjMzNC0uNjY1LjEzOC0uMTg2LjMwNy0uMzMuNTA2LS40MzEuMi0uMTAxLjQzMi0uMTUxLjY5Ny0uMTUxLjI3MSAwIC41MTIuMDUzLjcyMi4xNTkuMjEuMTA0LjM4Ni4yNTMuNTMuNDQ2LjE0My4xOTIuMjUuNDIzLjMyMi42OTQuMDcyLjI2OC4xMDguNTY3LjEwOC44OTZabS0uOTYuMDg0di0uMDg0YzAtLjE5OS0uMDE5LS4zODQtLjA1Ni0uNTU0YTEuNDUgMS40NSAwIDAgMC0uMTc1LS40NTQuODM4LjgzOCAwIDAgMC0uNzQ5LS40MTRjLS4xNyAwLS4zMTcuMDI5LS40MzkuMDg3YS44NDQuODQ0IDAgMCAwLS4zMDcuMjM2Yy0uMDgyLjEtLjE0Ni4yMTktLjE5LjM1NGEyLjEzMyAyLjEzMyAwIDAgMC0uMDk2LjQzNHYuNzczYy4wMzEuMTkyLjA4Ni4zNjcuMTYzLjUyNi4wNzcuMTYuMTg2LjI4Ny4zMjcuMzgzYS45OTMuOTkzIDAgMCAwIC41NS4xMzljLjE3MiAwIC4zMi0uMDM3LjQ0Mi0uMTExYS44NzguODc4IDAgMCAwIC4yOTktLjMwN2MuMDgtLjEzMy4xMzgtLjI4Ni4xNzUtLjQ1OGEyLjYxIDIuNjEgMCAwIDAgLjA1Ni0uNTVabTMuODk4LTIuMTk5di43MDFoLTIuNDN2LS43MDFoMi40M1ptLTEuNzI5LTEuMDU2aC45NnY0LjE3NWMwIC4xMzMuMDE5LjIzNS4wNTYuMzA3LjA0LjA2OS4wOTQuMTE1LjE2My4xMzkuMDcuMDI0LjE1LjAzNi4yNDQuMDM2YTEuOTEgMS45MSAwIDAgMCAuMzM5LS4wMzZsLjAwMy43MzNhMi4zMSAyLjMxIDAgMCAxLS4yNzkuMDY0IDIuMDA5IDIuMDA5IDAgMCAxLS4zNTguMDI4Yy0uMjIgMC0uNDE2LS4wMzgtLjU4Ni0uMTE1YS44NjUuODY1IDAgMCAxLS4zOTgtLjM4N2MtLjA5Ni0uMTc4LS4xNDQtLjQxNC0uMTQ0LS43MDl2LTQuMjM1Wm02LjA5MiA1LjM2NmgtLjk2di00LjcyNWMwLS4zMjEuMDYtLjU5MS4xOC0uODA5LjEyMi0uMjIuMjk2LS4zODYuNTIyLS40OTguMjI1LS4xMTQuNDkyLS4xNzEuOC0uMTcxLjA5NiAwIC4xOS4wMDcuMjgzLjAyLjA5My4wMTEuMTg0LjAyOC4yNzEuMDUybC0uMDI0Ljc0MWExLjEgMS4xIDAgMCAwLS4xNzUtLjAyOCAyLjQ0MiAyLjQ0MiAwIDAgMC0uMi0uMDA4LjgwMS44MDEgMCAwIDAtLjM3OC4wODQuNTUuNTUgMCAwIDAtLjIzOS4yMzUuODMuODMgMCAwIDAtLjA4LjM4MnY0LjcyNVptLjg4OS00LjMxdi43MDFoLTIuNTF2LS43MDFoMi41MVptMy40MzcgMy4yOTR2LTMuMjk0aC45NjR2NC4zMWgtLjkwOWwtLjA1NS0xLjAxNlptLjEzNS0uODk2LjMyMy0uMDA4YzAgLjI5LS4wMzIuNTU3LS4wOTYuODAxYTEuODQ1IDEuODQ1IDAgMCAxLS4yOTUuNjMzIDEuMzggMS4zOCAwIDAgMS0uNTEuNDE5IDEuNzMgMS43MyAwIDAgMS0uNzQ1LjE0N2MtLjIxIDAtLjQwMi0uMDMtLjU3Ny0uMDkyYTEuMTg3IDEuMTg3IDAgMCAxLS40NTUtLjI4MiAxLjI5NSAxLjI5NSAwIDAgMS0uMjktLjQ5OCAyLjMwNSAyLjMwNSAwIDAgMS0uMTA0LS43MzR2LTIuNzg0aC45NnYyLjc5MmMwIC4xNTcuMDE5LjI4OS4wNTYuMzk1YS42NzUuNjc1IDAgMCAwIC4xNTEuMjUxLjUzLjUzIDAgMCAwIC4yMjQuMTM1Ljg5Ljg5IDAgMCAwIC4yNy4wNGMuMjc0IDAgLjQ5LS4wNTMuNjQ2LS4xNTlhLjg4Mi44ODIgMCAwIDAgLjMzOS0uNDM4IDEuNzUgMS43NSAwIDAgMCAuMTAzLS42MThabTIuOTEtMS40Nzh2My4zOWgtLjk2di00LjMxaC45MDVsLjA1Ni45MlptLS4xNyAxLjA3Ni0uMzExLS4wMDRhMi44IDIuOCAwIDAgMSAuMTI3LS44NDFjLjA4NS0uMjU1LjIwMi0uNDc0LjM1LS42NTdhMS41NSAxLjU1IDAgMCAxIC41NDMtLjQyM2MuMjEtLjEwMS40NDMtLjE1MS43LS4xNTEuMjA4IDAgLjM5NS4wMjkuNTYzLjA4OC4xNy4wNTUuMzE0LjE0Ny40MzQuMjc0LjEyMi4xMjguMjE1LjI5NC4yNzkuNDk4LjA2NC4yMDIuMDk1LjQ1MS4wOTUuNzQ2djIuNzg0aC0uOTY0di0yLjc4OGMwLS4yMDgtLjAzLS4zNzEtLjA5MS0uNDkxYS41MS41MSAwIDAgMC0uMjYtLjI1OC45NTguOTU4IDAgMCAwLS40MTgtLjA4LjkzLjkzIDAgMCAwLS43NzMuMzg2IDEuMzggMS4zOCAwIDAgMC0uMjAzLjQxNSAxLjcwOCAxLjcwOCAwIDAgMC0uMDcyLjUwMlptNS42NjcgMS42MjlhLjk1Ljk1IDAgMCAwIC40MjItLjA5MS44MDYuODA2IDAgMCAwIC4zMDctLjI2My43MjEuNzIxIDAgMCAwIC4xMzItLjM4N2guOTA0YTEuMzQ1IDEuMzQ1IDAgMCAxLS4yNDcuNzYxYy0uMTYuMjI4LS4zNy40MS0uNjMzLjU0NmExLjkwNSAxLjkwNSAwIDAgMS0uODczLjE5OWMtLjMzIDAtLjYxNi0uMDU2LS44Ni0uMTY3YTEuNzAzIDEuNzAzIDAgMCAxLS42MS0uNDcgMi4wNzMgMi4wNzMgMCAwIDEtLjM2Ny0uNjkgMi44NCAyLjg0IDAgMCAxLS4xMi0uODM2di0uMTRjMC0uMjk3LjA0LS41NzYuMTItLjgzNi4wODMtLjI2My4yMDUtLjQ5NC4zNjctLjY5NGExLjY3IDEuNjcgMCAwIDEgLjYxLS40NjZjLjI0NC0uMTE0LjUzLS4xNzEuODU2LS4xNzEuMzQ1IDAgLjY0OC4wNjkuOTA5LjIwNy4yNi4xMzYuNDY0LjMyNS42MTMuNTcuMTUxLjI0Mi4yMy41MjMuMjM1Ljg0NGgtLjkwNGEuOTY2Ljk2NiAwIDAgMC0uMTItLjQzLjc5MS43OTEgMCAwIDAtLjI5NS0uMzExLjg0MS44NDEgMCAwIDAtLjQ1LS4xMTUuOS45IDAgMCAwLS40ODIuMTE5LjgwNy44MDcgMCAwIDAtLjI5OS4zMTkgMS41NTggMS41NTggMCAwIDAtLjE1NS40NWMtLjAzLjE2NS0uMDQ0LjMzNi0uMDQ0LjUxNHYuMTRjMCAuMTc4LjAxNS4zNS4wNDQuNTE4LjAzLjE2Ny4wOC4zMTcuMTUxLjQ1LjA3NS4xMy4xNzYuMjM1LjMwMy4zMTUuMTI4LjA3Ny4yOS4xMTUuNDg2LjExNVptNC42MjQtMy42MjV2LjcwMWgtMi40M3YtLjcwMWgyLjQzWm0tMS43My0xLjA1NmguOTYxdjQuMTc1YzAgLjEzMy4wMTkuMjM1LjA1Ni4zMDcuMDQuMDY5LjA5NC4xMTUuMTYzLjEzOS4wNy4wMjQuMTUuMDM2LjI0My4wMzZhMS45MSAxLjkxIDAgMCAwIC4zMzktLjAzNmwuMDA0LjczM2EyLjMxIDIuMzEgMCAwIDEtLjI3OS4wNjQgMi4wMDkgMi4wMDkgMCAwIDEtLjM1OS4wMjhjLS4yMiAwLS40MTUtLjAzOC0uNTg1LS4xMTVhLjg2NS44NjUgMCAwIDEtLjM5OS0uMzg3Yy0uMDk1LS4xNzgtLjE0My0uNDE0LS4xNDMtLjcwOXYtNC4yMzVabTMuNjQ1IDEuMDU2djQuMzFoLS45NjV2LTQuMzFoLjk2NVptLTEuMDI4LTEuMTMyYS40OS40OSAwIDAgMSAuMTQzLS4zNjIuNTQ3LjU0NyAwIDAgMSAuNDA3LS4xNDhjLjE3IDAgLjMwNC4wNDkuNDAyLjE0OGEuNDgyLjQ4MiAwIDAgMSAuMTQ3LjM2Mi40OC40OCAwIDAgMS0uMTQ3LjM1OS41NTUuNTU1IDAgMCAxLS40MDIuMTQzLjU2LjU2IDAgMCAxLS40MDctLjE0My40ODcuNDg3IDAgMCAxLS4xNDMtLjM1OVptMi4wNDIgMy4zMzV2LS4wOTJjMC0uMzExLjA0NS0uNTk5LjEzNS0uODY0LjA5LS4yNjkuMjItLjUwMS4zOS0uNjk4LjE3My0uMTk5LjM4My0uMzUzLjYzLS40NjIuMjUtLjExMS41MzEtLjE2Ny44NDUtLjE2Ny4zMTYgMCAuNTk4LjA1Ni44NDUuMTY3LjI1LjEwOS40Ni4yNjMuNjMzLjQ2Mi4xNzMuMTk3LjMwNC40MjkuMzk1LjY5OC4wOS4yNjUuMTM1LjU1My4xMzUuODY0di4wOTJjMCAuMzExLS4wNDUuNTk5LS4xMzUuODY0LS4wOS4yNjYtLjIyMi40OTgtLjM5NS42OThhMS44NCAxLjg0IDAgMCAxLS42My40NjIgMi4wNjIgMi4wNjIgMCAwIDEtLjg0LjE2M2MtLjMxNiAwLS41OTktLjA1NC0uODQ5LS4xNjNhMS44NCAxLjg0IDAgMCAxLS42My0uNDYyIDIuMDc3IDIuMDc3IDAgMCAxLS4zOTQtLjY5OCAyLjY2MyAyLjY2MyAwIDAgMS0uMTM1LS44NjRabS45Ni0uMDkydi4wOTJjMCAuMTk0LjAyLjM3Ny4wNi41NWExLjQgMS40IDAgMCAwIC4xODcuNDU0Yy4wODUuMTMuMTk0LjIzMi4zMjcuMzA3YS45Ni45NiAwIDAgMCAuNDc0LjExMS45Mi45MiAwIDAgMCAuNDYyLS4xMTEuOTM0LjkzNCAwIDAgMCAuMzI3LS4zMDcgMS40IDEuNCAwIDAgMCAuMTg3LS40NTRjLjA0My0uMTczLjA2NC0uMzU2LjA2NC0uNTV2LS4wOTJhMi4yMyAyLjIzIDAgMCAwLS4wNjQtLjU0MiAxLjM5NiAxLjM5NiAwIDAgMC0uMTkxLS40NTguODk5Ljg5OSAwIDAgMC0uNzkzLS40MjYuOTIuOTIgMCAwIDAtLjQ3LjExNS45My45MyAwIDAgMC0uMzIzLjMxMSAxLjQ1MSAxLjQ1MSAwIDAgMC0uMTg3LjQ1OGMtLjA0LjE3LS4wNi4zNTEtLjA2LjU0MlptNC45NS0xLjE5MXYzLjM5aC0uOTZ2LTQuMzFoLjkwNWwuMDU2LjkyWm0tLjE3IDEuMDc2LS4zMTEtLjAwNGMuMDAyLS4zMDYuMDQ1LS41ODYuMTI3LS44NDEuMDg1LS4yNTUuMjAyLS40NzQuMzUtLjY1N2ExLjU1IDEuNTUgMCAwIDEgLjU0Mi0uNDIzYy4yMS0uMTAxLjQ0NC0uMTUxLjcwMi0uMTUxLjIwNyAwIC4zOTQuMDI5LjU2MS4wODguMTcuMDU1LjMxNS4xNDcuNDM1LjI3NC4xMjIuMTI4LjIxNS4yOTQuMjc5LjQ5OC4wNjMuMjAyLjA5NS40NTEuMDk1Ljc0NnYyLjc4NGgtLjk2NHYtMi43ODhjMC0uMjA4LS4wMy0uMzcxLS4wOTEtLjQ5MWEuNTEuNTEgMCAwIDAtLjI2LS4yNTguOTU4Ljk1OCAwIDAgMC0uNDE4LS4wOC45My45MyAwIDAgMC0uNzczLjM4NmMtLjA4Ny4xMi0uMTU1LjI1OC0uMjAzLjQxNWExLjcwOCAxLjcwOCAwIDAgMC0uMDcyLjUwMlptLTY3LjkyIDEzLjM5NGEyLjMxIDIuMzEgMCAwIDEtLjg2NC0uMTU1IDEuOTE1IDEuOTE1IDAgMCAxLS42NTMtLjQ0MyAxLjk1MyAxLjk1MyAwIDAgMS0uNDEtLjY2NSAyLjMzIDIuMzMgMCAwIDEtLjE0NC0uODI1di0uMTU5YzAtLjMzNy4wNS0uNjQzLjE0Ny0uOTE2YTIuMDggMi4wOCAwIDAgMSAuNDEtLjcwMmMuMTc2LS4xOTYuMzgzLS4zNDYuNjIyLS40NS4yNC0uMTAzLjQ5OC0uMTU1Ljc3Ny0uMTU1LjMwOCAwIC41NzguMDUyLjgwOS4xNTUuMjMxLjEwNC40MjIuMjUuNTc0LjQzOS4xNTQuMTg1LjI2OC40MDcuMzQyLjY2NS4wNzcuMjU4LjExNi41NDIuMTE2Ljg1MnYuNDExaC0zLjMzdi0uNjg5aDIuMzgydi0uMDc2YTEuMzQ1IDEuMzQ1IDAgMCAwLS4xMDQtLjQ4Ni44MjQuODI0IDAgMCAwLS4yODMtLjM2N2MtLjEyNy0uMDkzLS4yOTctLjEzOS0uNTEtLjEzOWEuODY3Ljg2NyAwIDAgMC0uNDI2LjEwMy44NTMuODUzIDAgMCAwLS4zMDcuMjkxIDEuNTQyIDEuNTQyIDAgMCAwLS4xOTEuNDYyIDIuNjAxIDIuNjAxIDAgMCAwLS4wNjQuNjAydi4xNTljMCAuMTg5LjAyNS4zNjQuMDc2LjUyNi4wNTMuMTYuMTMuMjk5LjIzLjQxOS4xMDIuMTE5LjIyNC4yMTMuMzY3LjI4My4xNDQuMDY2LjMwNy4wOTkuNDkuMDk5LjIzMiAwIC40MzctLjA0Ni42MTgtLjEzOS4xOC0uMDkzLjMzNy0uMjI1LjQ3LS4zOTVsLjUwNi40OWExLjgxOCAxLjgxOCAwIDAgMS0uOTA4LjY5Yy0uMjEzLjA3Ny0uNDYuMTE1LS43NDEuMTE1Wm0zLjM1NC00LjM5LjgyIDEuNDMuODM3LTEuNDNoMS4wNTZsLTEuMzA3IDIuMTE1IDEuMzU5IDIuMTk1aC0xLjA1NmwtLjg3Ny0xLjQ5LS44NzYgMS40OWgtMS4wNmwxLjM1NS0yLjE5NS0xLjMwMy0yLjExNWgxLjA1MlptNS4zMzcgNC4zOWEyLjMxIDIuMzEgMCAwIDEtLjg2NS0uMTU1IDEuOTE1IDEuOTE1IDAgMCAxLS42NTMtLjQ0MyAxLjk1MiAxLjk1MiAwIDAgMS0uNDEtLjY2NSAyLjMzMSAyLjMzMSAwIDAgMS0uMTQ0LS44MjV2LS4xNTljMC0uMzM3LjA0OS0uNjQzLjE0Ny0uOTE2LjA5OS0uMjc0LjIzNS0uNTA4LjQxLS43MDIuMTc2LS4xOTYuMzgzLS4zNDYuNjIyLS40NS4yNC0uMTAzLjQ5OC0uMTU1Ljc3Ny0uMTU1LjMwOCAwIC41NzguMDUyLjgwOS4xNTUuMjMuMTA0LjQyMi4yNS41NzQuNDM5LjE1NC4xODUuMjY4LjQwNy4zNDIuNjY1LjA3Ny4yNTguMTE2LjU0Mi4xMTYuODUydi40MTFoLTMuMzMxdi0uNjg5aDIuMzgzdi0uMDc2YTEuMzQzIDEuMzQzIDAgMCAwLS4xMDQtLjQ4Ni44MjMuODIzIDAgMCAwLS4yODMtLjM2N2MtLjEyNy0uMDkzLS4yOTctLjEzOS0uNTEtLjEzOWEuODY2Ljg2NiAwIDAgMC0uNDI2LjEwMy44NTMuODUzIDAgMCAwLS4zMDcuMjkxIDEuNTQyIDEuNTQyIDAgMCAwLS4xOTEuNDYyIDIuNjAxIDIuNjAxIDAgMCAwLS4wNjQuNjAydi4xNTljMCAuMTg5LjAyNS4zNjQuMDc2LjUyNi4wNTMuMTYuMTMuMjk5LjIzLjQxOS4xMDIuMTE5LjIyNC4yMTMuMzY3LjI4My4xNDQuMDY2LjMwNy4wOTkuNDkuMDk5LjIzMiAwIC40MzctLjA0Ni42MTgtLjEzOS4xOC0uMDkzLjMzNy0uMjI1LjQ3LS4zOTVsLjUwNi40OWExLjgxOSAxLjgxOSAwIDAgMS0uOTA4LjY5Yy0uMjEzLjA3Ny0uNDYuMTE1LS43NDEuMTE1Wm00LjM4LS43NjVjLjE1NyAwIC4yOTgtLjAzLjQyMy0uMDkxYS44MDcuODA3IDAgMCAwIC4zMDctLjI2My43Mi43MiAwIDAgMCAuMTMxLS4zODdoLjkwNGExLjM0NSAxLjM0NSAwIDAgMS0uMjQ3Ljc2MWMtLjE1OS4yMjgtLjM3LjQxLS42MzMuNTQ2YTEuOTA0IDEuOTA0IDAgMCAxLS44NzMuMTk5Yy0uMzI5IDAtLjYxNi0uMDU2LS44Ni0uMTY3YTEuNzAxIDEuNzAxIDAgMCAxLS42MS0uNDcgMi4wNzUgMi4wNzUgMCAwIDEtLjM2Ni0uNjljLS4wOC0uMjYtLjEyLS41MzktLjEyLS44MzZ2LS4xNGMwLS4yOTcuMDQtLjU3Ni4xMi0uODM2LjA4Mi0uMjYzLjIwNC0uNDk0LjM2Ni0uNjk0YTEuNjcgMS42NyAwIDAgMSAuNjEtLjQ2NmMuMjQ0LS4xMTQuNTMtLjE3MS44NTYtLjE3MS4zNDYgMCAuNjQ5LjA2OS45MDkuMjA3LjI2LjEzNi40NjUuMzI1LjYxMy41Ny4xNTIuMjQyLjIzLjUyMy4yMzUuODQ0aC0uOTA0YS45NjQuOTY0IDAgMCAwLS4xMi0uNDMuNzkuNzkgMCAwIDAtLjI5NC0uMzExLjg0Ljg0IDAgMCAwLS40NS0uMTE1LjkuOSAwIDAgMC0uNDgzLjExOS44MDcuODA3IDAgMCAwLS4yOTguMzE5IDEuNTU4IDEuNTU4IDAgMCAwLS4xNTYuNDVjLS4wMjkuMTY1LS4wNDQuMzM2LS4wNDQuNTE0di4xNGMwIC4xNzguMDE1LjM1LjA0NC41MTguMDMuMTY3LjA4LjMxNy4xNTIuNDUuMDc0LjEzLjE3NS4yMzUuMzAyLjMxNS4xMjguMDc3LjI5LjExNS40ODcuMTE1Wm01LjI0Mi0uMzMxdi0zLjI5NGguOTY0djQuMzFoLS45MDhsLS4wNTYtMS4wMTZabS4xMzUtLjg5Ni4zMjMtLjAwOGMwIC4yOS0uMDMyLjU1Ny0uMDk2LjgwMWExLjg0NSAxLjg0NSAwIDAgMS0uMjk0LjYzMyAxLjM4IDEuMzggMCAwIDEtLjUxLjQxOSAxLjczIDEuNzMgMCAwIDEtLjc0NS4xNDdjLS4yMSAwLS40MDMtLjAzLS41NzgtLjA5MmExLjE4NyAxLjE4NyAwIDAgMS0uNDU0LS4yODIgMS4yOTUgMS4yOTUgMCAwIDEtLjI5MS0uNDk4IDIuMzA1IDIuMzA1IDAgMCAxLS4xMDQtLjczNHYtMi43ODRoLjk2djIuNzkyYzAgLjE1Ny4wMi4yODkuMDU2LjM5NWEuNjc1LjY3NSAwIDAgMCAuMTUyLjI1MS41My41MyAwIDAgMCAuMjIzLjEzNS44OS44OSAwIDAgMCAuMjcuMDRjLjI3NCAwIC40OS0uMDUzLjY0Ni0uMTU5YS44ODIuODgyIDAgMCAwIC4zMzktLjQzOCAxLjc1IDEuNzUgMCAwIDAgLjEwMy0uNjE4Wm0zLjkzNS0yLjM5OHYuNzAxaC0yLjQzdi0uNzAxaDIuNDNabS0xLjczLTEuMDU2aC45NjF2NC4xNzVjMCAuMTMzLjAxOS4yMzUuMDU2LjMwNy4wNC4wNjkuMDk0LjExNS4xNjMuMTM5YS43NC43NCAwIDAgMCAuMjQzLjAzNiAxLjkwMSAxLjkwMSAwIDAgMCAuMzM5LS4wMzZsLjAwNC43MzNjLS4wOC4wMjQtLjE3My4wNDYtLjI4LjA2NGEyLjAwOCAyLjAwOCAwIDAgMS0uMzU4LjAyOGMtLjIyIDAtLjQxNS0uMDM4LS41ODUtLjExNWEuODY1Ljg2NSAwIDAgMS0uMzk5LS4zODdjLS4wOTUtLjE3OC0uMTQzLS40MTQtLjE0My0uNzA5di00LjIzNVptMy42NDUgMS4wNTZ2NC4zMWgtLjk2NXYtNC4zMWguOTY1Wm0tMS4wMjgtMS4xMzJhLjQ5LjQ5IDAgMCAxIC4xNDMtLjM2Mi41NDcuNTQ3IDAgMCAxIC40MDctLjE0OGMuMTcgMCAuMzA0LjA0OS40MDIuMTQ4YS40ODIuNDgyIDAgMCAxIC4xNDcuMzYyLjQ4LjQ4IDAgMCAxLS4xNDcuMzU5LjU1NS41NTUgMCAwIDEtLjQwMy4xNDMuNTYuNTYgMCAwIDEtLjQwNi0uMTQzLjQ4Ny40ODcgMCAwIDEtLjE0My0uMzU5Wm0yLjA0MiAzLjMzNXYtLjA5MmMwLS4zMTEuMDQ1LS41OTkuMTM1LS44NjQuMDktLjI2OS4yMi0uNTAxLjM5LS42OTguMTczLS4xOTkuMzgzLS4zNTMuNjMtLjQ2Mi4yNS0uMTExLjUzMi0uMTY3Ljg0NS0uMTY3LjMxNiAwIC41OTguMDU2Ljg0NS4xNjcuMjUuMTA5LjQ2LjI2My42MzMuNDYyLjE3My4xOTcuMzA0LjQyOS4zOTUuNjk4LjA5LjI2NS4xMzUuNTUzLjEzNS44NjR2LjA5MmMwIC4zMTEtLjA0NS41OTktLjEzNi44NjQtLjA5LjI2Ni0uMjIxLjQ5OC0uMzk0LjY5OGExLjg0IDEuODQgMCAwIDEtLjYzLjQ2MiAyLjA2MiAyLjA2MiAwIDAgMS0uODQuMTYzIDIuMSAyLjEgMCAwIDEtLjg0OS0uMTYzIDEuODM5IDEuODM5IDAgMCAxLS42My0uNDYyIDIuMDc1IDIuMDc1IDAgMCAxLS4zOTQtLjY5OCAyLjY2MyAyLjY2MyAwIDAgMS0uMTM1LS44NjRabS45Ni0uMDkydi4wOTJjMCAuMTk0LjAyLjM3Ny4wNi41NS4wNC4xNzIuMTAyLjMyNC4xODcuNDU0cy4xOTQuMjMyLjMyNy4zMDdhLjk2Ljk2IDAgMCAwIC40NzQuMTExLjkyLjkyIDAgMCAwIC40NjItLjExMS45MzQuOTM0IDAgMCAwIC4zMjctLjMwNyAxLjQgMS40IDAgMCAwIC4xODctLjQ1NGMuMDQyLS4xNzMuMDY0LS4zNTYuMDY0LS41NXYtLjA5MmEyLjIzIDIuMjMgMCAwIDAtLjA2NC0uNTQyIDEuMzk5IDEuMzk5IDAgMCAwLS4xOTEtLjQ1OC45LjkgMCAwIDAtLjc5My0uNDI2LjkyLjkyIDAgMCAwLS40Ny4xMTUuOTMuOTMgMCAwIDAtLjMyMy4zMTEgMS40NTQgMS40NTQgMCAwIDAtLjE4Ny40NThjLS4wNC4xNy0uMDYuMzUxLS4wNi41NDJabTQuOTUtMS4xOTF2My4zOWgtLjk2di00LjMxaC45MDVsLjA1Ni45MlptLS4xNyAxLjA3Ni0uMzEyLS4wMDRjLjAwMy0uMzA2LjA0Ni0uNTg2LjEyOC0uODQxLjA4NS0uMjU1LjIwMi0uNDc0LjM1LS42NTdhMS41NSAxLjU1IDAgMCAxIC41NDMtLjQyM2MuMjEtLjEwMS40NDMtLjE1MS43LS4xNTEuMjA4IDAgLjM5NS4wMjkuNTYzLjA4OC4xNy4wNTUuMzE0LjE0Ny40MzQuMjc0LjEyMi4xMjguMjE1LjI5NC4yNzkuNDk4LjA2My4yMDIuMDk1LjQ1MS4wOTUuNzQ2djIuNzg0aC0uOTY0di0yLjc4OGMwLS4yMDgtLjAzLS4zNzEtLjA5Mi0uNDkxYS41MS41MSAwIDAgMC0uMjU4LS4yNTguOTU4Ljk1OCAwIDAgMC0uNDE5LS4wOC45My45MyAwIDAgMC0uNzczLjM4NmMtLjA4Ny4xMi0uMTU1LjI1OC0uMjAzLjQxNWExLjcwOCAxLjcwOCAwIDAgMC0uMDcyLjUwMlptNi4zMjQgMS4xNDdhLjQ4LjQ4IDAgMCAwLS4wNzEtLjI1OWMtLjA0OC0uMDgtLjE0LS4xNTEtLjI3NS0uMjE1YTIuNjQ2IDIuNjQ2IDAgMCAwLS41OS0uMTc1IDUuMTk0IDUuMTk0IDAgMCAxLS42My0uMTggMS45NyAxLjk3IDAgMCAxLS40ODUtLjI1OSAxLjA5IDEuMDkgMCAwIDEtLjMxNS0uMzU4Ljk5NS45OTUgMCAwIDEtLjExMi0uNDc4YzAtLjE3Ni4wMzktLjM0Mi4xMTYtLjQ5OC4wNzctLjE1Ny4xODctLjI5NS4zMy0uNDE1LjE0NC0uMTE5LjMxOC0uMjEzLjUyMi0uMjgzLjIwOC0uMDY5LjQzOS0uMTAzLjY5NC0uMTAzLjM2IDAgLjY3LjA2MS45MjguMTgzLjI2LjEyLjQ2LjI4My41OTguNDkuMTM4LjIwNS4yMDcuNDM2LjIwNy42OTNoLS45NmEuNjEuNjEgMCAwIDAtLjA4OC0uMzE4LjYwOS42MDkgMCAwIDAtLjI1NS0uMjQzLjg3MS44NzEgMCAwIDAtLjQzLS4wOTYuOTM1LjkzNSAwIDAgMC0uNDEuMDguNTYuNTYgMCAwIDAtLjI0LjE5OS41MS41MSAwIDAgMC0uMDM2LjQ2Ni40NS40NSAwIDAgMCAuMTQ0LjE1NWMuMDY2LjA0Ni4xNTcuMDg4LjI3LjEyOC4xMTguMDQuMjY0LjA3OC40MzkuMTE2LjMzLjA2OS42MTIuMTU4Ljg0OS4yNjYuMjM5LjEwNy40MjIuMjQ1LjU1LjQxNS4xMjcuMTY3LjE5LjM4LjE5LjYzNyAwIC4xOTItLjA0LjM2Ny0uMTIzLjUyNi0uMDguMTU3LS4xOTYuMjk0LS4zNS40MTFhMS43NDcgMS43NDcgMCAwIDEtLjU1NC4yNjYgMi40ODQgMi40ODQgMCAwIDEtLjcxNy4wOTZjLS4zOSAwLS43MjEtLjA2OS0uOTkyLS4yMDdhMS41ODIgMS41ODIgMCAwIDEtLjYxOC0uNTM4IDEuMjczIDEuMjczIDAgMCAxLS4yMDctLjY4NWguOTI4Yy4wMS4xNzguMDYuMzIuMTQ4LjQyNmEuNzkuNzkgMCAwIDAgLjMzNC4yMjdjLjEzNi4wNDUuMjc1LjA2OC40MTkuMDY4LjE3MiAwIC4zMTctLjAyMy40MzQtLjA2OGEuNjIzLjYyMyAwIDAgMCAuMjY3LS4xOTEuNDU1LjQ1NSAwIDAgMCAuMDkxLS4yNzlaIi8+PC9nPjxnIGNsaXAtcGF0aD0idXJsKCNvKSI+PHBhdGggZmlsbD0iIzAwMCIgZmlsbC1vcGFjaXR5PSIuNTQiIGQ9Ik0xMDguMTk3IDEyNi40Nzl2Ljc1OGMwIC40MDgtLjAzNi43NTItLjEwOSAxLjAzMi0uMDczLjI4LS4xNzguNTA1LS4zMTQuNjc2YTEuMiAxLjIgMCAwIDEtLjQ5Ni4zNzIgMS43NjEgMS43NjEgMCAwIDEtLjY0OC4xMTNjLS4xOTIgMC0uMzY4LS4wMjQtLjUzLS4wNzJhMS4yNjMgMS4yNjMgMCAwIDEtLjQzNy0uMjI5IDEuMzkzIDEuMzkzIDAgMCAxLS4zMjgtLjQxNyAyLjIyNyAyLjIyNyAwIDAgMS0uMjA4LS42MjEgNC40NjQgNC40NjQgMCAwIDEtLjA3Mi0uODU0di0uNzU4YzAtLjQwNy4wMzctLjc0OS4xMS0xLjAyNC4wNzUtLjI3Ni4xODEtLjQ5Ny4zMTctLjY2My4xMzctLjE2OC4zMDEtLjI4OS40OTItLjM2Mi4xOTMtLjA3My40MS0uMTA5LjY0OS0uMTA5LjE5MyAwIC4zNzEuMDI0LjUzMy4wNzJhMS4xOTggMS4xOTggMCAwIDEgLjc2MS42MjRjLjA5MS4xNjcuMTYxLjM3LjIwOC42MTIuMDQ4LjI0MS4wNzIuNTI1LjA3Mi44NVptLS42MzUuODYxdi0uOTY3YTMuNzQgMy43NCAwIDAgMC0uMDQxLS41ODcgMS44MzQgMS44MzQgMCAwIDAtLjExMy0uNDM3Ljg2Ni44NjYgMCAwIDAtLjE5MS0uMjk0LjY3Ni42NzYgMCAwIDAtLjI2My0uMTY0Ljk1My45NTMgMCAwIDAtLjMzMS0uMDU1Ljg5OC44OTggMCAwIDAtLjQuMDg2LjcxMy43MTMgMCAwIDAtLjI5My4yNjNjLS4wNzguMTItLjEzNy4yNzktLjE3OC40NzRhMy41NSAzLjU1IDAgMCAwLS4wNjEuNzE0di45NjdjMCAuMjIzLjAxMi40Mi4wMzcuNTlhMS45IDEuOSAwIDAgMCAuMTIuNDQ0Yy4wNTIuMTIzLjExNi4yMjUuMTkxLjMwNC4wNzUuMDguMTYxLjEzOS4yNTkuMTc4YS45OC45OCAwIDAgMCAuMzMyLjA1NWMuMTU0IDAgLjI5LS4wMy40MDYtLjA4OWEuNzMxLjczMSAwIDAgMCAuMjktLjI3N2MuMDgtLjEyNy4xMzktLjI5LjE3OC0uNDg4YTMuODIgMy44MiAwIDAgMCAuMDU4LS43MTdabTUuOTE2LTIuOTUxLTIuMDcyIDUuMzk5aC0uNTQzbDIuMDc2LTUuMzk5aC41MzlabTQuODk5LS4wMjd2NC45OTloLS42MzF2LTQuMjFsLTEuMjc0LjQ2NHYtLjU3bDEuODA2LS42ODNoLjA5OVptNS4yMTMgMi4xMTd2Ljc1OGMwIC40MDgtLjAzNy43NTItLjExIDEuMDMyLS4wNzMuMjgtLjE3Ny41MDUtLjMxNC42NzYtLjEzNy4xNy0uMzAyLjI5NS0uNDk1LjM3MmExLjc2NyAxLjc2NyAwIDAgMS0uNjQ5LjExM2MtLjE5MSAwLS4zNjgtLjAyNC0uNTI5LS4wNzJhMS4yNDUgMS4yNDUgMCAwIDEtLjQzNy0uMjI5IDEuMzc4IDEuMzc4IDAgMCAxLS4zMjgtLjQxNyAyLjIyNyAyLjIyNyAwIDAgMS0uMjA5LS42MjEgNC41NTMgNC41NTMgMCAwIDEtLjA3MS0uODU0di0uNzU4YzAtLjQwNy4wMzYtLjc0OS4xMDktMS4wMjQuMDc1LS4yNzYuMTgxLS40OTcuMzE4LS42NjMuMTM2LS4xNjguMy0uMjg5LjQ5MS0uMzYyYTEuODMgMS44MyAwIDAgMSAuNjQ5LS4xMDljLjE5NCAwIC4zNzEuMDI0LjUzMy4wNzIuMTY0LjA0NS4zMS4xMTkuNDM3LjIyMi4xMjguMS4yMzYuMjM0LjMyNC40MDIuMDkyLjE2Ny4xNjEuMzcuMjA5LjYxMi4wNDguMjQxLjA3Mi41MjUuMDcyLjg1Wm0tLjYzNi44NjF2LS45NjdjMC0uMjIzLS4wMTMtLjQxOS0uMDQxLS41ODdhMS44MzcgMS44MzcgMCAwIDAtLjExMi0uNDM3Ljg1My44NTMgMCAwIDAtLjE5Mi0uMjk0LjY2OS42NjkgMCAwIDAtLjI2My0uMTY0Ljk1Ljk1IDAgMCAwLS4zMzEtLjA1NS44OTQuODk0IDAgMCAwLS4zOTkuMDg2LjcyMi43MjIgMCAwIDAtLjI5NC4yNjNjLS4wNzcuMTItLjEzNy4yNzktLjE3OC40NzRhMy41NSAzLjU1IDAgMCAwLS4wNjEuNzE0di45NjdjMCAuMjIzLjAxMi40Mi4wMzcuNTkuMDI4LjE3MS4wNjguMzE5LjEyLjQ0NC4wNTIuMTIzLjExNi4yMjUuMTkxLjMwNC4wNzUuMDguMTYyLjEzOS4yNi4xNzguMS4wMzYuMjEuMDU1LjMzMS4wNTUuMTU1IDAgLjI5LS4wMy40MDYtLjA4OWEuNzM0LjczNCAwIDAgMCAuMjkxLS4yNzdjLjA3OS0uMTI3LjEzOS0uMjkuMTc3LS40ODhhMy44MiAzLjgyIDAgMCAwIC4wNTgtLjcxN1ptMi4wNTQtMi45NTFoLjYzOGwxLjYyOSA0LjA1NCAxLjYyNi00LjA1NGguNjQybC0yLjAyMiA0Ljk3MmgtLjQ5OWwtMi4wMTQtNC45NzJabS0uMjA5IDBoLjU2NGwuMDkyIDMuMDMzdjEuOTM5aC0uNjU2di00Ljk3MlptNC4zODUgMGguNTY0djQuOTcyaC0uNjU2di0xLjkzOWwuMDkyLTMuMDMzWiIvPjxnIGNsaXAtcGF0aD0idXJsKCNwKSI+PHJlY3Qgd2lkdGg9Ijg2LjAxMiIgaGVpZ2h0PSI0LjY2MyIgeD0iMTA0LjY2MyIgeT0iMTM0LjY5MiIgZmlsbD0iIzE5ODAzOCIgZmlsbC1vcGFjaXR5PSIuMDYiIHJ4PSIyLjMzMSIvPjwvZz48cGF0aCBmaWxsPSIjMTk4MDM4IiBkPSJNMTA4LjM5OSAxNDguMTV2LjUzN2gtMi42MzN2LS41MzdoMi42MzNabS0yLjUtNC40MzZ2NC45NzNoLS42NTl2LTQuOTczaC42NTlabTIuMTUxIDIuMTM4di41MzZoLTIuMjg0di0uNTM2aDIuMjg0Wm0uMzE0LTIuMTM4di41NGgtMi41OTh2LS41NGgyLjU5OFptMS42MiAyLjA2NnYyLjkwN2gtLjYzMnYtMy42OTVoLjU5OGwuMDM0Ljc4OFptLS4xNS45MTktLjI2My0uMDFjLjAwMi0uMjUzLjA0LS40ODYuMTEzLS43LjA3Mi0uMjE3LjE3NS0uNDA0LjMwNy0uNTY0YTEuMzggMS4zOCAwIDAgMSAxLjA4Mi0uNTAyYy4xODMgMCAuMzQ2LjAyNS40OTIuMDc1YS44OS44OSAwIDAgMSAuMzcyLjIzM2MuMTA1LjEwNy4xODUuMjQ1LjIzOS40MTYuMDU1LjE2OS4wODIuMzc1LjA4Mi42MTh2Mi40MjJoLS42MzV2LTIuNDI5YzAtLjE5My0uMDI4LS4zNDgtLjA4NS0uNDY0YS41MjYuNTI2IDAgMCAwLS4yNDktLjI1Ni44OTQuODk0IDAgMCAwLS40MDMtLjA4Mi45NC45NCAwIDAgMC0uNzYyLjM3MiAxLjM1OSAxLjM1OSAwIDAgMC0uMjE1LjRjLS4wNS4xNDgtLjA3NS4zMDUtLjA3NS40NzFabTUuNzk2IDEuMzU2di0xLjkwMmEuNzcyLjc3MiAwIDAgMC0uMDg5LS4zNzkuNTc3LjU3NyAwIDAgMC0uMjU5LS4yNTMuOTQ4Ljk0OCAwIDAgMC0uNDMxLS4wODljLS4xNTkgMC0uMjk5LjAyNy0uNDIuMDgyYS43NC43NCAwIDAgMC0uMjguMjE1LjQ3My40NzMgMCAwIDAtLjA5OS4yODdoLS42MzJjMC0uMTMyLjAzNS0uMjYzLjEwMy0uMzkzLjA2OC0uMTI5LjE2Ni0uMjQ3LjI5NC0uMzUxLjEyOS0uMTA3LjI4NC0uMTkyLjQ2NC0uMjUzLjE4Mi0uMDY0LjM4NS0uMDk2LjYwOC0uMDk2LjI2OCAwIC41MDUuMDQ2LjcxLjEzNy4yMDcuMDkxLjM2OS4yMjkuNDg1LjQxMy4xMTguMTgyLjE3OC40MTEuMTc4LjY4NnYxLjcyMmMwIC4xMjMuMDEuMjUzLjAzLjM5Mi4wMjMuMTM5LjA1Ni4yNTkuMDk5LjM1OXYuMDU1aC0uNjU5YTEuMTcgMS4xNyAwIDAgMS0uMDc1LS4yOTEgMi4zNyAyLjM3IDAgMCAxLS4wMjctLjM0MVptLjEwOS0xLjYwOS4wMDcuNDQ0aC0uNjM5Yy0uMTc5IDAtLjM0LjAxNS0uNDgxLjA0NS0uMTQxLjAyNy0uMjYuMDY5LS4zNTUuMTI2YS41Ny41NyAwIDAgMC0uMjk0LjUxMmMwIC4xMTYuMDI2LjIyMi4wNzkuMzE4YS41NzcuNTc3IDAgMCAwIC4yMzUuMjI5Ljg2NS44NjUgMCAwIDAgLjM5My4wODIgMS4wNjcgMS4wNjcgMCAwIDAgLjg2NC0uNDI0LjYzNi42MzYgMCAwIDAgLjE0My0uMzQ1bC4yNy4zMDRhLjkxLjkxIDAgMCAxLS4xMy4zMTggMS41MSAxLjUxIDAgMCAxLS43LjU5NyAxLjM0MiAxLjM0MiAwIDAgMS0uNTM5LjEwMyAxLjQxIDEuNDEgMCAwIDEtLjY1OS0uMTQ3IDEuMDMyIDEuMDMyIDAgMCAxLS41OTEtLjk0OWMwLS4xOTguMDM5LS4zNzMuMTE2LS41MjMuMDc3LS4xNTIuMTg5LS4yNzkuMzM1LS4zNzkuMTQ1LS4xMDIuMzIxLS4xOC41MjYtLjIzMi4yMDQtLjA1My40MzMtLjA3OS42ODYtLjA3OWguNzM0Wm0xLjc0Ni0zLjAwNWguNjM1djQuNTI4bC0uMDU0LjcxOGgtLjU4MXYtNS4yNDZabTMuMTMyIDMuMzY3di4wNzJjMCAuMjY5LS4wMzIuNTE4LS4wOTYuNzQ4YTEuODM0IDEuODM0IDAgMCAxLS4yOC41OTQgMS4zMDQgMS4zMDQgMCAwIDEtLjQ1MS4zOTNjLS4xNzcuMDkzLS4zODEuMTQtLjYxMS4xNC0uMjM1IDAtLjQ0MS0uMDQtLjYxOC0uMTJhMS4yMjQgMS4yMjQgMCAwIDEtLjQ0NC0uMzUxIDEuNzk0IDEuNzk0IDAgMCAxLS4yOS0uNTU0IDMuNDQgMy40NCAwIDAgMS0uMTQ3LS43M3YtLjMxNWEzLjQ0IDMuNDQgMCAwIDEgLjE0Ny0uNzM0Yy4wNzItLjIxNi4xNjktLjQwMS4yOS0uNTUzLjEyMS0uMTU1LjI2OS0uMjcyLjQ0NC0uMzUyLjE3NS0uMDgyLjM3OS0uMTIzLjYxMS0uMTIzLjIzMiAwIC40MzguMDQ2LjYxOC4xMzcuMTguMDg5LjMzLjIxNi40NTEuMzgyLjEyMy4xNjYuMjE2LjM2Ni4yOC41OTguMDY0LjIzLjA5Ni40ODYuMDk2Ljc2OFptLS42MzYuMDcydi0uMDcyYzAtLjE4NC0uMDE3LS4zNTctLjA1MS0uNTE5YTEuMzQyIDEuMzQyIDAgMCAwLS4xNjQtLjQzLjgyLjgyIDAgMCAwLS4yOTctLjI5NC44NzUuODc1IDAgMCAwLS40NTQtLjEwOS45ODUuOTg1IDAgMCAwLS40MTcuMDgyLjkxMi45MTIgMCAwIDAtLjI5Ny4yMjIgMS4xNyAxLjE3IDAgMCAwLS4yMDEuMzE0IDEuODEgMS44MSAwIDAgMC0uMTEzLjM2MnYuODIzYy4wMzcuMTU5LjA5Ni4zMTMuMTc4LjQ2MS4wODQuMTQ2LjE5Ni4yNjUuMzM0LjM1OS4xNDIuMDkzLjMxNi4xNC41MjMuMTRhLjg2Ny44NjcgMCAwIDAgLjQzNy0uMTAzLjgxNy44MTcgMCAwIDAgLjI5Ny0uMjkgMS4zNSAxLjM1IDAgMCAwIC4xNzEtLjQyN2MuMDM2LS4xNjIuMDU0LS4zMzUuMDU0LS41MTlabTIuMzU0LTMuNDM5djUuMjQ2aC0uNjM1di01LjI0NmguNjM1Wm0yLjc4MSA1LjMxNGMtLjI1NyAwLS40OTEtLjA0My0uNy0uMTNhMS41OTMgMS41OTMgMCAwIDEtLjUzNi0uMzcyIDEuNjczIDEuNjczIDAgMCAxLS4zNDItLjU2NyAyLjA5OSAyLjA5OSAwIDAgMS0uMTE5LS43MTd2LS4xNDRjMC0uMy4wNDQtLjU2OC4xMzMtLjgwMmExLjc5IDEuNzkgMCAwIDEgLjM2Mi0uNjAxIDEuNTM1IDEuNTM1IDAgMCAxIDEuMTItLjQ5OWMuMjY0IDAgLjQ5Mi4wNDYuNjgzLjEzNy4xOTQuMDkxLjM1Mi4yMTguNDc1LjM4Mi4xMjMuMTYyLjIxNC4zNTMuMjczLjU3NC4wNTkuMjE5LjA4OS40NTguMDg5LjcxN3YuMjg0aC0yLjc2di0uNTE2aDIuMTI4di0uMDQ4YTEuNTUgMS41NSAwIDAgMC0uMTAzLS40NzguODQzLjg0MyAwIDAgMC0uMjczLS4zODJjLS4xMjUtLjEwMS0uMjk2LS4xNTEtLjUxMi0uMTUxYS44NTYuODU2IDAgMCAwLS43MDcuMzU5IDEuMzMgMS4zMyAwIDAgMC0uMjAxLjQzNCAyLjE4IDIuMTggMCAwIDAtLjA3Mi41OXYuMTQ0YzAgLjE3NS4wMjQuMzQuMDcyLjQ5NS4wNS4xNTIuMTIxLjI4Ny4yMTUuNDAzLjA5NS4xMTYuMjEuMjA3LjM0NS4yNzMuMTM2LjA2Ni4yOTEuMDk5LjQ2NC4wOTkuMjIzIDAgLjQxMi0uMDQ1LjU2Ny0uMTM2LjE1NS0uMDkyLjI5LS4yMTMuNDA2LS4zNjZsLjM4My4zMDRhMS43NiAxLjc2IDAgMCAxLS4zMDQuMzQ1IDEuNDQ3IDEuNDQ3IDAgMCAxLS40NTQuMjY2IDEuNzQ0IDEuNzQ0IDAgMCAxLS42MzIuMTAzWm00LjczNy0uNzg2di00LjUyOGguNjM2djUuMjQ2aC0uNTgxbC0uMDU1LS43MThabS0yLjQ4Ni0xLjA4OXYtLjA3MmMwLS4yODIuMDM1LS41MzguMTAzLS43NjhhMS44NCAxLjg0IDAgMCAxIC4yOTctLjU5OCAxLjMxMiAxLjMxMiAwIDAgMSAxLjA2Mi0uNTE5Yy4yMzIgMCAuNDM1LjA0MS42MDguMTIzLjE3NS4wOC4zMjMuMTk3LjQ0NC4zNTIuMTIzLjE1Mi4yMi4zMzcuMjkuNTUzLjA3MS4yMTYuMTIuNDYxLjE0Ny43MzRWMTQ3Yy0uMDI1LjI3LS4wNzQuNTE0LS4xNDcuNzMtLjA3LjIxNy0uMTY3LjQwMS0uMjkuNTU0YTEuMjI0IDEuMjI0IDAgMCAxLS40NDQuMzUxYy0uMTc1LjA4LS4zOC4xMi0uNjE1LjEyLS4yMTYgMC0uNDE0LS4wNDctLjU5NC0uMTRhMS40MDMgMS40MDMgMCAwIDEtLjQ2MS0uMzkzIDEuODggMS44OCAwIDAgMS0uMjk3LS41OTQgMi42MjYgMi42MjYgMCAwIDEtLjEwMy0uNzQ4Wm0uNjM2LS4wNzJ2LjA3MmMwIC4xODQuMDE4LjM1Ny4wNTQuNTE5LjAzOS4xNjIuMDk4LjMwNC4xNzguNDI3LjA3OS4xMjMuMTgxLjIyLjMwNC4yOS4xMjMuMDY5LjI3LjEwMy40NC4xMDMuMjEgMCAuMzgyLS4wNDUuNTE2LS4xMzRhLjk4Ny45ODcgMCAwIDAgLjMyOC0uMzUxYy4wODItLjE0Ni4xNDUtLjMwNC4xOTEtLjQ3NXYtLjgyM2ExLjc2NyAxLjc2NyAwIDAgMC0uMTItLjM2MiAxLjExNSAxLjExNSAwIDAgMC0uMTk4LS4zMTQuODUzLjg1MyAwIDAgMC0uMjk3LS4yMjIuOTU3Ljk1NyAwIDAgMC0uNDEzLS4wODIuODcxLjg3MSAwIDAgMC0uNDQ3LjEwOS44NzIuODcyIDAgMCAwLS4zMDQuMjk0Yy0uMDguMTIzLS4xMzkuMjY2LS4xNzguNDNhMi4zODkgMi4zODkgMCAwIDAtLjA1NC41MTlaIi8+PGcgY2xpcC1wYXRoPSJ1cmwoI3EpIj48cGF0aCBmaWxsPSIjMTk4MDM4IiBkPSJNMTg2LjAxMiAxNDIuODAyYTMuODg2IDMuODg2IDAgMSAwIC4wMDIgNy43NzIgMy44ODYgMy44ODYgMCAwIDAtLjAwMi03Ljc3MlptLS43NzcgNS44MjgtMS45NDMtMS45NDMuNTQ4LS41NDggMS4zOTUgMS4zOTEgMi45NDktMi45NDkuNTQ4LjU1Mi0zLjQ5NyAzLjQ5N1oiLz48L2c+PC9nPjwvZz48cmVjdCB3aWR0aD0iMTk5LjQxNyIgaGVpZ2h0PSIxNTkuNDE3IiB4PSIuMjkxIiB5PSIuMjkxIiBzdHJva2U9IiMwMDAiIHN0cm9rZS1vcGFjaXR5PSIuMTIiIHN0cm9rZS13aWR0aD0iLjU4MyIgcng9IjMuNjI2Ii8+PC9nPjxkZWZzPjxjbGlwUGF0aCBpZD0iYSI+PHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIxNjAiIGZpbGw9IiNmZmYiIHJ4PSI0Ii8+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImMiPjxyZWN0IHdpZHRoPSIyMDAiIGhlaWdodD0iMTYwIiBmaWxsPSIjZmZmIiByeD0iMy45MTgiLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iZCI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTkuMzI1IDBIMTAwdjM4LjY1SDkuMzI1eiIvPjwvY2xpcFBhdGg+PGNsaXBQYXRoIGlkPSJlIj48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTAwIDBoOTAuNjc1djM4LjY1SDEwMHoiLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iZiI+PHJlY3Qgd2lkdGg9Ijg2LjAxMiIgaGVpZ2h0PSI0LjY2MyIgeD0iMTA0LjY2MyIgeT0iMTYuOTkzIiBmaWxsPSIjZmZmIiByeD0iMi4zMzEiLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iZyI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTkuMzI1IDM5LjIzM0gxMDB2MzguNjVIOS4zMjV6Ii8+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImgiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xMDAgMzkuMjMzaDkwLjY3NXYzOC42NUgxMDB6Ii8+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImkiPjxyZWN0IHdpZHRoPSI4Ni4wMTIiIGhlaWdodD0iNC42NjMiIHg9IjEwNC42NjMiIHk9IjU2LjIyNyIgZmlsbD0iI2ZmZiIgcng9IjIuMzMxIi8+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImoiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xODEuMzUgNjMuNTU5aDkuMzI1djkuMzI1aC05LjMyNXoiLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iayI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTkuMzI1IDc5Ljk2M0gxMDB2MzUuNjU2SDkuMzI1eiIvPjwvY2xpcFBhdGg+PGNsaXBQYXRoIGlkPSJsIj48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTAwIDc4LjQ2Nmg5MC42NzV2MzguNjVIMTAweiIvPjwvY2xpcFBhdGg+PGNsaXBQYXRoIGlkPSJtIj48cmVjdCB3aWR0aD0iODYuMDEyIiBoZWlnaHQ9IjQuNjYzIiB4PSIxMDQuNjYzIiB5PSI5NS40NTkiIGZpbGw9IiNmZmYiIHJ4PSIyLjMzMSIvPjwvY2xpcFBhdGg+PGNsaXBQYXRoIGlkPSJuIj48cGF0aCBmaWxsPSIjZmZmIiBkPSJNOS4zMjUgMTE5LjE5NkgxMDB2MzUuNjU2SDkuMzI1eiIvPjwvY2xpcFBhdGg+PGNsaXBQYXRoIGlkPSJvIj48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTAwIDExNy42OTloOTAuNjc1djM4LjY1SDEwMHoiLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0icCI+PHJlY3Qgd2lkdGg9Ijg2LjAxMiIgaGVpZ2h0PSI0LjY2MyIgeD0iMTA0LjY2MyIgeT0iMTM0LjY5MiIgZmlsbD0iI2ZmZiIgcng9IjIuMzMxIi8+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9InEiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0xODEuMzUgMTQyLjAyNGg5LjMyNXY5LjMyNWgtOS4zMjV6Ii8+PC9jbGlwUGF0aD48ZmlsdGVyIGlkPSJiIiB3aWR0aD0iMjA5LjMyNSIgaGVpZ2h0PSIxNjkuMzI1IiB4PSItNC42NjMiIHk9Ii0yLjMzMSIgY29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzPSJzUkdCIiBmaWx0ZXJVbml0cz0idXNlclNwYWNlT25Vc2UiPjxmZUZsb29kIGZsb29kLW9wYWNpdHk9IjAiIHJlc3VsdD0iQmFja2dyb3VuZEltYWdlRml4Ii8+PGZlQ29sb3JNYXRyaXggaW49IlNvdXJjZUFscGhhIiByZXN1bHQ9ImhhcmRBbHBoYSIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAxMjcgMCIvPjxmZU9mZnNldCBkeT0iMi4zMzEiLz48ZmVHYXVzc2lhbkJsdXIgc3RkRGV2aWF0aW9uPSIyLjMzMSIvPjxmZUNvbXBvc2l0ZSBpbjI9ImhhcmRBbHBoYSIgb3BlcmF0b3I9Im91dCIvPjxmZUNvbG9yTWF0cml4IHZhbHVlcz0iMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMC4wOCAwIi8+PGZlQmxlbmQgaW4yPSJCYWNrZ3JvdW5kSW1hZ2VGaXgiIHJlc3VsdD0iZWZmZWN0MV9kcm9wU2hhZG93Xzg0NDVfMTg5ODg0Ii8+PGZlQ29sb3JNYXRyaXggaW49IlNvdXJjZUFscGhhIiByZXN1bHQ9ImhhcmRBbHBoYSIgdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAxMjcgMCIvPjxmZU9mZnNldCBkeT0iLjU4MyIvPjxmZUdhdXNzaWFuQmx1ciBzdGREZXZpYXRpb249IjEuMTY2Ii8+PGZlQ29tcG9zaXRlIGluMj0iaGFyZEFscGhhIiBvcGVyYXRvcj0ib3V0Ii8+PGZlQ29sb3JNYXRyaXggdmFsdWVzPSIwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwLjA4IDAiLz48ZmVCbGVuZCBpbjI9ImVmZmVjdDFfZHJvcFNoYWRvd184NDQ1XzE4OTg4NCIgcmVzdWx0PSJlZmZlY3QyX2Ryb3BTaGFkb3dfODQ0NV8xODk4ODQiLz48ZmVCbGVuZCBpbj0iU291cmNlR3JhcGhpYyIgaW4yPSJlZmZlY3QyX2Ryb3BTaGFkb3dfODQ0NV8xODk4ODQiIHJlc3VsdD0ic2hhcGUiLz48L2ZpbHRlcj48L2RlZnM+PC9zdmc+", + "fileName": "api-usage-widget.png", + "publicResourceKey": "1TAYhxLWInZC20Xrn8PgbNbQs6fX8Pir", + "mediaType": "image/png", + "data": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAChVBMVEUAAADs7Ozd3d3f4+Xd4eLN0tLP0dTg4+Xp6en////y9ffx9/Pt7e0ZgDjl7uvh5Oawvs/7+/vx8fHO1+H9/f31+ffu7u78/f3s8fH9/v6Eu5Xg7uX6/PxHaY7i5efI0t1VdJf5+vvx9PaNornv8vVOb5NffJ262cRphaNaeJrs7/Ouvc2Wqb+BmLKmpqZjgJ9ip3ft8PXBzdmsu8yEm7Rviaf3+Pra4empqann7PHQ2eK/y9i3xNPD3stXdpi8vLyGnbVsh6VRcZV+t5A7X4ff5evW3ebU3eXN1+GzwdHOzs6pucuarcGQpbt7k66OwZ46klXc7OKcrsKx1Lytrq53kaxyjKhDZYs+Yoni5+3L4tOitMeKn7dmgqH09vn09PS6x9V1jqrm6/DT2+TEz9vX2NnV1dbHyMmltsisrKxhfp5cepuJvplLbZHj6e7d3d2hssWer8PBwcGoz7R0sojp7fLj8Oe/3MiYq8C4ubm1tbWvsLCeyauCuZQ3XIRepnTl6u/p8+zc4+rk5OTK1N/O5Na4xdSxv9CTp71+lrCSw6F4tItnq30cgTr29vbt8vTq6urh4eHU59rR5tefn6CMv5zd5OvX3+fIy8yt0riHvZj19/nm5+ja2tvR09S+v8B7to1ur4FkqXlPnmdHmWDz9fjZ4Oi8ydagscOVxaRxsYQqiUchhD/3+/jq8O/o6uzZ6t7Ly8uw07yysrKjzLCho6SVlpZao3Le4eOx1LuozbeZx6dWoW7t9e/c3uDC3czKysrDw8OytbaZm5xrrn8wjUze7eLO0NDI4c8zWYLg5uzV5t7FxcWOjo7v9vHY5+C01r7AydLKzc6nzLWBgoNCllzu8fMzg2RWAAAACXRSTlMA3Vrh2mBe3tXDgts5AAAPHUlEQVR42u3ch1sTZxzAcbr7eyEWaGw8uDR7r0JCAgkz7BFk7733kr2UvYTK0jpQqnW3bm3VWrXaveff0/cYLbXYQgiS0Hyf5y4hPjzw8T1y8X5EBweHl154zcZ74WUH3ItcZPPxX8TrsQ0cWPKywwtoW/SKw5toW/SaHWLG17qE0OVj6LWPEHrzE4SjdsWlP6PLpcdQyWV8/7JNQAK++wiVPnoHBdxC6GYHfoCbhPC94yVoZm6G++hTxP/1uE1A0Kd4Kd7BkEcfXZ6hIMe/wLtb5SV8JSq/fPMm951HNgZ5J6ntZwrSwacOr12PdilR0uWZ0i9unrAxSIny5msYculT/Bj/BJrhz6Cb/Jnfb31qK5B3jiFUXIy++Kj4i8ulCJ14DeG+TSpF7+CtDZ04dvwT24A80S682ebT75PZIf8XSHgrzgf9Wz7W+0p5BaR3yD9rSIreRsgD4fDt4h3+0gd4l1pA3XL5Cx/zKZXH0oYfw4/89Xmr1HmRtnof7jC7XasfWkG+SMZkR7CFPJSWwnTeJxF7IWZQtTM3v9JfF10tqe9ORuhgWlBqsiQIydjGojNslQKliCXpkf5sGUrx98+SqiqDuMMV7Aj0ZDvGaZaG4LhPhaSgM2d9U1Gajyw5wsjyfZsZGuHPqkB1muh8tLAiB5kosJXbzU11jgnKq4yI5Kb6eobqz9ar9GyUkBWdbWCzhOdYhejJ4mmbAeE/FRKLyoIiQ1CaNDIfFSn8+cyaXkFeChpKjlYsQaoRrx6FvN0tb1WjvORsFMYzhms09RUsFTqXlSBsbQ2riWHnoSd7+7NnuiIYMuyVtQgpqpQxm5iB+RofQb0kj4IY6/6E8PNj5ZlD2dHVPgK1pChFk18prW6tzAqvrsv3kWSm1KF/9MP8W6u3e5fZcdGqkKJQ1NuP+JEsX74v9yALva4+g5iGIT5Kl0UgHT7sC4cQkuYhfRiK5HIHIz0QS40/RcZCPrKIQaSXKWKQTq1HPs5nuciMNvU8wqxBay1ZVRGK1pP9hGg9kG/wCcDdHS106RtkRu6NcXFjO7YE8nFwcMby/a4fELp2By00fQStv0txNFzcjq2AnMTb7tkLlwaCg3d1BTfGX7uzo2vePb5xzBzILOUYCx7bohXZtf/S3ZaM+2MtXQPfdF27s/vGndngbzLMgXx46iJt7JfxuK1akf2oU3Tk8BFR1w87Gq/dCT5/J6Nxx7R5kPtuv4zT4rZoRe5TkLuNe691Ne6/f+3OwP7g+/f3N5oD2U071YlfvAVv6bPW28u7hT3fvH8nL/6wX0I2fx7ZEfzbb8GXtvw8spTtnxCRHWKH2EqvOZxz3hadc/jp9W3RTw5vwLbIyQ6xsuwQa+uZQhjAYcDChveAc8GPuQIQrgRw6IA3m4AcKC+GpACAR1GUqQ0gt+Mm0JUNbdDxsBxmHgB8V2sTkKSSY5CkJJxuUZDjkxhDeMOBE+CNbzq03t4wecs2ILdqla5JD4rbSilIO1Dhb/5bUHKSoN1V2dDX0WAZiJSF48Aa6meBGSnh28mkXO/yEgxxfbgEcQpYWJFyhjLX+3qpZSA6kyDFhODfEucAVUIMUNUZYD2VXE+it00EFM+VADTkAiyuy/X2B3CiPQCuQ8exqEkLHVpentCvEEuTxXmQwhMWkpqgTEjWqMJBXaE5ylJ4qbsDqe9fpYrJGRGrdQJJ05A4FoEZHYPVsxzEV9VU2B/KhLR0tUJfqW0lmb2e2aFsjilmKFsKTARQI9AGxvQaPNLoWTIoOz3iDFbRPyAKYKl4aZDGORtL1FdqgMmXCjyTwRA7pIEFSL8XPrQOCXndnCwZka8QZoJVtArEZDKELED6swpSEXOwLqWpOjw5moJU6wE8mIVeMWWK/m6tadgj7YzYGiGRB+HMWfDglcnJVlI3SKg1BmAm8KQQERhNhBkA8oYBoChQzcoZjkngfq3xyQtUF4FV9J/nESYfLNTAyfeogndarg/oa4foGGCZPlgas+91tGTEsz+z99A2A8J49pCd7z/7FTkkhU1I9F4cVeNOC8aAf4XkHQIqdSTYTisgHrFByac5gRVlkMnKC8zOSjey04fEIwhsohWQfQJSk5mZRUrCFJF1I2QqX54JzFC8OjbRSkglyLI0vqAYxBA5CAowhBWUXQBWGf1244UfnwJhhnk5R6aECWqWIAmBSJweKwML5ebmNgVLVTUDiBJhofjdsP6IvTTcSfqqELbpHJ2QBbLg7KEIFmSiAk26PjCBAxZqP95aDncSjuf3EFWd5z8QJdL3dJLk+fPmQA5TjlPzd59yaG1qH7q5kY/jdw/4NY9OV/n5nRcl7hG13Djv12IO5ORn47RTGRffWw1ChsKmRq3Iu7DH7/bsBb+q5p1josSuI6OiRtKsQ+vjC9c+zrhIi9uKiw97ExPdKYjb4f0tVaOziaLEFrfbGYmzu82BdNHmp/CLt8dbAYmfnr7XDN/vJDN6vu9pngJHR/h8AGDKvcecV24Xafglz/vNtn/JdPoU/mG/si2u/X6w034R25qzQ6wtJ4cDLtuiAw6Trtuiye1zaNkh1pUdYm3ZIeuvVukEtR1JLn0d5W9MKgGKywH3kAMQ0BYAJcpyp76k8omS6wBRbVYNORZwALyJqOIoeu2Due+coNwbAOjtAC5R4A0NfQwodymOivpuApRJVg0BDFHCXBQQSblzAW2ubUoAKK4F3IF2uNo246qEvodRAQ/7AiwFOcqC1SLQhiHeUFICAZ/AXGl5mxMFaSfwzrXcBd6A4ihvovZqVIn3daeNQdBXEnYmUKWzYbV6szYM+bZBmXt1pnRurrR4BjAktwEAiFsNpa4ND5VOUQHeb0SVPCif2CAkFXJCGDHkQV8M2WcqYwAAi8eC6EGeDvQ8Z899Mm1dtPwonOXpwZwmGABOE5Dr5JTLmSDwHYA+Vwri5OTkQv0JvJELHA5DS+RuDJLGKmND6lHP5HQ2YfRUlAHo2Hp2jTChTHyayYrlRYg9uotS1CxVhMD6rmyvhHSb0g4tQQpC8lWBAGWCfKOn8FCvpDAb1BSEiUfTw+x8QThYW38/tDJHgOkjwxBtqtSjAMBTTNeRFKSpWhuzDFHHwiECrK0VkKNi4GRLoyXJJp9kMFRkhwEQw8LknJGva2Ihs1LIOxTI9QJnGRko1JBgba31PCI3VHjCxnK867aG9jDWE7FuyMGyfthYjDjaWtprM+Npy0Lozx7iaAUQLlii2XHLQz6AtUOGwkBMWuZirfsa6rHAs1YhLw/KCvSDRKRJB+kmNWEIB7mO7ZUjp5+OkfPpCc4mLhFtCgNrbAWEK4io0PumsHVDKSwBR1AUKwt0hm6uVz0ZQnplyirIrwyx9Z7i8CCwxlZACpn5EhkYY2GkMp+pZwMABYGRQQgh00gI4YRANE+aLTaAVdS8Z0/zU8bT/pxQbYREIJWbIEzLzPFk8TL5X4HCF0MkYenVJAXRFzWlEWBWN1aMpzt7AL5sWfqOboAZjeKnjvHRVSGQKUyRxoZHJuTECk3EoDDfQ1eZXwEGSZOKEZatimCoIK/MI1lcv4Fh6JX4aSAy4qHqywFqzn5lGmDArKluBg332d4vt2QYeuMG+fjwu/GJtxs/rzo/eliU6Dc7Kjo8OmoO5MI4dgy8d2ErICfd3enUVHd69/7l8fR8cKe54+muYOygfbwlEEdHkoKMuXf5VYla7uJfGLjdfOV8i8gcyNj44fj3aLTGrYDgg2hKBFPNPaOiK18mdrp8Hk8/fNeR7LydAetvanw8Dv+0T9n+lcY91Jh9z3a4ZLqzpcXRfu3XirNDrK1tBKl12hbVbp8VsUOsKzvE2rJD1t9kexTgXCcBXL6YBE5b28QbJwCcGmwNoiQ6cvHNCbybO3EVSj/pa+j71RXaZ2wNgmfpxwCIdsDVXoX2XG1H3/UAl3LvrYRwPcyFzBUvQ1wnrvc1eEcVKzcBkhUBK6ObKlN8YKnYUKAKL8A7ffUg/K1+bk72f0IOKDkA7fQlyPHSq1F9DcdvEZsB8fKEQk+ttBcONmkNhVBUQUSOQJOhCfS6dH1OaKiB7yEe5oBWMSIt1OaEcwrDWQSkG1ABW032A6NID9rwwn5YvdwHBwBcShfv469aUkJMHGD0weSmQAa9hr1C/UFyMKi+wnMfs04KPsZzRq4g6HVVuCZ72FgjVByF014p6f5fhwl9QlrZBr1/jP8hgZzPhJQsr5jQtBgjC8zI0pDT9SNMkBT6QzivUg7SmGpfNQ90SFAAGBIJ7BqFLwAkJMACRADyOp4aCklVGJ+ZkwpNAvyXwIsGM7Ig5IwMKvRZ5/AIuk5ST6Q21cvz1MBS+WqgiC/woSDOBFO6DJH0Fi5AzskzYXABQqaSvf7WAPEwqsSkrzEojfD5qgDE2ewsqVBsjNRWiLM5ixB/YSyUGX0WIHJ/1SKkwD87FgIlC362pzVAAHLwpqWv+ABOk9QAHhbTRFL3EAELnYblji5P5zgk/FsE3dIR5p1HZBEbm7w5Wr6dW3BmJxzXltWPp7cNBB9amxCsHRLuA0LSMmtCt3iwKoRrqIHXtdIzEGrIAY4nvm0ClodYzmERRD/eInRFdDhjsL5fOnsCwjHWG9Nbh8We/ZJWIaEaFhZRU919Qo02hMFLVijIr+RCtV5SxgZrbAUkTGCqVHMEXqAIMqWFG6GpZnE8nQchjFQtPW1hPK0zxuwDq4j4PJ6xOkQnrNGhfdVGTqC8RufBJHrDsmT0JUi1NCeVpCAHw84yCTCraT+/HliKGm1cmYKF7vmBGWXE4WnotVUhkJItqQkaHJbXsIO8CFMFW1dUHdQNZWxpCMOX7a9egOgk+V5gXm9RELoLAEmHqmYXajzNIAFczBqGxr9PwxOruIGnvEShw2IIb1oCb4zl1yQkZ3lFT29oqjv7Yc+PwfvvVc0/zhAlxl+4MJ3xeMwcyEnKIXp8cqvm7NRU995d6t3TjvOiRLcutyNjO81akbjGU++L3t2ad08vvw187F6nX9WVz6tEiaNTjO+PuE+ZAzl1MePLd/G7dbcCMuvmNtAJflcy5qsGbs/O90xnOLp1xd+bn/3RrP9h4OJb+OjqtP0rjcE03F7S9iGQUXVERNiv/Vpvdoi1tY0gz8G26DmH5+mwDaI/7+Dw3DaQ0F91wD3/nJON9xxejz8ATqyBto+N2i0AAAAASUVORK5CYII=", "public": true } ], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.scss index a4ec35ee26..7cbd58f5f7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.scss @@ -85,6 +85,7 @@ $warning-color: #FAA405; flex-direction: row; align-items: center; justify-content: space-between; + gap: 8px; padding: 5px 16px; .api-item-title { display: flex; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts index 0183c6e1f2..1401104352 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts @@ -105,8 +105,7 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy { this.currentState = this.ctx.stateController.getStateId(); this.ctx.stateController.stateId().subscribe((state) => { - // @ts-ignore - this.ctx.dashboardWidget.updateCustomHeaderActions(); + this.ctx.updateParamsFromData(true); this.currentState = state; this.cd.markForCheck(); }); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.html index d8e908acce..3ada7fc5a0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-data-key-row.component.html @@ -21,53 +21,56 @@ class="tb-label-field tb-inline-field" appearance="outline" subscriptSizing="dynamic"> - - - + + + +
+ + + + + +
- - - - - -
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts index b71b406bfa..16fca5a94b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts @@ -39,7 +39,7 @@ import { ApiUsageSettingsContext } from "@home/components/widget/lib/settings/cards/api-usage-settings.component.models"; import { deepClone } from "@core/utils"; -import { Observable } from "rxjs"; +import { Observable, of } from "rxjs"; import { DataKeyConfigDialogComponent, DataKeyConfigDialogData @@ -81,12 +81,16 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent { }; } + protected doUpdateSettings(settingsForm: UntypedFormGroup, settings: WidgetSettings) { + settingsForm.setControl('dataKeys', this.prepareDataKeysFormArray(settings?.dataKeys), {emitEvent: false}); + } + dataKeysFormArray(): UntypedFormArray { return this.apiUsageWidgetSettingsForm.get('dataKeys') as UntypedFormArray; } - trackByDataKey(index: number, dataKeyControl: AbstractControl): any { - return dataKeyControl; + trackByDataKey(index: number): any { + return index; } get dragEnabled(): boolean { @@ -112,7 +116,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent { current: null }; const dataKeysArray = this.apiUsageWidgetSettingsForm.get('dataKeys') as UntypedFormArray; - const dataKeyControl = this.fb.control(dataKey, [this.mapDataKeyValidator()]); + const dataKeyControl = this.fb.control(dataKey, [this.apiUsageDataKeyValidator()]); dataKeysArray.push(dataKeyControl); } @@ -124,6 +128,16 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent { return apiUsageDefaultSettings; } + protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { + return { + dsEntityAliasId: settings?.dsEntityAliasId, + dataKeys: settings?.dataKeys, + targetDashboardState: settings?.targetDashboardState, + background: settings?.background, + padding: settings.padding + }; + } + protected onSettingsSet(settings: WidgetSettings) { this.apiUsageWidgetSettingsForm = this.fb.group({ dsEntityAliasId: [settings?.dsEntityAliasId], @@ -138,7 +152,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent { const dataKeysControls: Array = []; if (dataKeys) { dataKeys.forEach((dataLayer) => { - dataKeysControls.push(this.fb.control(dataLayer, [this.mapDataKeyValidator()])); + dataKeysControls.push(this.fb.control(dataLayer, [this.apiUsageDataKeyValidator()])); }); } return this.fb.array(dataKeysControls); @@ -151,7 +165,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent { protected updateValidators() { } - mapDataKeyValidator = (): ValidatorFn => { + apiUsageDataKeyValidator = (): ValidatorFn => { return (control: AbstractControl): ValidationErrors | null => { const value: ApiUsageDataKeysSettings = control.value; if (!value?.label || !value?.current || !value?.maxLimit || !value?.status) { @@ -190,4 +204,8 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent { private generateDataKey(key: DataKey): DataKey { return this.callbacks.generateDataKey(key.name, key.type, null, false, null); } + + fetchDashboardStates(searchText?: string): Observable> { + return of(this.callbacks.fetchDashboardStates(searchText)); + } } diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 4a22129ba8..c582a21b3f 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -147,6 +147,7 @@ export interface WidgetAction extends IWidgetAction { export interface IDashboardWidget { updateWidgetParams(): void; + updateParamsFromData(detectChanges?: boolean): void; } export class WidgetContext { @@ -478,6 +479,10 @@ export class WidgetContext { } } + updateParamsFromData(detectChanges = false) { + this.dashboardWidget.updateParamsFromData(detectChanges); + } + updateAliases(aliasIds?: Array) { this.aliasController.updateAliases(aliasIds); } diff --git a/ui-ngx/src/assets/dashboard/api_usage.json b/ui-ngx/src/assets/dashboard/api_usage.json index 77dc18c11e..3fc49e171f 100644 --- a/ui-ngx/src/assets/dashboard/api_usage.json +++ b/ui-ngx/src/assets/dashboard/api_usage.json @@ -1484,18 +1484,7 @@ "titleStyle": null, "configMode": "basic", "actions": { - "headerButton": [ - { - "name": "{i18n:api-usage.view-details}", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "transport", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "id": "6ef12f6a-0266-25cf-6ca5-5dcb772252c6" - } - ] + "headerButton": [] }, "showTitleIcon": false, "titleIcon": "thermostat", @@ -1868,18 +1857,7 @@ "titleStyle": null, "configMode": "basic", "actions": { - "headerButton": [ - { - "name": "{i18n:api-usage.view-details}", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "transport", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "id": "6ef12f6a-0266-25cf-6ca5-5dcb772252c6" - } - ] + "headerButton": [] }, "showTitleIcon": false, "titleIcon": "thermostat", @@ -2298,16 +2276,6 @@ "stateEntityParamName": null, "openRightLayout": false, "id": "f9f08190-9ed9-d802-5b7a-c57ff84b5648" - }, - { - "name": "{i18n:api-usage.view-details}", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "rule_engine_execution", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "id": "1aec196b-44ba-ddf4-c4dc-c3f60c1eb6fc" } ] }, @@ -2718,18 +2686,7 @@ "titleStyle": null, "configMode": "basic", "actions": { - "headerButton": [ - { - "name": "{i18n:api-usage.view-details}", - "icon": "insert_chart", - "type": "openDashboardState", - "targetDashboardStateId": "telemetry_persistence", - "setEntityId": false, - "stateEntityParamName": null, - "openRightLayout": false, - "id": "16707efb-e572-bd02-c219-55fc1b0f672a" - } - ] + "headerButton": [] }, "showTitleIcon": false, "titleIcon": "thermostat", @@ -5408,9 +5365,9 @@ "customButtonStyle": {}, "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", - "type": "updateDashboardState", + "type": "openDashboardState", "targetDashboardStateId": "rule_engine_statistics", - "setEntityId": false, + "setEntityId": true, "stateEntityParamName": null, "openRightLayout": false, "openInSeparateDialog": false, @@ -5835,9 +5792,9 @@ "customButtonStyle": {}, "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", - "type": "updateDashboardState", + "type": "openDashboardState", "targetDashboardStateId": "rule_engine_statistics", - "setEntityId": false, + "setEntityId": true, "stateEntityParamName": null, "openRightLayout": false, "openInSeparateDialog": false, @@ -6272,9 +6229,9 @@ "customButtonStyle": {}, "useShowWidgetActionFunction": null, "showWidgetActionFunction": "return true;", - "type": "updateDashboardState", + "type": "openDashboardState", "targetDashboardStateId": "rule_engine_statistics", - "setEntityId": false, + "setEntityId": true, "stateEntityParamName": null, "openRightLayout": false, "openInSeparateDialog": false, @@ -13275,7 +13232,7 @@ "padding": "0 16px" }, "useShowWidgetActionFunction": true, - "showWidgetActionFunction": "console.log(widgetContext.stateController.getStateId(), widgetContext.settings.targetDashboardState)\nreturn widgetContext.stateController.getStateId() !== widgetContext.settings.targetDashboardState && widgetContext.settings.targetDashboardState;", + "showWidgetActionFunction": "return widgetContext.stateController.getStateId() !== widgetContext.settings.targetDashboardState && widgetContext.settings.targetDashboardState;", "type": "custom", "customFunction": "const state = widgetContext.settings.targetDashboardState?.length ? widgetContext.settings.targetDashboardState : 'default';\nwidgetContext.stateController.updateState(state, widgetContext.stateController.getStateParams(), false);", "openInSeparateDialog": false, @@ -13340,25 +13297,33 @@ "sizeX": 7, "sizeY": 5, "row": 0, - "col": 0 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, "d0a10a8f-8f48-f9d6-8306-d12af9b49690": { "sizeX": 7, "sizeY": 5, "row": 0, - "col": 7 + "col": 7, + "resizable": true, + "mobileHeight": 6 }, "4544080d-9b6f-b592-9cd4-0e0335d33857": { "sizeX": 7, "sizeY": 5, "row": 5, - "col": 0 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, "5d0f2f57-499d-1324-8e1b-cfbc0b3149d2": { "sizeX": 7, "sizeY": 5, "row": 5, - "col": 7 + "col": 7, + "resizable": true, + "mobileHeight": 6 } }, "gridSettings": { @@ -13390,19 +13355,25 @@ "sizeX": 24, "sizeY": 5, "row": 7, - "col": 0 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, "fa938580-33db-f1b3-fafc-bc3e3784ad57": { "sizeX": 12, "sizeY": 7, "row": 0, - "col": 0 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, "2ee89893-4e38-5331-95b7-3fd4f310c5a7": { "sizeX": 12, "sizeY": 7, "row": 0, - "col": 12 + "col": 12, + "resizable": true, + "mobileHeight": 6 } }, "gridSettings": { @@ -13434,7 +13405,9 @@ "sizeX": 24, "sizeY": 39, "row": 0, - "col": 0 + "col": 0, + "resizable": true, + "mobileHeight": 4 } }, "gridSettings": { @@ -13463,19 +13436,25 @@ "sizeX": 12, "sizeY": 4, "row": 0, - "col": 0 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, "fb155957-1af4-233e-e2fb-09e648e75d6e": { "sizeX": 6, "sizeY": 4, "row": 4, - "col": 0 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, "4817e33b-87be-5be3-eaca-ca68a2eb4e0c": { "sizeX": 6, "sizeY": 4, "row": 4, - "col": 6 + "col": 6, + "resizable": true, + "mobileHeight": 6 } }, "gridSettings": { @@ -13536,19 +13515,25 @@ "sizeX": 12, "sizeY": 4, "row": 0, - "col": 0 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, "79056202-c92b-1dae-ce49-318ec52e2d3b": { "sizeX": 6, "sizeY": 4, "row": 4, - "col": 0 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, "966ffee7-ba0d-8e54-f903-e8d015ca8cd2": { "sizeX": 6, "sizeY": 4, "row": 4, - "col": 6 + "col": 6, + "resizable": true, + "mobileHeight": 6 } }, "gridSettings": { @@ -13609,19 +13594,25 @@ "sizeX": 12, "sizeY": 4, "row": 0, - "col": 0 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, "84fbe63a-bcb6-7bc1-8af0-46b3b1ee5adc": { "sizeX": 6, "sizeY": 4, "row": 4, - "col": 0 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, "43a2b982-6c02-d9bd-71ee-34e8e6cf8893": { "sizeX": 6, "sizeY": 4, "row": 4, - "col": 6 + "col": 6, + "resizable": true, + "mobileHeight": 6 } }, "gridSettings": { @@ -13682,19 +13673,25 @@ "sizeX": 12, "sizeY": 4, "row": 0, - "col": 0 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, "a43598d1-7bfd-f329-ee61-c343f34f069f": { "sizeX": 6, "sizeY": 4, "row": 4, - "col": 0 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, "3ebd62a8-dcb7-c96b-8571-e61084248f5b": { "sizeX": 6, "sizeY": 4, "row": 4, - "col": 6 + "col": 6, + "resizable": true, + "mobileHeight": 6 } }, "gridSettings": { @@ -13755,19 +13752,25 @@ "sizeX": 12, "sizeY": 4, "row": 0, - "col": 0 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, "a1b5731c-e3b3-8cfb-7c50-3abcdce891d2": { "sizeX": 6, "sizeY": 4, "row": 4, - "col": 0 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, "efc8d4e9-dee2-b677-c378-c1a666543bf4": { "sizeX": 6, "sizeY": 4, "row": 4, - "col": 6 + "col": 6, + "resizable": true, + "mobileHeight": 6 } }, "gridSettings": { @@ -13828,19 +13831,25 @@ "sizeX": 12, "sizeY": 4, "row": 0, - "col": 0 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, "1249d3e2-6b3a-4e4a-65e9-6ed22959871e": { "sizeX": 6, "sizeY": 4, "row": 4, - "col": 0 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, "c2f2da29-741d-54f6-5f1d-6f6ae616ea02": { "sizeX": 6, "sizeY": 4, "row": 4, - "col": 6 + "col": 6, + "resizable": true, + "mobileHeight": 6 } }, "gridSettings": { @@ -13901,19 +13910,25 @@ "sizeX": 12, "sizeY": 4, "row": 0, - "col": 0 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, "b12fb875-89fe-af4c-b344-bf4178de419f": { "sizeX": 6, "sizeY": 4, "row": 4, - "col": 0 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, "0b00099d-d131-3e8b-97ce-c4b8d7bcab1f": { "sizeX": 6, "sizeY": 4, "row": 4, - "col": 6 + "col": 6, + "resizable": true, + "mobileHeight": 6 } }, "gridSettings": { @@ -13974,19 +13989,25 @@ "sizeX": 12, "sizeY": 4, "row": 0, - "col": 0 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, "ab5518c1-34d6-7e17-04b4-6520496d5fe1": { "sizeX": 6, "sizeY": 4, "row": 4, - "col": 0 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, "2e7326ac-98d3-e68c-b7cf-948118a3f140": { "sizeX": 6, "sizeY": 4, "row": 4, - "col": 6 + "col": 6, + "resizable": true, + "mobileHeight": 6 } }, "gridSettings": { @@ -14047,19 +14068,25 @@ "sizeX": 12, "sizeY": 4, "row": 0, - "col": 0 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, "e0fe9887-d61c-7813-05a7-f60811e5c5bf": { "sizeX": 6, "sizeY": 4, "row": 4, - "col": 0 + "col": 0, + "resizable": true, + "mobileHeight": 6 }, "99a40c35-c232-16c5-c42f-3cc80ddb9243": { "sizeX": 6, "sizeY": 4, "row": 4, - "col": 6 + "col": 6, + "resizable": true, + "mobileHeight": 6 } }, "gridSettings": { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index f1fc7835f5..32a6fb8e18 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -9520,8 +9520,11 @@ "label": "Label", "state-name": "State name", "status": "Status", + "status-required": "Status is required.", "limit": "Max limit", + "limit-required": "Max limit is required.", "current-number": "Current number", + "current-number-required": "Current number is required.", "add-key": "Add key", "no-key": "No key", "delete-key": "Delete key", From 3abb23780d493b81793144899682ec19c2f6f2b6 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Wed, 3 Sep 2025 17:21:50 +0300 Subject: [PATCH 098/216] Added TimeUnit support --- ...alculatedFieldManagerMessageProcessor.java | 3 +- .../cf/ctx/state/CalculatedFieldCtx.java | 5 +- .../cf/CalculatedFieldIntegrationTest.java | 3 +- ...eofencingCalculatedFieldConfiguration.java | 7 ++- ...SupportedCalculatedFieldConfiguration.java | 31 ++++++++++- ...ncingCalculatedFieldConfigurationTest.java | 51 +++++++++++++++++-- .../dao/cf/BaseCalculatedFieldService.java | 10 +++- .../service/CalculatedFieldServiceTest.java | 16 +++--- 8 files changed, 106 insertions(+), 20 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index 6a100d9d50..1d38480b91 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -60,7 +60,6 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Function; @@ -454,7 +453,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware log.debug("[{}][{}] Dynamic arguments refresh task for CF already exists!", tenantId, cf.getId()); return; } - long refreshDynamicSourceInterval = TimeUnit.SECONDS.toMillis(scheduledCfConfig.getScheduledUpdateIntervalSec()); + long refreshDynamicSourceInterval = scheduledCfConfig.getTimeUnit().toMillis(scheduledCfConfig.getScheduledUpdateInterval()); var scheduledMsg = new CalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfCtx.getCfId()); ScheduledFuture scheduledFuture = systemContext diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 9f6896392e..260cbe447f 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -325,8 +325,9 @@ public class CalculatedFieldCtx { if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration thisConfig && other.calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration otherConfig) { boolean refreshTriggerChanged = thisConfig.isScheduledUpdateEnabled() != otherConfig.isScheduledUpdateEnabled(); - boolean refreshIntervalChanged = thisConfig.getScheduledUpdateIntervalSec() != otherConfig.getScheduledUpdateIntervalSec(); - return refreshTriggerChanged || refreshIntervalChanged; + boolean refreshIntervalChanged = thisConfig.getScheduledUpdateInterval() != otherConfig.getScheduledUpdateInterval(); + boolean timeUnitChanged = thisConfig.getTimeUnit() != otherConfig.getTimeUnit(); + return refreshTriggerChanged || refreshIntervalChanged || timeUnitChanged; } return false; } diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index f9e5dec789..8df1d04f2e 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -799,7 +799,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes cfg.setOutput(out); // Enable scheduled refresh with a 6-second interval - cfg.setScheduledUpdateIntervalSec(6); + cfg.setScheduledUpdateInterval(6); + cfg.setTimeUnit(TimeUnit.SECONDS); cf.setConfiguration(cfg); CalculatedField savedCalculatedField = doPost("/api/calculatedField", cf, CalculatedField.class); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java index ef20ad16bb..b009142f37 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java @@ -28,13 +28,15 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.TimeUnit; @Data public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration { private EntityCoordinates entityCoordinates; private List zoneGroups; - private int scheduledUpdateIntervalSec; + private int scheduledUpdateInterval; + private TimeUnit timeUnit; private Output output; @@ -63,11 +65,12 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal @Override public boolean isScheduledUpdateEnabled() { - return scheduledUpdateIntervalSec > 0 && zoneGroups.stream().anyMatch(ZoneGroupConfiguration::hasDynamicSource); + return scheduledUpdateInterval > 0 && zoneGroups.stream().anyMatch(ZoneGroupConfiguration::hasDynamicSource); } @Override public void validate() { + ScheduledUpdateSupportedCalculatedFieldConfiguration.super.validate(); if (entityCoordinates == null) { throw new IllegalArgumentException("Geofencing calculated field entity coordinates must be specified!"); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java index 0d386577ab..afc8402722 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java @@ -17,12 +17,39 @@ package org.thingsboard.server.common.data.cf.configuration; import com.fasterxml.jackson.annotation.JsonIgnore; +import java.util.EnumSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; + public interface ScheduledUpdateSupportedCalculatedFieldConfiguration extends CalculatedFieldConfiguration { + Set SUPPORTED_TIME_UNITS = + EnumSet.of(TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS); + + @JsonIgnore boolean isScheduledUpdateEnabled(); - int getScheduledUpdateIntervalSec(); + int getScheduledUpdateInterval(); + + void setScheduledUpdateInterval(int interval); + + TimeUnit getTimeUnit(); + + void setTimeUnit(TimeUnit timeUnit); - void setScheduledUpdateIntervalSec(int interval); + @Override + default void validate() { + if (!isScheduledUpdateEnabled()) { + return; + } + var timeUnit = getTimeUnit(); + if (timeUnit == null) { + throw new IllegalArgumentException("Scheduled update time unit should be specified!"); + } + if (!SUPPORTED_TIME_UNITS.contains(timeUnit)) { + throw new IllegalArgumentException("Unsupported scheduled update time unit: " + timeUnit + + ". Allowed: " + SUPPORTED_TIME_UNITS); + } + } } diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java index 1b12c37820..90d2768608 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java @@ -17,6 +17,8 @@ package org.thingsboard.server.common.data.cf.configuration; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.cf.CalculatedFieldType; @@ -25,6 +27,7 @@ import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupC import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; @@ -33,6 +36,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration.SUPPORTED_TIME_UNITS; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; @@ -122,10 +126,50 @@ public class GeofencingCalculatedFieldConfigurationTest { verify(zoneGroupConfigurationB, never()).validate(); } + @Test + void validateShouldThrowWhenScheduledUpdateIntervalIsSetButTimeUnitIsNotSpecified() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setScheduledUpdateInterval(60); + var zg = new ZoneGroupConfiguration("allowedZones", "perimeter", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + zg.setRefDynamicSourceConfiguration(mock(RelationQueryDynamicSourceConfiguration.class)); + cfg.setZoneGroups(List.of(zg)); + cfg.setTimeUnit(null); + + assertThat(cfg.isScheduledUpdateEnabled()).isTrue(); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Scheduled update time unit should be specified!"); + } + + @ParameterizedTest + @EnumSource(TimeUnit.class) + void validateShouldThrowWhenScheduledUpdateIntervalIsSetButTimeUnitIsNotSupported(TimeUnit timeUnit) { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setScheduledUpdateInterval(60); + var zg = new ZoneGroupConfiguration("allowedZones", "perimeter", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + zg.setRefDynamicSourceConfiguration(mock(RelationQueryDynamicSourceConfiguration.class)); + cfg.setZoneGroups(List.of(zg)); + cfg.setEntityCoordinates(mock(EntityCoordinates.class)); + cfg.setTimeUnit(timeUnit); + + assertThat(cfg.isScheduledUpdateEnabled()).isTrue(); + + if (SUPPORTED_TIME_UNITS.contains(timeUnit)) { + assertThatCode(cfg::validate).doesNotThrowAnyException(); + return; + } + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported scheduled update time unit: " + timeUnit + + ". Allowed: " + SUPPORTED_TIME_UNITS); + } + + @Test void scheduledUpdateDisabledWhenIntervalIsZero() { var cfg = new GeofencingCalculatedFieldConfiguration(); - cfg.setScheduledUpdateIntervalSec(0); + cfg.setScheduledUpdateInterval(0); assertThat(cfg.isScheduledUpdateEnabled()).isFalse(); } @@ -135,17 +179,18 @@ public class GeofencingCalculatedFieldConfigurationTest { var zoneGroupConfigurationMock = mock(ZoneGroupConfiguration.class); when(zoneGroupConfigurationMock.hasDynamicSource()).thenReturn(false); cfg.setZoneGroups(List.of(zoneGroupConfigurationMock)); - cfg.setScheduledUpdateIntervalSec(60); + cfg.setScheduledUpdateInterval(60); assertThat(cfg.isScheduledUpdateEnabled()).isFalse(); } @Test void scheduledUpdateEnabledWhenIntervalIsGreaterThanZeroAndDynamicArgumentsPresent() { var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setTimeUnit(TimeUnit.SECONDS); var zoneGroupConfigurationMock = mock(ZoneGroupConfiguration.class); when(zoneGroupConfigurationMock.hasDynamicSource()).thenReturn(true); cfg.setZoneGroups(List.of(zoneGroupConfigurationMock)); - cfg.setScheduledUpdateIntervalSec(60); + cfg.setScheduledUpdateInterval(60); assertThat(cfg.isScheduledUpdateEnabled()).isTrue(); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 436f75457c..4e84cdebe1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -38,6 +38,7 @@ import org.thingsboard.server.dao.service.DataValidator; import java.util.List; import java.util.Optional; +import java.util.concurrent.TimeUnit; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validatePageLink; @@ -98,10 +99,15 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements if (!configuration.isScheduledUpdateEnabled()) { return; } - int tenantProfileMinAllowedValue = tbTenantProfileCache.get(calculatedField.getTenantId()) + TimeUnit timeUnit = configuration.getTimeUnit(); + long intervalInSeconds = timeUnit.toSeconds(configuration.getScheduledUpdateInterval()); + int tenantProfileMinAllowedSecValue = tbTenantProfileCache.get(calculatedField.getTenantId()) .getDefaultProfileConfiguration() .getMinAllowedScheduledUpdateIntervalInSecForCF(); - configuration.setScheduledUpdateIntervalSec(Math.max(configuration.getScheduledUpdateIntervalSec(), tenantProfileMinAllowedValue)); + if (intervalInSeconds < tenantProfileMinAllowedSecValue) { + configuration.setScheduledUpdateInterval(tenantProfileMinAllowedSecValue); + configuration.setTimeUnit(TimeUnit.SECONDS); + } } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index 81041180c7..0d70e22339 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java @@ -46,6 +46,7 @@ import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -117,7 +118,8 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { cfg.setZoneGroups(List.of(zoneGroupConfiguration)); // Set a scheduled interval to some value - cfg.setScheduledUpdateIntervalSec(600); + cfg.setScheduledUpdateInterval(600); + cfg.setTimeUnit(TimeUnit.SECONDS); // Create & save Calculated Field CalculatedField cf = new CalculatedField(); @@ -136,7 +138,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration(); // Assert: the interval is saved, but scheduling is not enabled - int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateIntervalSec(); + int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateInterval(); boolean scheduledUpdateEnabled = geofencingCalculatedFieldConfiguration.isScheduledUpdateEnabled(); assertThat(savedInterval).isEqualTo(600); @@ -167,7 +169,8 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { cfg.setZoneGroups(List.of(zoneGroupConfiguration)); // Enable scheduling with an interval below tenant min - cfg.setScheduledUpdateIntervalSec(600); + cfg.setScheduledUpdateInterval(600); + cfg.setTimeUnit(TimeUnit.SECONDS); // Create & save Calculated Field CalculatedField cf = new CalculatedField(); @@ -186,7 +189,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration(); // Assert: the interval is clamped up to tenant profile min - int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateIntervalSec(); + int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateInterval(); int min = tbTenantProfileCache.get(tenantId) .getDefaultProfileConfiguration() @@ -225,7 +228,8 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { // Enable scheduling with an interval greater than tenant min int valueFromConfig = min + 100; - cfg.setScheduledUpdateIntervalSec(valueFromConfig); + cfg.setScheduledUpdateInterval(valueFromConfig); + cfg.setTimeUnit(TimeUnit.SECONDS); // Create & save Calculated Field CalculatedField cf = new CalculatedField(); @@ -244,7 +248,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration(); // Assert: the interval is clamped up to tenant profile min (or stays >= original if already >= min) - int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateIntervalSec(); + int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateInterval(); assertThat(savedInterval).isEqualTo(valueFromConfig); calculatedFieldService.deleteCalculatedField(tenantId, saved.getId()); From 94c7c5b651a6b77cdc7270bda59722284bcdff9b Mon Sep 17 00:00:00 2001 From: devaskim Date: Thu, 4 Sep 2025 13:15:10 +0500 Subject: [PATCH 099/216] Added description to server.rest.rule_engine configuration property. --- application/src/main/resources/thingsboard.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index cf29b7c8be..e34f6b52fe 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -101,6 +101,7 @@ server: # Limit that prohibits resetting the password for the user too often. The value of the rate limit. By default, no more than 5 requests per hour reset_password_per_user: "${RESET_PASSWORD_PER_USER_RATE_LIMIT_CONFIGURATION:5:3600}" rule_engine: + # Deafult timeout for waiting response of REST API request to Rule Engine in milliseconds response_timeout: "${DEFAULT_RULE_ENGINE_RESPONSE_TIMEOUT:10000}" # Application info parameters From 468fc68aa7a3c7f91a0b5da0361d94039ba94715 Mon Sep 17 00:00:00 2001 From: devaskim Date: Thu, 4 Sep 2025 13:17:43 +0500 Subject: [PATCH 100/216] Fixed mistype in description of server.rest.rule_engine.response_timeout configuration property. --- application/src/main/resources/thingsboard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index e34f6b52fe..89be36f470 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -101,7 +101,7 @@ server: # Limit that prohibits resetting the password for the user too often. The value of the rate limit. By default, no more than 5 requests per hour reset_password_per_user: "${RESET_PASSWORD_PER_USER_RATE_LIMIT_CONFIGURATION:5:3600}" rule_engine: - # Deafult timeout for waiting response of REST API request to Rule Engine in milliseconds + # Default timeout for waiting response of REST API request to Rule Engine in milliseconds response_timeout: "${DEFAULT_RULE_ENGINE_RESPONSE_TIMEOUT:10000}" # Application info parameters From 15c10354163bda05a57c1fd9e126c082df3b09bc Mon Sep 17 00:00:00 2001 From: dshvaika Date: Thu, 4 Sep 2025 12:03:45 +0300 Subject: [PATCH 101/216] Updated logic due to review comments --- .../main/data/upgrade/basic/schema_update.sql | 26 ++++++- ...alculatedFieldManagerMessageProcessor.java | 57 +++++--------- .../controller/SystemInfoController.java | 2 + .../cf/ctx/state/GeofencingArgumentEntry.java | 16 +--- .../state/GeofencingCalculatedFieldState.java | 14 ++-- .../cf/ctx/state/GeofencingEvalResult.java | 7 +- .../cf/ctx/state/GeofencingZoneState.java | 8 +- .../server/utils/CalculatedFieldUtils.java | 2 +- .../cf/CalculatedFieldIntegrationTest.java | 4 +- .../GeofencingCalculatedFieldStateTest.java | 6 +- .../GeofencingValueArgumentEntryTest.java | 7 +- .../cf/ctx/state/GeofencingZoneStateTest.java | 8 +- .../utils/CalculatedFieldUtilsTest.java | 2 +- .../server/common/data/SystemParams.java | 2 + .../CalculatedFieldConfiguration.java | 1 + ...lationQueryDynamicSourceConfiguration.java | 12 +-- ...SupportedCalculatedFieldConfiguration.java | 10 +-- ...eofencingCalculatedFieldConfiguration.java | 9 ++- .../{ => geofencing}/GeofencingEvent.java | 2 +- .../GeofencingPresenceStatus.java | 2 +- .../GeofencingReportStrategy.java | 2 +- .../GeofencingTransitionEvent.java | 2 +- .../geofencing/ZoneGroupConfiguration.java | 6 +- .../DefaultTenantProfileConfiguration.java | 2 + ...onQueryDynamicSourceConfigurationTest.java | 27 ++++++- ...ortedCalculatedFieldConfigurationTest.java | 78 +++++++++++++++++++ ...ncingCalculatedFieldConfigurationTest.java | 50 +----------- .../ZoneGroupConfigurationTest.java | 2 +- .../dao/cf/BaseCalculatedFieldService.java | 20 ----- .../CalculatedFieldDataValidator.java | 66 ++++++++++++---- .../service/CalculatedFieldServiceTest.java | 53 +++++++++---- .../server/msa/cf/CalculatedFieldTest.java | 4 +- 32 files changed, 304 insertions(+), 205 deletions(-) rename common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/{ => geofencing}/GeofencingCalculatedFieldConfiguration.java (87%) rename common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/{ => geofencing}/GeofencingEvent.java (91%) rename common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/{ => geofencing}/GeofencingPresenceStatus.java (91%) rename common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/{ => geofencing}/GeofencingReportStrategy.java (91%) rename common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/{ => geofencing}/GeofencingTransitionEvent.java (90%) create mode 100644 common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java rename common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/{ => geofencing}/GeofencingCalculatedFieldConfigurationTest.java (77%) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 42040a1acb..320d3e5bdd 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -20,11 +20,29 @@ UPDATE tenant_profile SET profile_data = jsonb_set( profile_data, '{configuration}', - (profile_data -> 'configuration') || '{ - "minAllowedScheduledUpdateIntervalInSecForCF": 3600 - }'::jsonb, + (profile_data -> 'configuration') + || jsonb_strip_nulls( + jsonb_build_object( + 'minAllowedScheduledUpdateIntervalInSecForCF', + CASE + WHEN (profile_data -> 'configuration') ? 'minAllowedScheduledUpdateIntervalInSecForCF' + THEN NULL + ELSE to_jsonb(3600) + END, + 'maxRelationLevelPerCfArgument', + CASE + WHEN (profile_data -> 'configuration') ? 'maxRelationLevelPerCfArgument' + THEN NULL + ELSE to_jsonb(10) + END + ) + ), false ) -WHERE (profile_data -> 'configuration' -> 'minAllowedScheduledUpdateIntervalInSecForCF') IS NULL; +WHERE NOT ( + (profile_data -> 'configuration') ? 'minAllowedScheduledUpdateIntervalInSecForCF' + AND + (profile_data -> 'configuration') ? 'maxRelationLevelPerCfArgument' + ); -- UPDATE TENANT PROFILE CONFIGURATION END diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index 1d38480b91..f2265b3697 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -61,7 +61,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledFuture; import java.util.function.BiConsumer; -import java.util.function.Function; import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto; @@ -359,7 +358,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware if (existingTask != null) { existingTask.cancel(false); String reason = cfDeleted ? "deletion" : "update"; - log.debug("[{}][{}] Cancelled dynamic arguments refresh task due to CF " + reason + "!", tenantId, cfId); + log.debug("[{}][{}] Cancelled dynamic arguments refresh task due to CF {}!", tenantId, cfId, reason); } } @@ -400,9 +399,10 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware for (var linkProto : linksList) { var link = fromProto(linkProto); var cf = calculatedFields.get(link.cfId()); - applyToTargetCfEntityActors(link, callback, - cb -> new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, callback), - this::linkedTelemetryMsgForEntity); + withTargetEntities(link.entityId(), callback, (ids, cb) -> { + var linkedTelemetryMsg = new EntityCalculatedFieldLinkedTelemetryMsg(tenantId, sourceEntityId, proto.getMsg(), cf, cb); + ids.forEach(id -> linkedTelemetryMsgForEntity(id, linkedTelemetryMsg)); + }); } } @@ -594,48 +594,29 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } } - private void applyToTargetCfEntityActors(CalculatedFieldCtx calculatedFieldCtx, + private void applyToTargetCfEntityActors(CalculatedFieldCtx ctx, TbCallback callback, BiConsumer action) { - if (isProfileEntity(calculatedFieldCtx.getEntityId().getEntityType())) { - var ids = entityProfileCache.getEntityIdsByProfileId(calculatedFieldCtx.getEntityId()); - if (ids.isEmpty()) { - callback.onSuccess(); - return; - } - var multiCallback = new MultipleTbCallback(ids.size(), callback); - ids.forEach(id -> { - if (isMyPartition(id, multiCallback)) { - action.accept(id, multiCallback); - } - }); - return; - } - if (isMyPartition(calculatedFieldCtx.getEntityId(), callback)) { - action.accept(calculatedFieldCtx.getEntityId(), callback); - } + withTargetEntities(ctx.getEntityId(), callback, (ids, cb) -> ids.forEach(id -> action.accept(id, cb))); } - private void applyToTargetCfEntityActors(CalculatedFieldEntityCtxId link, TbCallback callback, - Function messageFactory, BiConsumer action) { - if (isProfileEntity(link.entityId().getEntityType())) { - var ids = entityProfileCache.getEntityIdsByProfileId(link.entityId()); + private void withTargetEntities(EntityId entityId, TbCallback parentCallback, BiConsumer, TbCallback> consumer) { + if (isProfileEntity(entityId.getEntityType())) { + var ids = entityProfileCache.getEntityIdsByProfileId(entityId); if (ids.isEmpty()) { - callback.onSuccess(); + parentCallback.onSuccess(); return; } - var multiCallback = new MultipleTbCallback(ids.size(), callback); - var msg = messageFactory.apply(multiCallback); - ids.forEach(id -> { - if (isMyPartition(id, multiCallback)) { - action.accept(id, msg); - } - }); + var multiCallback = new MultipleTbCallback(ids.size(), parentCallback); + var profileEntityIds = ids.stream().filter(id -> isMyPartition(id, multiCallback)).toList(); + if (profileEntityIds.isEmpty()) { + return; + } + consumer.accept(profileEntityIds, multiCallback); return; } - if (isMyPartition(link.entityId(), callback)) { - var msg = messageFactory.apply(callback); - action.accept(link.entityId(), msg); + if (isMyPartition(entityId, parentCallback)) { + consumer.accept(List.of(entityId), parentCallback); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java index 29f4daa783..b9968aefa9 100644 --- a/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java +++ b/application/src/main/java/org/thingsboard/server/controller/SystemInfoController.java @@ -162,6 +162,8 @@ public class SystemInfoController extends BaseController { } systemParams.setMaxArgumentsPerCF(tenantProfileConfiguration.getMaxArgumentsPerCF()); systemParams.setMaxDataPointsPerRollingArg(tenantProfileConfiguration.getMaxDataPointsPerRollingArg()); + systemParams.setMinAllowedScheduledUpdateIntervalInSecForCF(tenantProfileConfiguration.getMinAllowedScheduledUpdateIntervalInSecForCF()); + systemParams.setMaxRelationLevelPerCfArgument(tenantProfileConfiguration.getMaxRelationLevelPerCfArgument()); systemParams.setTrendzSettings(trendzSettingsService.findTrendzSettings(currentUser.getTenantId())); } systemParams.setMobileQrEnabled(Optional.ofNullable(qrCodeSettingService.findQrCodeSettings(TenantId.SYS_TENANT_ID)) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java index 509bb46c60..3794764351 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java @@ -23,7 +23,6 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.KvEntry; import java.util.Map; -import java.util.Objects; import java.util.stream.Collectors; @Data @@ -76,17 +75,10 @@ public class GeofencingArgumentEntry implements ArgumentEntry { } private Map toZones(Map entityIdKvEntryMap) { - return entityIdKvEntryMap.entrySet().stream().map(entry -> { - try { - if (entry.getValue().getJsonValue().isEmpty()) { - return null; - } - return Map.entry(entry.getKey(), new GeofencingZoneState(entry.getKey(), entry.getValue())); - } catch (Exception e) { - log.error("Failed to parse geofencing zone perimeter for entity id: {}", entry.getKey(), e); - return null; - } - }).filter(Objects::nonNull).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + return entityIdKvEntryMap.entrySet().stream() + .filter(entry -> entry.getValue().getJsonValue().isPresent()) + .collect(Collectors.toMap(Map.Entry::getKey, + entry -> new GeofencingZoneState(entry.getKey(), entry.getValue()))); } private boolean updateZone(Map.Entry zoneEntry) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java index e44829b3a0..ad53b44d62 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java @@ -24,10 +24,11 @@ import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.geo.Coordinates; +import org.thingsboard.server.actors.calculatedField.CalculatedFieldException; import org.thingsboard.server.common.data.cf.CalculatedFieldType; -import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy; -import org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -40,10 +41,10 @@ import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.INSIDE; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.OUTSIDE; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.INSIDE; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.OUTSIDE; @Data @Slf4j @@ -130,8 +131,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { getGeofencingArguments().forEach((argumentKey, argumentEntry) -> { ZoneGroupConfiguration zoneGroupCfg = zoneGroups.get(argumentKey); if (zoneGroupCfg == null) { - log.error("[{}][{}] Zone group config is missing for the {}", entityId, ctx.getCalculatedField().getId(), argumentKey); - return; + throw new RuntimeException("Zone group configuration is missing for the: " + entityId); } boolean createRelationsWithMatchedZones = zoneGroupCfg.isCreateRelationsWithMatchedZones(); List zoneResults = new ArrayList<>(argumentEntry.getZoneStates().size()); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingEvalResult.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingEvalResult.java index dff9a4d9ea..d7794466a8 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingEvalResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingEvalResult.java @@ -16,8 +16,9 @@ package org.thingsboard.server.service.cf.ctx.state; import jakarta.annotation.Nullable; -import org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus; -import org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent; public record GeofencingEvalResult(@Nullable GeofencingTransitionEvent transition, - GeofencingPresenceStatus status) {} + GeofencingPresenceStatus status) { +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java index d6475cc47f..bdedb8940c 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java @@ -20,16 +20,16 @@ import lombok.EqualsAndHashCode; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.geo.Coordinates; import org.thingsboard.common.util.geo.PerimeterDefinition; -import org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus; -import org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.gen.transport.TransportProtos.GeofencingZoneProto; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.INSIDE; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.OUTSIDE; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.INSIDE; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.OUTSIDE; @Data public class GeofencingZoneState { diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java index 4d51e8096b..337d42fff4 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java @@ -18,7 +18,7 @@ package org.thingsboard.server.utils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.CalculatedFieldType; -import org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index 8df1d04f2e..e2359fff0c 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -33,7 +33,6 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; @@ -41,6 +40,7 @@ import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicS import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.id.AssetProfileId; @@ -58,9 +58,9 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; @DaoSqlTest public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTest { diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index a7204f259f..ef481375c7 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -26,12 +26,12 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.DeviceId; @@ -58,9 +58,9 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; @ExtendWith(MockitoExtension.class) public class GeofencingCalculatedFieldStateTest { diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java index 87ef9bf0a1..7b086f1ce8 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java @@ -171,10 +171,11 @@ public class GeofencingValueArgumentEntryTest { } @Test - void testNotParsableToPerimeterJsonKvEntryResultInEmptyArgument() { + void testNotParsableToPerimeterJsonKvEntryResultInExceptionTrowed() { BaseAttributeKvEntry invalidZoneEntry = new BaseAttributeKvEntry(new JsonDataEntry("zone", "\"{}\""), 363L, 155L); - GeofencingArgumentEntry geofencingArgumentEntry = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, invalidZoneEntry)); - assertThat(geofencingArgumentEntry.isEmpty()).isTrue(); + assertThatThrownBy(() -> new GeofencingArgumentEntry(Map.of(ZONE_1_ID, invalidZoneEntry))) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("The given string value cannot be transformed to Json object: \"{}\""); } } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java index 3a6d0fa30d..f11e37921a 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java @@ -25,10 +25,10 @@ import org.thingsboard.server.common.data.kv.JsonDataEntry; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.INSIDE; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus.OUTSIDE; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent.ENTERED; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingTransitionEvent.LEFT; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.INSIDE; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus.OUTSIDE; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent.ENTERED; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent.LEFT; public class GeofencingZoneStateTest { diff --git a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java index ac6d45ad47..bd2111e834 100644 --- a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java @@ -18,7 +18,7 @@ package org.thingsboard.server.utils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; -import org.thingsboard.server.common.data.cf.configuration.GeofencingPresenceStatus; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.DeviceId; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java index fe3eb4e4d8..f83a812529 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/SystemParams.java @@ -38,5 +38,7 @@ public class SystemParams { String calculatedFieldDebugPerTenantLimitsConfiguration; long maxArgumentsPerCF; long maxDataPointsPerRollingArg; + int minAllowedScheduledUpdateIntervalInSecForCF; + int maxRelationLevelPerCfArgument; TrendzSettings trendzSettings; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java index 2fe554b801..972a3e0ee9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CalculatedFieldConfiguration.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java index ac8bfb691d..4e9b4252c9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java @@ -17,7 +17,6 @@ package org.thingsboard.server.common.data.cf.configuration; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; -import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -25,7 +24,6 @@ import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; -import org.thingsboard.server.common.data.util.CollectionsUtil; import java.util.Collections; import java.util.List; @@ -48,9 +46,6 @@ public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynami if (maxLevel < 1) { throw new IllegalArgumentException("Relation query dynamic source configuration max relation level can't be less than 1!"); } - if (maxLevel > 2) { - throw new IllegalArgumentException("Relation query dynamic source configuration max relation level can't be greater than 2!"); - } if (direction == null) { throw new IllegalArgumentException("Relation query dynamic source configuration direction must be specified!"); } @@ -64,6 +59,13 @@ public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynami return maxLevel == 1; } + public void validateMaxRelationLevel(String argumentName, int maxAllowedRelationLevel) { + if (maxLevel > maxAllowedRelationLevel) { + throw new IllegalArgumentException("Max relation level is greater than configured " + + "maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + argumentName); + } + } + public EntityRelationsQuery toEntityRelationsQuery(EntityId rootEntityId) { if (isSimpleRelation()) { throw new IllegalArgumentException("Entity relations query can't be created for a simple relation!"); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java index afc8402722..7818f2f5b2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java @@ -38,11 +38,7 @@ public interface ScheduledUpdateSupportedCalculatedFieldConfiguration extends Ca void setTimeUnit(TimeUnit timeUnit); - @Override - default void validate() { - if (!isScheduledUpdateEnabled()) { - return; - } + default void validate(long minAllowedScheduledUpdateInterval) { var timeUnit = getTimeUnit(); if (timeUnit == null) { throw new IllegalArgumentException("Scheduled update time unit should be specified!"); @@ -51,5 +47,9 @@ public interface ScheduledUpdateSupportedCalculatedFieldConfiguration extends Ca throw new IllegalArgumentException("Unsupported scheduled update time unit: " + timeUnit + ". Allowed: " + SUPPORTED_TIME_UNITS); } + if (timeUnit.toSeconds(getScheduledUpdateInterval()) < minAllowedScheduledUpdateInterval) { + throw new IllegalArgumentException("Scheduled update interval is less than configured " + + "minimum allowed interval in tenant profile: " + minAllowedScheduledUpdateInterval); + } } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java similarity index 87% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java index b009142f37..b797f91c68 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java @@ -13,13 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.cf.configuration; +package org.thingsboard.server.common.data.cf.configuration.geofencing; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.Data; import org.thingsboard.server.common.data.cf.CalculatedFieldType; -import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; -import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Output; +import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.EntityId; import java.util.HashMap; @@ -70,7 +72,6 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal @Override public void validate() { - ScheduledUpdateSupportedCalculatedFieldConfiguration.super.validate(); if (entityCoordinates == null) { throw new IllegalArgumentException("Geofencing calculated field entity coordinates must be specified!"); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingEvent.java similarity index 91% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingEvent.java index ca3b91baec..a6ee0cfcd6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingEvent.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.cf.configuration; +package org.thingsboard.server.common.data.cf.configuration.geofencing; public sealed interface GeofencingEvent permits GeofencingTransitionEvent, GeofencingPresenceStatus { } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingPresenceStatus.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingPresenceStatus.java similarity index 91% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingPresenceStatus.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingPresenceStatus.java index 3e88744132..38977cb650 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingPresenceStatus.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingPresenceStatus.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.cf.configuration; +package org.thingsboard.server.common.data.cf.configuration.geofencing; import lombok.Getter; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingReportStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingReportStrategy.java similarity index 91% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingReportStrategy.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingReportStrategy.java index 774e725650..a7937bb93c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingReportStrategy.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingReportStrategy.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.cf.configuration; +package org.thingsboard.server.common.data.cf.configuration.geofencing; public enum GeofencingReportStrategy { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingTransitionEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingTransitionEvent.java similarity index 90% rename from common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingTransitionEvent.java rename to common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingTransitionEvent.java index edd747587e..d7cf996fa7 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/GeofencingTransitionEvent.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingTransitionEvent.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.cf.configuration; +package org.thingsboard.server.common.data.cf.configuration.geofencing; public enum GeofencingTransitionEvent implements GeofencingEvent { ENTERED, LEFT diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java index 891940bf08..7ac87db10d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.cf.configuration.geofencing; +import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Data; import org.springframework.lang.Nullable; import org.thingsboard.server.common.data.AttributeScope; @@ -22,12 +23,12 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CfArgumentDynamicSourceConfiguration; -import org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntitySearchDirection; @Data +@JsonInclude(JsonInclude.Include.NON_NULL) public class ZoneGroupConfiguration { @Nullable @@ -65,6 +66,9 @@ public class ZoneGroupConfiguration { if (direction == null) { throw new IllegalArgumentException("Relation direction must be specified for '" + name + "' zone group!"); } + if (hasDynamicSource()) { + refDynamicSourceConfiguration.validate(); + } } public boolean hasDynamicSource() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index 7c174b005b..4c8b9e06bd 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -174,6 +174,8 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura private long maxArgumentsPerCF = 10; @Schema(example = "3600") private int minAllowedScheduledUpdateIntervalInSecForCF = 3600; + @Schema(example = "10") + private int maxRelationLevelPerCfArgument = 10; @Builder.Default @Min(value = 1, message = "must be at least 1") @Schema(example = "1000") diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java index 1fb55673d1..86fa52ba66 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java @@ -67,15 +67,34 @@ public class RelationQueryDynamicSourceConfigurationTest { } @Test - void validateShouldThrowWhenMaxLevelGreaterThanTwo() { + void validateShouldThrowWhenMaxLevelGreaterThanMaxAllowedLevelFromTenantProfile() { + int maxAllowedRelationLevel = 2; + int argumentMaxRelationLevel = 3; + var cfg = new RelationQueryDynamicSourceConfiguration(); - cfg.setMaxLevel(3); + cfg.setMaxLevel(argumentMaxRelationLevel); cfg.setDirection(EntitySearchDirection.FROM); cfg.setRelationType(EntityRelation.CONTAINS_TYPE); - assertThatThrownBy(cfg::validate) + String testRelationArgument = "testRelationArgument"; + assertThatThrownBy(() -> cfg.validateMaxRelationLevel(testRelationArgument, maxAllowedRelationLevel)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Relation query dynamic source configuration max relation level can't be greater than 2!"); + .hasMessage("Max relation level is greater than configured " + + "maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + testRelationArgument); + } + + @Test + void validateShouldPassValidationWhenMaxLevelLessThanMaxAllowedLevelFromTenantProfile() { + int maxAllowedRelationLevel = 5; + int argumentMaxRelationLevel = 2; + + var cfg = new RelationQueryDynamicSourceConfiguration(); + cfg.setMaxLevel(argumentMaxRelationLevel); + cfg.setDirection(EntitySearchDirection.FROM); + cfg.setRelationType(EntityRelation.CONTAINS_TYPE); + + String testRelationArgument = "testRelationArgument"; + assertThatCode(() -> cfg.validateMaxRelationLevel(testRelationArgument, maxAllowedRelationLevel)).doesNotThrowAnyException(); } @Test diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java new file mode 100644 index 0000000000..e31ee97a58 --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfigurationTest.java @@ -0,0 +1,78 @@ +/** + * 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.common.data.cf.configuration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; + +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration.SUPPORTED_TIME_UNITS; + +@ExtendWith(MockitoExtension.class) +class ScheduledUpdateSupportedCalculatedFieldConfigurationTest { + + @ParameterizedTest + @EnumSource(TimeUnit.class) + void validateShouldThrowWhenScheduledUpdateIntervalIsSetButTimeUnitIsNotSupported(TimeUnit timeUnit) { + int scheduledUpdateInterval = 60; + int minAllowedInterval = (int) timeUnit.toSeconds(scheduledUpdateInterval - 1); + + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setScheduledUpdateInterval(scheduledUpdateInterval); + cfg.setTimeUnit(timeUnit); + + if (SUPPORTED_TIME_UNITS.contains(timeUnit)) { + assertThatCode(() -> cfg.validate(minAllowedInterval)).doesNotThrowAnyException(); + return; + } + assertThatThrownBy(() -> cfg.validate(minAllowedInterval)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Unsupported scheduled update time unit: " + timeUnit + ". Allowed: " + SUPPORTED_TIME_UNITS); + } + + @Test + void validateShouldThrowWhenScheduledUpdateIntervalIsSetButTimeUnitIsNotSpecified() { + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setScheduledUpdateInterval(60); + cfg.setTimeUnit(null); + + assertThatThrownBy(() -> cfg.validate(0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Scheduled update time unit should be specified!"); + } + + @Test + void validateShouldThrowWhenScheduledUpdateIntervalIsLessThanMinAllowedIntervalInTenantProfile() { + int minAllowedInterval = (int) TimeUnit.HOURS.toSeconds(2); + + var cfg = new GeofencingCalculatedFieldConfiguration(); + cfg.setScheduledUpdateInterval(1); + cfg.setTimeUnit(TimeUnit.HOURS); + + assertThatThrownBy(() -> cfg.validate(minAllowedInterval)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Scheduled update interval is less than configured " + + "minimum allowed interval in tenant profile: " + minAllowedInterval); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java similarity index 77% rename from common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java rename to common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java index 90d2768608..9031474388 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/GeofencingCalculatedFieldConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java @@ -13,17 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.common.data.cf.configuration; +package org.thingsboard.server.common.data.cf.configuration.geofencing; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.cf.CalculatedFieldType; -import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; -import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import java.util.List; import java.util.Map; @@ -36,7 +35,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration.SUPPORTED_TIME_UNITS; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; @@ -126,46 +124,6 @@ public class GeofencingCalculatedFieldConfigurationTest { verify(zoneGroupConfigurationB, never()).validate(); } - @Test - void validateShouldThrowWhenScheduledUpdateIntervalIsSetButTimeUnitIsNotSpecified() { - var cfg = new GeofencingCalculatedFieldConfiguration(); - cfg.setScheduledUpdateInterval(60); - var zg = new ZoneGroupConfiguration("allowedZones", "perimeter", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); - zg.setRefDynamicSourceConfiguration(mock(RelationQueryDynamicSourceConfiguration.class)); - cfg.setZoneGroups(List.of(zg)); - cfg.setTimeUnit(null); - - assertThat(cfg.isScheduledUpdateEnabled()).isTrue(); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Scheduled update time unit should be specified!"); - } - - @ParameterizedTest - @EnumSource(TimeUnit.class) - void validateShouldThrowWhenScheduledUpdateIntervalIsSetButTimeUnitIsNotSupported(TimeUnit timeUnit) { - var cfg = new GeofencingCalculatedFieldConfiguration(); - cfg.setScheduledUpdateInterval(60); - var zg = new ZoneGroupConfiguration("allowedZones", "perimeter", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); - zg.setRefDynamicSourceConfiguration(mock(RelationQueryDynamicSourceConfiguration.class)); - cfg.setZoneGroups(List.of(zg)); - cfg.setEntityCoordinates(mock(EntityCoordinates.class)); - cfg.setTimeUnit(timeUnit); - - assertThat(cfg.isScheduledUpdateEnabled()).isTrue(); - - if (SUPPORTED_TIME_UNITS.contains(timeUnit)) { - assertThatCode(cfg::validate).doesNotThrowAnyException(); - return; - } - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Unsupported scheduled update time unit: " + timeUnit + - ". Allowed: " + SUPPORTED_TIME_UNITS); - } - - @Test void scheduledUpdateDisabledWhenIntervalIsZero() { var cfg = new GeofencingCalculatedFieldConfiguration(); diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java index f3c1ae3263..988995ddbe 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java @@ -31,7 +31,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; public class ZoneGroupConfigurationTest { diff --git a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java index 4e84cdebe1..c0cb886747 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/cf/BaseCalculatedFieldService.java @@ -22,7 +22,6 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.CalculatedFieldLinkId; import org.thingsboard.server.common.data.id.EntityId; @@ -38,7 +37,6 @@ import org.thingsboard.server.dao.service.DataValidator; import java.util.List; import java.util.Optional; -import java.util.concurrent.TimeUnit; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validatePageLink; @@ -80,7 +78,6 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements TenantId tenantId = calculatedField.getTenantId(); log.trace("Executing save calculated field, [{}]", calculatedField); updateDebugSettings(tenantId, calculatedField, System.currentTimeMillis()); - updatedSchedulingConfiguration(calculatedField); CalculatedField savedCalculatedField = calculatedFieldDao.save(tenantId, calculatedField); createOrUpdateCalculatedFieldLink(tenantId, savedCalculatedField); eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(savedCalculatedField.getTenantId()).entityId(savedCalculatedField.getId()) @@ -94,23 +91,6 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements } } - private void updatedSchedulingConfiguration(CalculatedField calculatedField) { - if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration configuration) { - if (!configuration.isScheduledUpdateEnabled()) { - return; - } - TimeUnit timeUnit = configuration.getTimeUnit(); - long intervalInSeconds = timeUnit.toSeconds(configuration.getScheduledUpdateInterval()); - int tenantProfileMinAllowedSecValue = tbTenantProfileCache.get(calculatedField.getTenantId()) - .getDefaultProfileConfiguration() - .getMinAllowedScheduledUpdateIntervalInSecForCF(); - if (intervalInSeconds < tenantProfileMinAllowedSecValue) { - configuration.setScheduledUpdateInterval(tenantProfileMinAllowedSecValue); - configuration.setTimeUnit(TimeUnit.SECONDS); - } - } - } - @Override public CalculatedField findById(TenantId tenantId, CalculatedFieldId calculatedFieldId) { log.trace("Executing findById, tenantId [{}], calculatedFieldId [{}]", tenantId, calculatedFieldId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java index 296aeff13a..05b782c26c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java @@ -18,9 +18,9 @@ package org.thingsboard.server.dao.service.validator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.cf.CalculatedField; -import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.dao.cf.CalculatedFieldDao; @@ -28,6 +28,9 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import java.util.Map; +import java.util.stream.Collectors; + @Component public class CalculatedFieldDataValidator extends DataValidator { @@ -38,33 +41,33 @@ public class CalculatedFieldDataValidator extends DataValidator private ApiLimitService apiLimitService; @Override - protected void validateCreate(TenantId tenantId, CalculatedField calculatedField) { - validateNumberOfCFsPerEntity(tenantId, calculatedField.getEntityId()); + protected void validateDataImpl(TenantId tenantId, CalculatedField calculatedField) { validateNumberOfArgumentsPerCF(tenantId, calculatedField); validateCalculatedFieldConfiguration(calculatedField); + validateSchedulingConfiguration(tenantId, calculatedField); + validateRelationQuerySourceArguments(tenantId, calculatedField); } @Override - protected CalculatedField validateUpdate(TenantId tenantId, CalculatedField calculatedField) { - CalculatedField old = calculatedFieldDao.findById(calculatedField.getTenantId(), calculatedField.getId().getId()); - if (old == null) { - throw new DataValidationException("Can't update non existing calculated field!"); - } - validateNumberOfArgumentsPerCF(tenantId, calculatedField); - validateCalculatedFieldConfiguration(calculatedField); - return old; - } - - private void validateNumberOfCFsPerEntity(TenantId tenantId, EntityId entityId) { + protected void validateCreate(TenantId tenantId, CalculatedField calculatedField) { long maxCFsPerEntity = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxCalculatedFieldsPerEntity); if (maxCFsPerEntity <= 0) { return; } - if (calculatedFieldDao.countCFByEntityId(tenantId, entityId) >= maxCFsPerEntity) { + if (calculatedFieldDao.countCFByEntityId(tenantId, calculatedField.getEntityId()) >= maxCFsPerEntity) { throw new DataValidationException("Calculated fields per entity limit reached!"); } } + @Override + protected CalculatedField validateUpdate(TenantId tenantId, CalculatedField calculatedField) { + CalculatedField old = calculatedFieldDao.findById(calculatedField.getTenantId(), calculatedField.getId().getId()); + if (old == null) { + throw new DataValidationException("Can't update non existing calculated field!"); + } + return old; + } + private void validateNumberOfArgumentsPerCF(TenantId tenantId, CalculatedField calculatedField) { if (!(calculatedField instanceof ArgumentsBasedCalculatedFieldConfiguration argumentsBasedCfg)) { return; @@ -79,8 +82,37 @@ public class CalculatedFieldDataValidator extends DataValidator } private void validateCalculatedFieldConfiguration(CalculatedField calculatedField) { + wrapAsDataValidation(calculatedField.getConfiguration()::validate); + } + + private void validateSchedulingConfiguration(TenantId tenantId, CalculatedField calculatedField) { + if (!(calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledUpdateCfg) + || !scheduledUpdateCfg.isScheduledUpdateEnabled()) { + return; + } + long minAllowedScheduledUpdateInterval = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMinAllowedScheduledUpdateIntervalInSecForCF); + wrapAsDataValidation(() -> scheduledUpdateCfg.validate(minAllowedScheduledUpdateInterval)); + } + + private void validateRelationQuerySourceArguments(TenantId tenantId, CalculatedField calculatedField) { + if (!(calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argumentsBasedCfg)) { + return; + } + Map relationQueryBasedArguments = argumentsBasedCfg.getArguments().entrySet() + .stream() + .filter(entry -> entry.getValue().hasDynamicSource()) + .collect(Collectors.toMap(Map.Entry::getKey, entry -> (RelationQueryDynamicSourceConfiguration) entry.getValue().getRefDynamicSourceConfiguration())); + if (relationQueryBasedArguments.isEmpty()) { + return; + } + int maxRelationLevel = (int) apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxRelationLevelPerCfArgument); + relationQueryBasedArguments.forEach((argumentName, relationQueryDynamicSourceConfiguration) -> + wrapAsDataValidation(() -> relationQueryDynamicSourceConfiguration.validateMaxRelationLevel(argumentName, maxRelationLevel))); + } + + private static void wrapAsDataValidation(Runnable validation) { try { - calculatedField.getConfiguration().validate(); + validation.run(); } catch (IllegalArgumentException e) { throw new DataValidationException(e.getMessage(), e); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index 0d70e22339..1310b6c0b3 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java @@ -28,13 +28,13 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntityRelation; @@ -50,7 +50,7 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; @DaoSqlTest public class CalculatedFieldServiceTest extends AbstractServiceTest { @@ -148,7 +148,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { } @Test - public void testSaveGeofencingCalculatedField_shouldClampScheduledIntervalToTenantMin() { + public void testSaveGeofencingCalculatedField_shouldThrowWhenScheduledIntervalIsLessThanMinAllowedIntervalInTenantProfile() { // Arrange a device Device device = createTestDevice(); @@ -181,22 +181,47 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { cf.setConfigurationVersion(0); cf.setConfiguration(cfg); - CalculatedField saved = calculatedFieldService.save(cf); + assertThatThrownBy(() -> calculatedFieldService.save(cf)) + .isInstanceOf(DataValidationException.class) + .hasCauseInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("Scheduled update interval is less than configured " + + "minimum allowed interval in tenant profile: "); + } - assertThat(saved).isNotNull(); - assertThat(saved.getConfiguration()).isInstanceOf(GeofencingCalculatedFieldConfiguration.class); + @Test + public void testSaveGeofencingCalculatedField_shouldThrowWhenRelationLevelIsGreaterThanMaxAllowedRelationLevelInTenantProfile() { + // Arrange a device + Device device = createTestDevice(); - var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration(); + // Build a valid Geofencing configuration + GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); - // Assert: the interval is clamped up to tenant profile min - int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateInterval(); + // Coordinates: TS_LATEST, no dynamic source + EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); + cfg.setEntityCoordinates(entityCoordinates); - int min = tbTenantProfileCache.get(tenantId) - .getDefaultProfileConfiguration() - .getMinAllowedScheduledUpdateIntervalInSecForCF(); - assertThat(savedInterval).isEqualTo(min); + // Zone-group argument (ATTRIBUTE) — make it DYNAMIC so scheduling is enabled + ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", "allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + var dynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); + dynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); + dynamicSourceConfiguration.setMaxLevel(Integer.MAX_VALUE); + dynamicSourceConfiguration.setRelationType(EntityRelation.CONTAINS_TYPE); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); + cfg.setZoneGroups(List.of(zoneGroupConfiguration)); - calculatedFieldService.deleteCalculatedField(tenantId, saved.getId()); + // Create & save Calculated Field + CalculatedField cf = new CalculatedField(); + cf.setTenantId(tenantId); + cf.setEntityId(device.getId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("GF clamp test"); + cf.setConfigurationVersion(0); + cf.setConfiguration(cfg); + + assertThatThrownBy(() -> calculatedFieldService.save(cf)) + .isInstanceOf(DataValidationException.class) + .hasCauseInstanceOf(IllegalArgumentException.class) + .hasMessageStartingWith("Max relation level is greater than configured maximum allowed relation level in tenant profile"); } @Test diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java index bf6ac7cf20..1693ee2763 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java @@ -30,7 +30,6 @@ import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; -import org.thingsboard.server.common.data.cf.configuration.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; @@ -38,6 +37,7 @@ import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicS import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.device.data.DefaultDeviceConfiguration; @@ -61,7 +61,7 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; import static org.thingsboard.server.common.data.AttributeScope.SERVER_SCOPE; -import static org.thingsboard.server.common.data.cf.configuration.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultAssetProfile; import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultDeviceProfile; import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultTenantAdmin; From 7567ac25cf3e7439f62a7ecd91cbeb35be0f9d67 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Thu, 4 Sep 2025 12:09:39 +0300 Subject: [PATCH 102/216] moved geofencing state classes to inner package geofencing --- .../server/service/cf/ctx/state/ArgumentEntry.java | 1 + .../server/service/cf/ctx/state/CalculatedFieldState.java | 2 ++ .../state/{ => geofencing}/GeofencingArgumentEntry.java | 4 +++- .../{ => geofencing}/GeofencingCalculatedFieldState.java | 8 ++++++-- .../ctx/state/{ => geofencing}/GeofencingEvalResult.java | 2 +- .../ctx/state/{ => geofencing}/GeofencingZoneState.java | 2 +- .../server/utils/CalculatedFieldArgumentUtils.java | 2 +- .../thingsboard/server/utils/CalculatedFieldUtils.java | 6 +++--- .../cf/ctx/state/GeofencingCalculatedFieldStateTest.java | 2 ++ .../cf/ctx/state/GeofencingValueArgumentEntryTest.java | 2 ++ .../service/cf/ctx/state/GeofencingZoneStateTest.java | 2 ++ .../server/utils/CalculatedFieldUtilsTest.java | 6 +++--- 12 files changed, 27 insertions(+), 12 deletions(-) rename application/src/main/java/org/thingsboard/server/service/cf/ctx/state/{ => geofencing}/GeofencingArgumentEntry.java (93%) rename application/src/main/java/org/thingsboard/server/service/cf/ctx/state/{ => geofencing}/GeofencingCalculatedFieldState.java (95%) rename application/src/main/java/org/thingsboard/server/service/cf/ctx/state/{ => geofencing}/GeofencingEvalResult.java (93%) rename application/src/main/java/org/thingsboard/server/service/cf/ctx/state/{ => geofencing}/GeofencingZoneState.java (98%) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java index c7f830431b..2d43883131 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/ArgumentEntry.java @@ -22,6 +22,7 @@ import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.KvEntry; import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; import java.util.List; import java.util.Map; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java index e58ca699e2..5f8e7538c4 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java @@ -23,6 +23,8 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.service.cf.CalculatedFieldResult; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; import java.util.List; import java.util.Map; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java similarity index 93% rename from application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java rename to application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java index 3794764351..7f610aaf48 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.cf.ctx.state; +package org.thingsboard.server.service.cf.ctx.state.geofencing; import lombok.Data; import lombok.extern.slf4j.Slf4j; @@ -21,6 +21,8 @@ import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfTsGeofencingArg; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; import java.util.Map; import java.util.stream.Collectors; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java similarity index 95% rename from application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java rename to application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java index ad53b44d62..5425fbe41f 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.cf.ctx.state; +package org.thingsboard.server.service.cf.ctx.state.geofencing; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; @@ -24,7 +24,6 @@ import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.geo.Coordinates; -import org.thingsboard.server.actors.calculatedField.CalculatedFieldException; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy; @@ -33,6 +32,11 @@ import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupC import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; +import org.thingsboard.server.service.cf.ctx.state.BaseCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import java.util.ArrayList; import java.util.HashMap; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingEvalResult.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingEvalResult.java similarity index 93% rename from application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingEvalResult.java rename to application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingEvalResult.java index d7794466a8..c6bf3dd65e 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingEvalResult.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingEvalResult.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.cf.ctx.state; +package org.thingsboard.server.service.cf.ctx.state.geofencing; import jakarta.annotation.Nullable; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingPresenceStatus; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java similarity index 98% rename from application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java rename to application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java index bdedb8940c..348c1ba9f1 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.thingsboard.server.service.cf.ctx.state; +package org.thingsboard.server.service.cf.ctx.state.geofencing; import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java index 008fc17acd..055c97efc3 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java @@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.kv.StringDataEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; -import org.thingsboard.server.service.cf.ctx.state.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java index 337d42fff4..4e93c8233e 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldUtils.java @@ -38,9 +38,9 @@ import org.thingsboard.server.gen.transport.TransportProtos.TsValueProto; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; -import org.thingsboard.server.service.cf.ctx.state.GeofencingArgumentEntry; -import org.thingsboard.server.service.cf.ctx.state.GeofencingCalculatedFieldState; -import org.thingsboard.server.service.cf.ctx.state.GeofencingZoneState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState; import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index ef481375c7..596f9f7b33 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -44,6 +44,8 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.service.cf.CalculatedFieldResult; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; import java.util.HashMap; import java.util.List; diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java index 7b086f1ce8..b3487f0e83 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java @@ -24,6 +24,8 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState; import java.util.Map; import java.util.UUID; diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java index f11e37921a..f6c6778ced 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingZoneStateTest.java @@ -21,6 +21,8 @@ import org.thingsboard.common.util.geo.Coordinates; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingEvalResult; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState; import java.util.UUID; diff --git a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java index bd2111e834..2697b2b804 100644 --- a/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java +++ b/application/src/test/java/org/thingsboard/server/utils/CalculatedFieldUtilsTest.java @@ -31,9 +31,9 @@ import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; -import org.thingsboard.server.service.cf.ctx.state.GeofencingArgumentEntry; -import org.thingsboard.server.service.cf.ctx.state.GeofencingCalculatedFieldState; -import org.thingsboard.server.service.cf.ctx.state.GeofencingZoneState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingZoneState; import java.util.LinkedHashMap; import java.util.List; From 88170fb83b83987a81a802c9f0b539a837d2b621 Mon Sep 17 00:00:00 2001 From: Ruslan Vasylkiv Date: Thu, 4 Sep 2025 18:41:21 +0300 Subject: [PATCH 103/216] Update README.md --- README.md | 171 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 137 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index bffd87ffa5..27992d57bd 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,146 @@ -# ThingsBoard -[![ThingsBoard Builds Server Status](https://img.shields.io/teamcity/build/e/ThingsBoard_Build?label=TB%20builds%20server&server=https%3A%2F%2Fbuilds.thingsboard.io&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAALzAAAC8wHS6QoqAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAB9FJREFUeJzVm3+MXUUVx7+zWwqEtnRLWisQ2lKVUisIQmsqYCohpUhpEGsFKSJJTS0qGiGIISJ/8CNGYzSaEKBQEZUiP7RgVbCVdpE0xYKBWgI2rFLZJZQWtFKobPfjH3Pfdu7s3Pvmzntv3/JNNr3bOXPO+Z6ZO3PumVmjFgEYJWmWpDmSZks6VtIESV3Zv29LWmGMubdVPgw7gEOBJcAaYC/18fd2+zyqngAwXdL7M9keSduMMXgyH5R0laRPSRpbwf62CrLDB8AAS4HnAqP2EvA1YBTwPuBnwP46I70H+DPwALAS+B5wBTCu3VyHIJvG98dMX+B/BW1vAvcAnwdmAp3t5hWFbORXR5AvwmPARcCYdnNJAnCBR+gd7HQ9HZgLfAt4PUB8AzCv3f43DGCTQ6o/RAo43gtCL2Da4W9TAUwEBhxiPymRvcabAR8eTl+biQ7neYokdyTXlvR7xPt9etM8GmZ0FDxL+WD42FdBdkTDJd0jyU1wzi7pd473e0+qA8AM4AbgkrK1BDgOWAc8ChyTaq+eM5ud93ofcHpAZiY2sanhZaDDaTfAZ7HJUmlWCJzm6bqLQM6QBanXkfthcxgPNbTEW9z2AT8AzgTmANdikxwXX/d0XOi0bQEmFNj6GPAfhuKnXkB98kNsNjsITwacKkI3MNrrf4UnswXoiiRfwyqgo4D8L2hVZglMw456DDYCRwR0jCH/KuWCgE2oysjX8KsA+V+2jHzm3CrP4PMBx/4JfAU4qETP+EAQ/gKcA/w7gnwNbl5yD7bG0DLyM7DZXw3d2f9PA+YD5wIzK+gLBSEFA/XIA2cAVwLvbSQAt3mGP5Gs7IDO8dg1ZYDGcAfOwujZuIwDn+ObUx09hHx+v7Eh5nndCyIIDgBbgd0lMiv9IABfIF+LeDnVyU97xj5XR/6bwI5sZEaXyH2UuHd+WSbfRXktYjAIAfL9wGdSA/Cgo+gtSio12IKJa3hNKAgZ+TciyL+AlwECKzI/ioLgTvsa+YtTyXeSz8ZW15E3wN88p3JBwCZNMeShIKkBTsRmmSG4a0o/sDSJfGboBE/5pRF9pgI9oSBUJP8mXpLk2bm6pO9Aw+QzI8s8xVFbXRaEf3h911cgD7Cyjg0/L/GxnoLdoUoA3O1vDxUyLWyO4AehCpYX6D2L/LpUhtsaCkIWxRoeT+g/DVsqT8EWYDowC5jh6FxUUc+tJJblOmSPqWp4JUFHl6TDUoxLOlnSdknPSnK3sA2S9lfQs0zS7SkzwQ/A61U6A6dKWufpSMVg5mmMeUPSXyv2v0zSN6oa7ZAdwRqiA5CRf0TS+KpGAxiQ1OFN4z8l6PErVXUxSvmp1hvTqUnk35adPWskPWSM6fPaq84ASXqscg/gi9gcvJuC6o0nfwrhw5EYvIpNn88HStcN4M6KulfTys/lzKlO0lb8P2Lrf6VbLDAF+DLweEX998aSx372bwP6gPlVA3BEAvm9FJwVYtPqjwDXA08n6AZbOYoeeeAWp++mSlPGGLMLeFjSuRW6Iektx4GDJc2TdJ6khZKOruKDh/skXWSM6a/Q5yjn+dDKFrE1vw0VR2m2039x4kj7uJ+SslyJ/+7rtaly4mCM+a+kBaq2TbnVpfWy216jmCzpkIR+7kK/MymHNsbslX0NYoMweMpsjNklaWuKXQ9zJf2eOocvAbzHee5N/ojIgvBVxY3madh3v4b1iWZ/o3zw5kpaS+SFDGCq8jPguUQ/CmsCZfi403dhwjv/AHAQMAl41mvbGBMEhq4/c1PJTwmQr1f7u97pfzj5EnwUead/KAg/ivD7Zkf+HSBpFwiRfwibI3SXkOj29PgEivAggdU+C8JWR+6+CN9dm1tSyHcBLwbIj87ax1Kcxe0DJmVyY4CdEeR/TXnVeRLwc+C3wHF1fP+Qp/uGlABc6Cl5mPziVi8IzwDfAZ6KIN9LyhQt9v1GT/+sFCXTOVBBXuOTd+TGkp+eqWjKSTBwMPAvR+9TjSibjK35l93mWIxdZFKOxPzFseEgAJd7Olt6v+AC8jdIqwRhLbZM758HRH3tYa/vnoqtKZ4JHIk99tvh6HqNVl3RLSB/JfBEBPnBwxXsJ2uf176qxO7hwE3ALq/PfuyVXhdXt4r8+QHyK7K2cXWCMLiTOPqODwTh2IDdD2CP12LwCnUKMankO8kfiAySd2SKgjCEfEEQ+nznsZc7eyLJA9zddPKZIx0c2NcHgMsL5MZhr83XULiTeCSXAEcG2m4PjPCXsEWWBdhbZ/4h6knN4u07Mxv4MbCojtxo7DW6RTRwopMFxt0xeoCJAblLvCDdlWpzRAG42CO2sET2UUfuVbetsYPF9mKq8zwg6Q8lsm7bRJxt8N0cAPdar5FUupYU9X03B2C782wknVUi+0nneacxZk9rXBpGABO8RXA72demJ7fcWyvubIe/TQN2y11MuJ6wA5v3z8HeMbjba+8n5StwJCDb9lYUEI/Fde3mEQ1svnBKRvp32K/LEPYQd1z3XQJfsG3/Sw/gKElLZev8tb8rnizpBEmF1SDZ06ZbJN0saa+kayQtV77qi6QnJF1njFnXdOebAcIXssvQB3yfcGrcCZwEnAfMC8mMKGArNUVT28VubF4/nyZflx8Jr8BVkr4tm83tzn5ek/S8pM2SnpT0gv8H283C/wGTFfhGtexQwQAAAABJRU5ErkJggg==&labelColor=305680)](https://builds.thingsboard.io/viewType.html?buildTypeId=ThingsBoard_Build&guest=1) +![banner](https://github.com/user-attachments/assets/3584b592-33dd-4fb4-91d4-47b62b34806c) + +
+ +# Open-source IoT platform for data collection, processing, visualization, and device management. + +
+
+
+ +💡 [Get started](https://thingsboard.io/docs/getting-started-guides/helloworld/) • 🌐 [Website](https://thingsboard.io/) • 📚 [Documentation](https://thingsboard.io/docs/) • 📔 [Blog](https://thingsboard.io/blog/) • ▶️ [Live demo](https://demo.thingsboard.io/signup) • 🔗 [LinkedIn](https://www.linkedin.com/company/thingsboard/posts/?feedView=all) + +
+ +## 🚀 Installation options + +* Install ThingsBoard [On-premise](https://thingsboard.io/docs/user-guide/install/installation-options/?ceInstallType=onPremise) +* Try [ThingsBoard Cloud](https://thingsboard.io/installations/) +* or [Use our Live demo](https://demo.thingsboard.io/signup) + +## 💡 Getting started with ThingsBoard + +Check out our [Getting Started guide](https://thingsboard.io/docs/getting-started-guides/helloworld/) or [watch the video](https://www.youtube.com/watch?v=80L0ubQLXsc) to learn the basics of ThingsBoard and create your first dashboard! You will learn to: + +* Connect devices to ThingsBoard +* Push data from devices to ThingsBoard +* Build real-time dashboards +* Create a Customer and assign the dashboard with them. +* Define thresholds and trigger alarms +* Set up notifications via email, SMS, mobile apps, or integrate with third-party services. + +## ✨ Features + + + + + + + + + + +
+
+
+ Provision and manage devices and assets +

Provision and manage
devices and assets

+
+
+

Provision, monitor and control your IoT entities in secure way using rich server-side APIs. Define relations between your devices, assets, customers or any other entities.

+
+ +
+
+
+
+ Collect and visualize your data +

Collect and visualize
your data

+
+
+

Collect and store telemetry data in scalable and fault-tolerant way. Visualize your data with built-in or custom widgets and flexible dashboards. Share dashboards with your customers.

+
+ +
+
+
+
+ SCADA Dashboards +

SCADA Dashboards

+
+
+

Monitor and control your industrial processes in real time with SCADA. Use SCADA symbols on dashboards to create and manage any workflow, offering full flexibility to design and oversee operations according to your requirements.

+
+ +
+
+
+
+ Process and React +

Process and React

+
+
+

Define data processing rule chains. Transform and normalize your device data. Raise alarms on incoming telemetry events, attribute updates, device inactivity and user actions.

+
+
+ +
+
+ +## ⚙️ Powerful IoT Rule Engine + +ThingsBoard allows you to create complex [Rule Chains](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/) to process data from your devices and match your application specific use cases. + +[![IoT Rule Engine](https://github.com/user-attachments/assets/ccc048a8-5aa3-44dc-abd4-c20d1d833102 "IoT Rule Engine")](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/) + +
+ +[**Read more about Rule Engine 🡪**](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/) + +
+ +## 📦 Real-Time IoT Dashboards + +ThingsBoard is a scalable, user-friendly, and device-agnostic IoT platform that speeds up time-to-market with powerful built-in solution templates. It enables data collection and analysis from any devices, saving resources on routine tasks and letting you focus on your solution’s unique aspects. See more our Use Cases [here](https://thingsboard.io/iot-use-cases/). + +[**Smart energy**](https://thingsboard.io/use-cases/smart-energy/) + +[![Smart energy](https://github.com/user-attachments/assets/7952d0f1-2ba4-4989-bfc9-75b40de6ea3f "Smart energy")](https://thingsboard.io/use-cases/smart-energy/) + +[**SCADA swimming pool**](https://thingsboard.io/use-cases/scada/) + +[![SCADA Swimming pool](https://github.com/user-attachments/assets/b357c129-ea72-4b64-9dfe-ac25011603b6 "SCADA Swimming pool")](https://thingsboard.io/use-cases/scada/) + +[**Fleet tracking**](https://thingsboard.io/use-cases/fleet-tracking/) + +[![Fleet tracking](https://github.com/user-attachments/assets/80b63841-40c9-4db9-bec2-6a400dc6e58d "Fleet tracking")](https://thingsboard.io/use-cases/fleet-tracking/) + +[**Smart farming**](https://thingsboard.io/use-cases/smart-farming/) + +[![Smart farming](https://github.com/user-attachments/assets/8fe84ad6-6ea4-4cb1-bc31-6cd5c20c357b "Smart farming")](https://thingsboard.io/use-cases/smart-farming/) -ThingsBoard is an open-source IoT platform for data collection, processing, visualization, and device management. - - - - -## Documentation - -ThingsBoard documentation is hosted on [thingsboard.io](https://thingsboard.io/docs). - -## IoT use cases - -[**Smart energy**](https://thingsboard.io/smart-energy/) -[![Smart energy](https://user-images.githubusercontent.com/8308069/152984256-eb48564a-645c-468d-912b-f554b63104a5.gif "Smart energy")](https://thingsboard.io/smart-energy/) - -[**SCADA Swimming pool**](https://thingsboard.io/use-cases/scada/) -[![SCADA Swimming pool](https://github.com/user-attachments/assets/0878a2f5-d358-47c5-b295-03b4533685cf "SCADA Swimming pool")](https://thingsboard.io/use-cases/scada/) - -[**Fleet tracking**](https://thingsboard.io/fleet-tracking/) -[![Fleet tracking](https://user-images.githubusercontent.com/8308069/152984528-0054ed55-8b8b-4cda-ba45-02fe95a81222.gif "Fleet tracking")](https://thingsboard.io/fleet-tracking/) - -[**Smart farming**](https://thingsboard.io/smart-farming/) -[![Smart farming](https://user-images.githubusercontent.com/8308069/152984443-a98b7d3d-ff7a-4037-9011-e71e1e6f755f.gif "Smart farming")](https://thingsboard.io/smart-farming/) +[**Smart metering**](https://thingsboard.io/smart-metering/) -[**IoT Rule Engine**](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/) -[![IoT Rule Engine](https://img.thingsboard.io/demo/send-email-rule-chain.gif "IoT Rule Engine")](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/) +[![Smart metering](https://github.com/user-attachments/assets/564e5ed0-afad-452c-a16c-6270b468ebdc "Smart metering")](https://thingsboard.io/smart-metering/) -[**Smart metering**](https://thingsboard.io/smart-metering/) -[![Smart metering](https://user-images.githubusercontent.com/8308069/31455788-6888a948-aec1-11e7-9819-410e0ba785e0.gif "Smart metering")](https://thingsboard.io/smart-metering/) +
-## Getting Started +[**Check more of our use cases 🡪**](https://thingsboard.io/iot-use-cases/) -Collect and Visualize your IoT data in minutes by following this [guide](https://thingsboard.io/docs/getting-started-guides/helloworld/). +
-## Support +## 🫶 Support - - [Stackoverflow](http://stackoverflow.com/questions/tagged/thingsboard) +To get support, please visit our [GitHub issues page](https://github.com/thingsboard/thingsboard/issues) -## Licenses +## 📄 Licenses -This project is released under [Apache 2.0 License](./LICENSE). +This project is released under [Apache 2.0 License](./LICENSE) From a3c28e9db4527f0374ad170e19b718e144d686b4 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Fri, 5 Sep 2025 12:51:31 +0300 Subject: [PATCH 104/216] Feature custom icon in Custom icon init implementation. --- .../modules/home/menu/menu-link.component.ts | 18 ++- .../material-icon-select.component.html | 6 +- .../material-icon-select.component.ts | 18 ++- .../components/material-icons.component.html | 134 ++++++++++-------- .../components/material-icons.component.ts | 12 +- .../assets/locale/locale.constant-en_US.json | 1 + 6 files changed, 127 insertions(+), 62 deletions(-) diff --git a/ui-ngx/src/app/modules/home/menu/menu-link.component.ts b/ui-ngx/src/app/modules/home/menu/menu-link.component.ts index da8a583183..da7a21632b 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-link.component.ts +++ b/ui-ngx/src/app/modules/home/menu/menu-link.component.ts @@ -14,8 +14,9 @@ /// limitations under the License. /// -import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core'; import { MenuSection } from '@core/services/menu.models'; +import { tbImageIcon } from '@shared/models/custom-menu.models'; @Component({ selector: 'tb-menu-link', @@ -23,14 +24,27 @@ import { MenuSection } from '@core/services/menu.models'; styleUrls: ['./menu-link.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class MenuLinkComponent implements OnInit { +export class MenuLinkComponent implements OnInit, OnChanges { @Input() section: MenuSection; + isCustomIcon: boolean; + constructor() { } ngOnInit() { } + ngOnChanges(changes: SimpleChanges): void { + for (const propName of Object.keys(changes)) { + const change = changes[propName]; + if (change.currentValue !== change.previousValue) { + if (propName === 'section' && change.currentValue) { + this.isCustomIcon = tbImageIcon(change.currentValue.icon); + } + } + } + } + } diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.html b/ui-ngx/src/app/shared/components/material-icon-select.component.html index 25aa51d30d..13bf46d1e0 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.html +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.html @@ -37,6 +37,10 @@ [disabled]="disabled" #matButton (click)="openIconPopup($event, matButton)"> - {{materialIconFormGroup.get('icon').value}} + @if (!isCustomIcon) { + {{materialIconFormGroup.get('icon').value}} + } @else { + icon + } diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.ts b/ui-ngx/src/app/shared/components/material-icon-select.component.ts index 9432b3c96e..97c5519ed3 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.ts +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.ts @@ -36,6 +36,7 @@ import { TbPopoverService } from '@shared/components/popover.service'; import { MaterialIconsComponent } from '@shared/components/material-icons.component'; import { MatButton } from '@angular/material/button'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { tbImageIcon } from '@shared/models/custom-menu.models'; @Component({ selector: 'tb-material-icon-select', @@ -71,6 +72,12 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit @coerceBoolean() iconClearButton = false; + @Input() + @coerceBoolean() + allowedCustomIcon = false; + + isCustomIcon = false; + private requiredValue: boolean; get required(): boolean { return this.requiredValue; @@ -131,11 +138,13 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit this.materialIconFormGroup.patchValue( { icon: this.modelValue }, {emitEvent: false} ); + this.defineIconType(value); } private updateModel() { const icon: string = this.materialIconFormGroup.get('icon').value; if (this.modelValue !== icon) { + this.defineIconType(icon); this.modelValue = icon; this.propagateChange(this.modelValue); } @@ -169,7 +178,8 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit this.viewContainerRef, MaterialIconsComponent, 'left', true, null, { selectedIcon: this.materialIconFormGroup.get('icon').value, - iconClearButton: this.iconClearButton + iconClearButton: this.iconClearButton, + allowedCustomIcon: this.allowedCustomIcon, }, {}, {}, {}, true); @@ -188,4 +198,10 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit this.materialIconFormGroup.get('icon').patchValue(null, {emitEvent: true}); this.cd.markForCheck(); } + + private defineIconType(icon: string) { + if (this.allowedCustomIcon) { + this.isCustomIcon = tbImageIcon(icon); + } + } } diff --git a/ui-ngx/src/app/shared/components/material-icons.component.html b/ui-ngx/src/app/shared/components/material-icons.component.html index d4d9fe93a6..dcd285a99d 100644 --- a/ui-ngx/src/app/shared/components/material-icons.component.html +++ b/ui-ngx/src/app/shared/components/material-icons.component.html @@ -17,62 +17,84 @@ -->
icon.icons
- - search - - - - -
- - - - + @if (allowedCustomIcon) { +
+ + {{ 'resource.system' | translate }} + {{ 'icon.custom' | translate }} +
- - -
-
-
{{ 'icon.no-icons-found' | translate:{iconSearch: searchIconControl.value} }}
+ } + @if (!isCustomIcon) { + + search + + + + +
+ + + + +
+
+ +
+
+
{{ 'icon.no-icons-found' | translate:{iconSearch: searchIconControl.value} }}
+
+
+
+ + +
- -
- - - -
+ } @else { + + +
+ + +
+ }
diff --git a/ui-ngx/src/app/shared/components/material-icons.component.ts b/ui-ngx/src/app/shared/components/material-icons.component.ts index ece1a99108..6fe4c995e5 100644 --- a/ui-ngx/src/app/shared/components/material-icons.component.ts +++ b/ui-ngx/src/app/shared/components/material-icons.component.ts @@ -37,6 +37,7 @@ import { TbPopoverComponent } from '@shared/components/popover.component'; import { BreakpointObserver } from '@angular/cdk/layout'; import { MediaBreakpoints } from '@shared/models/constants'; import { coerceBoolean } from '@shared/decorators/coercion'; +import { tbImageIcon } from '@shared/models/custom-menu.models'; @Component({ selector: 'tb-material-icons', @@ -61,6 +62,10 @@ export class MaterialIconsComponent extends PageComponent implements OnInit { @coerceBoolean() showTitle = true; + @Input() + @coerceBoolean() + allowedCustomIcon = false; + @Input() popover: TbPopoverComponent; @@ -71,6 +76,8 @@ export class MaterialIconsComponent extends PageComponent implements OnInit { showAllSubject = new BehaviorSubject(false); searchIconControl: UntypedFormControl; + isCustomIcon = false; + iconsRowHeight = 48; iconsPanelHeight: string; @@ -122,14 +129,15 @@ export class MaterialIconsComponent extends PageComponent implements OnInit { map((data) => data.iconRows), share() ); + this.isCustomIcon = tbImageIcon(this.selectedIcon) } clearSearch() { this.searchIconControl.patchValue('', {emitEvent: true}); } - selectIcon(icon: MaterialIcon) { - this.iconSelected.emit(icon.name); + selectIcon(icon: string) { + this.iconSelected.emit(icon); } clearIcon() { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index fd368e2853..bb17471cbc 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -9507,6 +9507,7 @@ "icon": { "icon": "Icon", "icons": "Icons", + "custom": "Custom", "select-icon": "Select icon", "material-icons": "Material icons", "show-all": "Show all icons", From 20e44ab004f728282ef68bcfe0f67570b3a6232c Mon Sep 17 00:00:00 2001 From: deaflynx Date: Fri, 8 Aug 2025 16:52:48 +0300 Subject: [PATCH 105/216] Update tb-icon to accept custom image. --- .../modules/home/menu/menu-link.component.ts | 18 +------- .../app/shared/components/icon.component.ts | 45 ++++++++++++++++++- .../material-icon-select.component.ts | 4 +- .../components/material-icons.component.html | 11 ++--- .../components/material-icons.component.ts | 4 +- .../src/app/shared/models/resource.models.ts | 1 + ui-ngx/src/styles.scss | 2 +- 7 files changed, 58 insertions(+), 27 deletions(-) diff --git a/ui-ngx/src/app/modules/home/menu/menu-link.component.ts b/ui-ngx/src/app/modules/home/menu/menu-link.component.ts index da7a21632b..da8a583183 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-link.component.ts +++ b/ui-ngx/src/app/modules/home/menu/menu-link.component.ts @@ -14,9 +14,8 @@ /// limitations under the License. /// -import { ChangeDetectionStrategy, Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { MenuSection } from '@core/services/menu.models'; -import { tbImageIcon } from '@shared/models/custom-menu.models'; @Component({ selector: 'tb-menu-link', @@ -24,27 +23,14 @@ import { tbImageIcon } from '@shared/models/custom-menu.models'; styleUrls: ['./menu-link.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class MenuLinkComponent implements OnInit, OnChanges { +export class MenuLinkComponent implements OnInit { @Input() section: MenuSection; - isCustomIcon: boolean; - constructor() { } ngOnInit() { } - ngOnChanges(changes: SimpleChanges): void { - for (const propName of Object.keys(changes)) { - const change = changes[propName]; - if (change.currentValue !== change.previousValue) { - if (propName === 'section' && change.currentValue) { - this.isCustomIcon = tbImageIcon(change.currentValue.icon); - } - } - } - } - } diff --git a/ui-ngx/src/app/shared/components/icon.component.ts b/ui-ngx/src/app/shared/components/icon.component.ts index 2b6170b71c..36c4e2e0e8 100644 --- a/ui-ngx/src/app/shared/components/icon.component.ts +++ b/ui-ngx/src/app/shared/components/icon.component.ts @@ -33,6 +33,8 @@ import { Subscription } from 'rxjs'; import { take } from 'rxjs/operators'; import { isSvgIcon, splitIconName } from '@shared/models/icon.models'; import { ContentObserver } from '@angular/cdk/observers'; +import { isTbImage } from '@shared/models/resource.models'; +import { ImagePipe } from '@shared/pipe/image.pipe'; const _TbIconBase = mixinColor( class { @@ -70,7 +72,7 @@ const funcIriPattern = /^url\(['"]?#(.*?)['"]?\)$/; host: { role: 'img', class: 'mat-icon notranslate', - '[attr.data-mat-icon-type]': '!_useSvgIcon ? "font" : "svg"', + '[attr.data-mat-icon-type]': '_useSvgIcon ? "svg" : (_useImageIcon ? null : "font")', '[attr.data-mat-icon-name]': '_svgName', '[attr.data-mat-icon-namespace]': '_svgNamespace', '[class.mat-icon-no-color]': 'color !== "primary" && color !== "accent" && color !== "warn"', @@ -99,6 +101,9 @@ export class TbIconComponent extends _TbIconBase private _textElement = null; + _useImageIcon = false; + private _imageElement = null; + private _previousPath?: string; private _elementsWithExternalReferences?: Map; @@ -109,6 +114,7 @@ export class TbIconComponent extends _TbIconBase private contentObserver: ContentObserver, private renderer: Renderer2, private _iconRegistry: MatIconRegistry, + private imagePipe: ImagePipe, @Inject(MAT_ICON_LOCATION) private _location: MatIconLocation, private readonly _errorHandler: ErrorHandler) { super(elementRef); @@ -148,16 +154,29 @@ export class TbIconComponent extends _TbIconBase private _updateIcon() { const useSvgIcon = isSvgIcon(this.icon); + const useImageIcon = isTbImage(this.icon); if (this._useSvgIcon !== useSvgIcon) { this._useSvgIcon = useSvgIcon; if (!this._useSvgIcon) { this._updateSvgIcon(undefined); } else { this._updateFontIcon(undefined); + this._updateImageIcon(undefined); + } + } + if (this._useImageIcon !== useImageIcon) { + this._useImageIcon = useImageIcon; + if (!this._useImageIcon) { + this._updateImageIcon(undefined); + } else { + this._updateFontIcon(undefined); + this._updateSvgIcon(undefined); } } if (this._useSvgIcon) { this._updateSvgIcon(this.icon); + } else if (this._useImageIcon) { + this._updateImageIcon(this.icon); } else { this._updateFontIcon(this.icon); } @@ -278,4 +297,28 @@ export class TbIconComponent extends _TbIconBase } } + private _updateImageIcon(rawName: string | undefined) { + if (rawName) { + this._clearImageIcon(); + this.imagePipe.transform(rawName, { asString: true, ignoreLoadingImage: true }).subscribe( + imageUrl => { + const imgElement = this.renderer.createElement('img'); + this.renderer.setAttribute(imgElement, 'src', imageUrl as string); + const elem: HTMLElement = this._elementRef.nativeElement; + this.renderer.insertBefore(elem, imgElement, this._iconNameContent.nativeElement); + this._imageElement = imgElement; + } + ); + } else { + this._clearImageIcon(); + } + } + + private _clearImageIcon() { + const elem: HTMLElement = this._elementRef.nativeElement; + if (this._imageElement !== null) { + this.renderer.removeChild(elem, this._imageElement); + this._imageElement = null; + } + } } diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.ts b/ui-ngx/src/app/shared/components/material-icon-select.component.ts index 97c5519ed3..c2e904d8d0 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.ts +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.ts @@ -36,7 +36,7 @@ import { TbPopoverService } from '@shared/components/popover.service'; import { MaterialIconsComponent } from '@shared/components/material-icons.component'; import { MatButton } from '@angular/material/button'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { tbImageIcon } from '@shared/models/custom-menu.models'; +import { isTbImage } from '@shared/models/resource.models'; @Component({ selector: 'tb-material-icon-select', @@ -201,7 +201,7 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit private defineIconType(icon: string) { if (this.allowedCustomIcon) { - this.isCustomIcon = tbImageIcon(icon); + this.isCustomIcon = isTbImage(icon); } } } diff --git a/ui-ngx/src/app/shared/components/material-icons.component.html b/ui-ngx/src/app/shared/components/material-icons.component.html index dcd285a99d..558ffd5e0a 100644 --- a/ui-ngx/src/app/shared/components/material-icons.component.html +++ b/ui-ngx/src/app/shared/components/material-icons.component.html @@ -16,15 +16,16 @@ -->
-
icon.icons
- @if (allowedCustomIcon) { -
+
+ icon.icons + @if (allowedCustomIcon) { {{ 'resource.system' | translate }} {{ 'icon.custom' | translate }} -
- } + + } +
@if (!isCustomIcon) { search diff --git a/ui-ngx/src/app/shared/components/material-icons.component.ts b/ui-ngx/src/app/shared/components/material-icons.component.ts index 6fe4c995e5..d144fd45e6 100644 --- a/ui-ngx/src/app/shared/components/material-icons.component.ts +++ b/ui-ngx/src/app/shared/components/material-icons.component.ts @@ -37,7 +37,7 @@ import { TbPopoverComponent } from '@shared/components/popover.component'; import { BreakpointObserver } from '@angular/cdk/layout'; import { MediaBreakpoints } from '@shared/models/constants'; import { coerceBoolean } from '@shared/decorators/coercion'; -import { tbImageIcon } from '@shared/models/custom-menu.models'; +import { isTbImage } from '@shared/models/resource.models'; @Component({ selector: 'tb-material-icons', @@ -129,7 +129,7 @@ export class MaterialIconsComponent extends PageComponent implements OnInit { map((data) => data.iconRows), share() ); - this.isCustomIcon = tbImageIcon(this.selectedIcon) + this.isCustomIcon = isTbImage(this.selectedIcon) } clearSearch() { diff --git a/ui-ngx/src/app/shared/models/resource.models.ts b/ui-ngx/src/app/shared/models/resource.models.ts index 0419e40a7c..c1e692e04f 100644 --- a/ui-ngx/src/app/shared/models/resource.models.ts +++ b/ui-ngx/src/app/shared/models/resource.models.ts @@ -188,6 +188,7 @@ export const isImageResourceUrl = (url: string): boolean => url && IMAGES_URL_RE export const isJSResourceUrl = (url: string): boolean => url && RESOURCES_URL_REGEXP.test(url); export const isJSResource = (url: string): boolean => url?.startsWith(TB_RESOURCE_PREFIX); +export const isTbImage = (url: string): boolean => url?.startsWith(TB_IMAGE_PREFIX); export const extractParamsFromImageResourceUrl = (url: string): {type: ImageResourceType; key: string} => { const res = url.match(IMAGES_URL_REGEXP); diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index a8cf53534e..6d49e9f698 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -896,7 +896,7 @@ pre.tb-highlight { } .mat-icon { - svg { + svg, img { vertical-align: inherit; } &.tb-mat-12 { From f2e66ca01280bcf5998a10b8e46adc57ea21a6cc Mon Sep 17 00:00:00 2001 From: deaflynx Date: Fri, 8 Aug 2025 17:06:02 +0300 Subject: [PATCH 106/216] Feature custom image icon fixes after review. --- ui-ngx/src/app/shared/components/icon.component.ts | 1 + .../components/material-icon-select.component.html | 6 +----- .../components/material-icon-select.component.ts | 11 ----------- 3 files changed, 2 insertions(+), 16 deletions(-) diff --git a/ui-ngx/src/app/shared/components/icon.component.ts b/ui-ngx/src/app/shared/components/icon.component.ts index 36c4e2e0e8..dfc7944716 100644 --- a/ui-ngx/src/app/shared/components/icon.component.ts +++ b/ui-ngx/src/app/shared/components/icon.component.ts @@ -303,6 +303,7 @@ export class TbIconComponent extends _TbIconBase this.imagePipe.transform(rawName, { asString: true, ignoreLoadingImage: true }).subscribe( imageUrl => { const imgElement = this.renderer.createElement('img'); + this.renderer.addClass(imgElement, 'mat-icon'); this.renderer.setAttribute(imgElement, 'src', imageUrl as string); const elem: HTMLElement = this._elementRef.nativeElement; this.renderer.insertBefore(elem, imgElement, this._iconNameContent.nativeElement); diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.html b/ui-ngx/src/app/shared/components/material-icon-select.component.html index 13bf46d1e0..25aa51d30d 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.html +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.html @@ -37,10 +37,6 @@ [disabled]="disabled" #matButton (click)="openIconPopup($event, matButton)"> - @if (!isCustomIcon) { - {{materialIconFormGroup.get('icon').value}} - } @else { - icon - } + {{materialIconFormGroup.get('icon').value}} diff --git a/ui-ngx/src/app/shared/components/material-icon-select.component.ts b/ui-ngx/src/app/shared/components/material-icon-select.component.ts index c2e904d8d0..2bbc5b7528 100644 --- a/ui-ngx/src/app/shared/components/material-icon-select.component.ts +++ b/ui-ngx/src/app/shared/components/material-icon-select.component.ts @@ -36,7 +36,6 @@ import { TbPopoverService } from '@shared/components/popover.service'; import { MaterialIconsComponent } from '@shared/components/material-icons.component'; import { MatButton } from '@angular/material/button'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { isTbImage } from '@shared/models/resource.models'; @Component({ selector: 'tb-material-icon-select', @@ -76,8 +75,6 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit @coerceBoolean() allowedCustomIcon = false; - isCustomIcon = false; - private requiredValue: boolean; get required(): boolean { return this.requiredValue; @@ -138,13 +135,11 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit this.materialIconFormGroup.patchValue( { icon: this.modelValue }, {emitEvent: false} ); - this.defineIconType(value); } private updateModel() { const icon: string = this.materialIconFormGroup.get('icon').value; if (this.modelValue !== icon) { - this.defineIconType(icon); this.modelValue = icon; this.propagateChange(this.modelValue); } @@ -198,10 +193,4 @@ export class MaterialIconSelectComponent extends PageComponent implements OnInit this.materialIconFormGroup.get('icon').patchValue(null, {emitEvent: true}); this.cd.markForCheck(); } - - private defineIconType(icon: string) { - if (this.allowedCustomIcon) { - this.isCustomIcon = isTbImage(icon); - } - } } From a4cf5f7d32d9084fb4db9c912972a62a36b5f4c5 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Fri, 8 Aug 2025 21:54:20 +0300 Subject: [PATCH 107/216] Minor fix in tb-icon custom image feature - add alt attribute. --- ui-ngx/src/app/shared/components/icon.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui-ngx/src/app/shared/components/icon.component.ts b/ui-ngx/src/app/shared/components/icon.component.ts index dfc7944716..0aba7bed93 100644 --- a/ui-ngx/src/app/shared/components/icon.component.ts +++ b/ui-ngx/src/app/shared/components/icon.component.ts @@ -304,6 +304,7 @@ export class TbIconComponent extends _TbIconBase imageUrl => { const imgElement = this.renderer.createElement('img'); this.renderer.addClass(imgElement, 'mat-icon'); + this.renderer.setAttribute(imgElement, 'alt', 'Image icon'); this.renderer.setAttribute(imgElement, 'src', imageUrl as string); const elem: HTMLElement = this._elementRef.nativeElement; this.renderer.insertBefore(elem, imgElement, this._iconNameContent.nativeElement); From b976e052569c8179939409854428b19b5031a0a4 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Tue, 19 Aug 2025 12:37:24 +0300 Subject: [PATCH 108/216] Support custom icons in custom menu popover components Edit menu item; Add custom menu item. Update mixins to handle sizing for image icons. --- ui-ngx/src/scss/mixins.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui-ngx/src/scss/mixins.scss b/ui-ngx/src/scss/mixins.scss index 8e60483cb1..fb4a51ebce 100644 --- a/ui-ngx/src/scss/mixins.scss +++ b/ui-ngx/src/scss/mixins.scss @@ -26,6 +26,10 @@ width: #{$size}px; height: #{$size}px; } + img { + width: #{$size}px; + height: #{$size}px; + } } @mixin tb-mat-icon-button-size($size) { From 34a50f800918303444516923f9e00fd8124f81fd Mon Sep 17 00:00:00 2001 From: deaflynx Date: Wed, 3 Sep 2025 16:21:32 +0300 Subject: [PATCH 109/216] Custom icon feature svg support. --- .../app/shared/components/icon.component.ts | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/ui-ngx/src/app/shared/components/icon.component.ts b/ui-ngx/src/app/shared/components/icon.component.ts index 0aba7bed93..4fe6bb26aa 100644 --- a/ui-ngx/src/app/shared/components/icon.component.ts +++ b/ui-ngx/src/app/shared/components/icon.component.ts @@ -35,6 +35,7 @@ import { isSvgIcon, splitIconName } from '@shared/models/icon.models'; import { ContentObserver } from '@angular/cdk/observers'; import { isTbImage } from '@shared/models/resource.models'; import { ImagePipe } from '@shared/pipe/image.pipe'; +import { DomSanitizer } from '@angular/platform-browser'; const _TbIconBase = mixinColor( class { @@ -115,6 +116,7 @@ export class TbIconComponent extends _TbIconBase private renderer: Renderer2, private _iconRegistry: MatIconRegistry, private imagePipe: ImagePipe, + private sanitizer: DomSanitizer, @Inject(MAT_ICON_LOCATION) private _location: MatIconLocation, private readonly _errorHandler: ErrorHandler) { super(elementRef); @@ -302,13 +304,26 @@ export class TbIconComponent extends _TbIconBase this._clearImageIcon(); this.imagePipe.transform(rawName, { asString: true, ignoreLoadingImage: true }).subscribe( imageUrl => { - const imgElement = this.renderer.createElement('img'); - this.renderer.addClass(imgElement, 'mat-icon'); - this.renderer.setAttribute(imgElement, 'alt', 'Image icon'); - this.renderer.setAttribute(imgElement, 'src', imageUrl as string); - const elem: HTMLElement = this._elementRef.nativeElement; - this.renderer.insertBefore(elem, imgElement, this._iconNameContent.nativeElement); - this._imageElement = imgElement; + const urlStr = imageUrl as string; + const isSvg = rawName?.endsWith('.svg'); + if (isSvg) { + const safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(urlStr); + this._iconRegistry + .getSvgIconFromUrl(safeUrl) + .pipe(take(1)) + .subscribe({ + next: (svg) => { + this.renderer.insertBefore(this._elementRef.nativeElement, svg, this._iconNameContent.nativeElement); + this._imageElement = svg; + }, + error: (err: Error) => { + console.log('err', err) + this._setImageElement(urlStr); + } + }); + } else { + this._setImageElement(urlStr); + } } ); } else { @@ -316,6 +331,16 @@ export class TbIconComponent extends _TbIconBase } } + private _setImageElement(urlStr: string) { + const imgElement = this.renderer.createElement('img'); + this.renderer.addClass(imgElement, 'mat-icon'); + this.renderer.setAttribute(imgElement, 'alt', 'Image icon'); + this.renderer.setAttribute(imgElement, 'src', urlStr); + const elem: HTMLElement = this._elementRef.nativeElement; + this.renderer.insertBefore(elem, imgElement, this._iconNameContent.nativeElement); + this._imageElement = imgElement; + } + private _clearImageIcon() { const elem: HTMLElement = this._elementRef.nativeElement; if (this._imageElement !== null) { From 3c2e289cb3f01c3830d6daa07efb3f82c47a7af3 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Thu, 4 Sep 2025 17:03:10 +0300 Subject: [PATCH 110/216] Refactor feature custom image icon. --- ui-ngx/src/app/shared/components/icon.component.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/ui-ngx/src/app/shared/components/icon.component.ts b/ui-ngx/src/app/shared/components/icon.component.ts index 4fe6bb26aa..20ee9c4a72 100644 --- a/ui-ngx/src/app/shared/components/icon.component.ts +++ b/ui-ngx/src/app/shared/components/icon.component.ts @@ -305,7 +305,7 @@ export class TbIconComponent extends _TbIconBase this.imagePipe.transform(rawName, { asString: true, ignoreLoadingImage: true }).subscribe( imageUrl => { const urlStr = imageUrl as string; - const isSvg = rawName?.endsWith('.svg'); + const isSvg = urlStr?.startsWith('data:image/svg+xml') || urlStr?.endsWith('.svg'); if (isSvg) { const safeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(urlStr); this._iconRegistry @@ -316,10 +316,7 @@ export class TbIconComponent extends _TbIconBase this.renderer.insertBefore(this._elementRef.nativeElement, svg, this._iconNameContent.nativeElement); this._imageElement = svg; }, - error: (err: Error) => { - console.log('err', err) - this._setImageElement(urlStr); - } + error: () => this._setImageElement(urlStr) }); } else { this._setImageElement(urlStr); @@ -336,8 +333,7 @@ export class TbIconComponent extends _TbIconBase this.renderer.addClass(imgElement, 'mat-icon'); this.renderer.setAttribute(imgElement, 'alt', 'Image icon'); this.renderer.setAttribute(imgElement, 'src', urlStr); - const elem: HTMLElement = this._elementRef.nativeElement; - this.renderer.insertBefore(elem, imgElement, this._iconNameContent.nativeElement); + this.renderer.insertBefore(this._elementRef.nativeElement, imgElement, this._iconNameContent.nativeElement); this._imageElement = imgElement; } From 8af4c70e1389438faae2e1424445c1ca448afc6b Mon Sep 17 00:00:00 2001 From: deaflynx Date: Fri, 5 Sep 2025 11:54:42 +0300 Subject: [PATCH 111/216] Support custom icons in widget config. --- .../modules/home/components/widget/widget-config.component.html | 1 + ui-ngx/src/app/shared/components/material-icons.component.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html index 8d46ede134..35c6fde3f1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/widget-config.component.html @@ -71,6 +71,7 @@ diff --git a/ui-ngx/src/app/shared/components/material-icons.component.html b/ui-ngx/src/app/shared/components/material-icons.component.html index 558ffd5e0a..b6880e4ffe 100644 --- a/ui-ngx/src/app/shared/components/material-icons.component.html +++ b/ui-ngx/src/app/shared/components/material-icons.component.html @@ -86,7 +86,7 @@
} @else { -
From 6b810703df993233f5ea00c1dfb3197300e49df9 Mon Sep 17 00:00:00 2001 From: Ruslan Vasylkiv Date: Fri, 5 Sep 2025 17:31:16 +0300 Subject: [PATCH 112/216] Update README.md --- README.md | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 27992d57bd..6267cc3a7a 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,9 @@ Check out our [Getting Started guide](https://thingsboard.io/docs/getting-starte

Provision, monitor and control your IoT entities in secure way using rich server-side APIs. Define relations between your devices, assets, customers or any other entities.

+

@@ -56,8 +57,9 @@ Check out our [Getting Started guide](https://thingsboard.io/docs/getting-starte

Collect and store telemetry data in scalable and fault-tolerant way. Visualize your data with built-in or custom widgets and flexible dashboards. Share dashboards with your customers.

+

@@ -72,12 +74,13 @@ Check out our [Getting Started guide](https://thingsboard.io/docs/getting-starte

Monitor and control your industrial processes in real time with SCADA. Use SCADA symbols on dashboards to create and manage any workflow, offering full flexibility to design and oversee operations according to your requirements.

+

- +
Process and React @@ -87,8 +90,9 @@ Check out our [Getting Started guide](https://thingsboard.io/docs/getting-starte

Define data processing rule chains. Transform and normalize your device data. Raise alarms on incoming telemetry events, attribute updates, device inactivity and user actions.


+

@@ -99,11 +103,11 @@ Check out our [Getting Started guide](https://thingsboard.io/docs/getting-starte ThingsBoard allows you to create complex [Rule Chains](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/) to process data from your devices and match your application specific use cases. -[![IoT Rule Engine](https://github.com/user-attachments/assets/ccc048a8-5aa3-44dc-abd4-c20d1d833102 "IoT Rule Engine")](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/) +[![IoT Rule Engine](https://github.com/user-attachments/assets/43d21dc9-0e18-4f1b-8f9a-b72004e12f07 "IoT Rule Engine")](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/)
-[**Read more about Rule Engine 🡪**](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/) +[**Read more about Rule Engine ➜**](https://thingsboard.io/docs/user-guide/rule-engine-2-0/re-getting-started/)
@@ -113,27 +117,27 @@ ThingsBoard is a scalable, user-friendly, and device-agnostic IoT platform that [**Smart energy**](https://thingsboard.io/use-cases/smart-energy/) -[![Smart energy](https://github.com/user-attachments/assets/7952d0f1-2ba4-4989-bfc9-75b40de6ea3f "Smart energy")](https://thingsboard.io/use-cases/smart-energy/) +[![Smart energy](https://github.com/user-attachments/assets/2a0abf13-6dc5-4f5e-9c30-1aea1d39af1e "Smart energy")](https://thingsboard.io/use-cases/smart-energy/) [**SCADA swimming pool**](https://thingsboard.io/use-cases/scada/) -[![SCADA Swimming pool](https://github.com/user-attachments/assets/b357c129-ea72-4b64-9dfe-ac25011603b6 "SCADA Swimming pool")](https://thingsboard.io/use-cases/scada/) +[![SCADA Swimming pool](https://github.com/user-attachments/assets/68fd9e29-99f1-4c16-8c4c-476f4ccb20c0 "SCADA Swimming pool")](https://thingsboard.io/use-cases/scada/) [**Fleet tracking**](https://thingsboard.io/use-cases/fleet-tracking/) -[![Fleet tracking](https://github.com/user-attachments/assets/80b63841-40c9-4db9-bec2-6a400dc6e58d "Fleet tracking")](https://thingsboard.io/use-cases/fleet-tracking/) +[![Fleet tracking](https://github.com/user-attachments/assets/9e8938ba-ee0c-4599-9494-d74b7de8a63d "Fleet tracking")](https://thingsboard.io/use-cases/fleet-tracking/) [**Smart farming**](https://thingsboard.io/use-cases/smart-farming/) -[![Smart farming](https://github.com/user-attachments/assets/8fe84ad6-6ea4-4cb1-bc31-6cd5c20c357b "Smart farming")](https://thingsboard.io/use-cases/smart-farming/) +[![Smart farming](https://github.com/user-attachments/assets/56b84c99-ef24-44e5-a903-b925b7f9d142 "Smart farming")](https://thingsboard.io/use-cases/smart-farming/) [**Smart metering**](https://thingsboard.io/smart-metering/) -[![Smart metering](https://github.com/user-attachments/assets/564e5ed0-afad-452c-a16c-6270b468ebdc "Smart metering")](https://thingsboard.io/smart-metering/) +[![Smart metering](https://github.com/user-attachments/assets/adc05e3d-397c-48ef-bed6-535bbd698455 "Smart metering")](https://thingsboard.io/smart-metering/)
-[**Check more of our use cases 🡪**](https://thingsboard.io/iot-use-cases/) +[**Check more of our use cases ➜**](https://thingsboard.io/iot-use-cases/)
From ef9a6637f12ec1648bb681d9863203adc0429bb6 Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Mon, 8 Sep 2025 14:51:58 +0300 Subject: [PATCH 113/216] Attribute dialog improvement --- .../components/attribute/add-attribute-dialog.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html index 453f9f6217..cd87bde8f2 100644 --- a/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/attribute/add-attribute-dialog.component.html @@ -29,8 +29,8 @@
-
- +
+ attribute.key From 171e824684a95f458c1176db2ac39964165c8bac Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 9 Sep 2025 13:02:27 +0300 Subject: [PATCH 114/216] Clear alarm node: async processing --- .../rule/engine/action/TbClearAlarmNode.java | 46 +++++++++++-------- .../engine/action/TbClearAlarmNodeTest.java | 11 +++-- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java index 6a806e7bc1..fc541ce4e4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java @@ -16,9 +16,7 @@ package org.thingsboard.rule.engine.action; import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -31,18 +29,21 @@ import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -@Slf4j +import static com.google.common.util.concurrent.Futures.immediateFuture; +import static com.google.common.util.concurrent.Futures.transform; +import static com.google.common.util.concurrent.Futures.transformAsync; + @RuleNode( type = ComponentType.ACTION, name = "clear alarm", relationTypes = {"Cleared", "False"}, configClazz = TbClearAlarmNodeConfiguration.class, nodeDescription = "Clear Alarm", - nodeDetails = - "Details - JS function that creates JSON object based on incoming message. This object will be added into Alarm.details field.\n" + - "Node output:\n" + - "If alarm was not cleared, original message is returned. Otherwise new Message returned with type 'ALARM', Alarm object in 'msg' property and 'metadata' will contains 'isClearedAlarm' property. " + - "Message payload can be accessed via msg property. For example 'temperature = ' + msg.temperature ;. " + - "Message metadata can be accessed via metadata property. For example 'name = ' + metadata.customerName;.", + nodeDetails = """ + Details - JS function that creates JSON object based on incoming message. This object will be added into Alarm.details field. + Node output: + If alarm was not cleared, original message is returned. Otherwise new Message returned with type 'ALARM', Alarm object in 'msg' property and 'metadata' will contains 'isClearedAlarm' property. + Message payload can be accessed via msg property. For example 'temperature = ' + msg.temperature ;. + Message metadata can be accessed via metadata property. For example 'name = ' + metadata.customerName;.""", configDirective = "tbActionNodeClearAlarmConfig", icon = "notifications_off" ) @@ -55,22 +56,26 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode processAlarm(TbContext ctx, TbMsg msg) { - String alarmType = TbNodeUtils.processPattern(this.config.getAlarmType(), msg); - Alarm alarm; - if (msg.getOriginator().getEntityType().equals(EntityType.ALARM)) { - alarm = ctx.getAlarmService().findAlarmById(ctx.getTenantId(), new AlarmId(msg.getOriginator().getId())); + String alarmType = TbNodeUtils.processPattern(config.getAlarmType(), msg); + + ListenableFuture alarmFuture; + if (msg.getOriginator().getEntityType() == EntityType.ALARM) { + alarmFuture = ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), new AlarmId(msg.getOriginator().getId())); } else { - alarm = ctx.getAlarmService().findLatestActiveByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), alarmType); + alarmFuture = ctx.getAlarmService().findLatestActiveByOriginatorAndTypeAsync(ctx.getTenantId(), msg.getOriginator(), alarmType); } - if (alarm != null && !alarm.getStatus().isCleared()) { - return clearAlarm(ctx, msg, alarm); - } - return Futures.immediateFuture(new TbAlarmResult(false, false, false, null)); + + return transformAsync(alarmFuture, alarm -> { + if (alarm != null && !alarm.getStatus().isCleared()) { + return clearAlarmAsync(ctx, msg, alarm); + } + return immediateFuture(new TbAlarmResult(false, false, false, null)); + }, ctx.getDbCallbackExecutor()); } - private ListenableFuture clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) { + private ListenableFuture clearAlarmAsync(TbContext ctx, TbMsg msg, Alarm alarm) { ListenableFuture asyncDetails = buildAlarmDetails(msg, alarm.getDetails()); - return Futures.transform(asyncDetails, details -> { + return transform(asyncDetails, details -> { AlarmApiCallResult result = ctx.getAlarmService().clearAlarm(ctx.getTenantId(), alarm.getId(), System.currentTimeMillis(), details); if (result.isSuccessful()) { return new TbAlarmResult(false, false, result.isCleared(), result.getAlarm()); @@ -79,4 +84,5 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode Date: Tue, 9 Sep 2025 18:42:09 +0300 Subject: [PATCH 115/216] Map widget: fixed lat/long keys duplication --- .../src/app/shared/models/widget/maps/map-model.definition.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts b/ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts index 8013889017..1be4ecd5c6 100644 --- a/ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts +++ b/ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts @@ -31,6 +31,7 @@ import { PolygonsDataLayerSettings } from '@shared/models/widget/maps/map.models'; import { WidgetModelDefinition } from '@shared/models/widget/widget-model.definition'; +import { deepClone } from '@core/utils'; interface AliasFilterPair { alias?: EntityAliasInfo, @@ -271,7 +272,7 @@ const getMapLatestDataLayersDatasources = (settings: MapDataLayerSettings[], const getMapLatestDataLayerDatasourceDataKeys = (settings: MapDataLayerSettings, dataLayerType: MapDataLayerType): DataKey[] => { - const dataKeys = settings.additionalDataKeys || []; + const dataKeys = settings.additionalDataKeys?.length ? deepClone(settings.additionalDataKeys) : []; switch (dataLayerType) { case 'markers': const markersSettings = settings as MarkersDataLayerSettings; From cd8e21244c5e704bac2ac07b455f801e83a90801 Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Wed, 10 Sep 2025 11:20:36 +0300 Subject: [PATCH 116/216] Fixed help link for JavaScript library --- .../pages/admin/resource/js-library-table-config.resolver.ts | 4 +++- ui-ngx/src/app/shared/models/constants.ts | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts index cd992f8886..341bbd2e06 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/js-library-table-config.resolver.ts @@ -81,7 +81,9 @@ export class JsLibraryTableConfigResolver { search: 'javascript.search', selectedEntities: 'javascript.selected-javascript-resources' }; - this.config.entityResources = entityTypeResources.get(EntityType.TB_RESOURCE); + this.config.entityResources = { + helpLinkId: 'jsExtension' + }; this.config.headerComponent = JsLibraryTableHeaderComponent; this.config.entityTitle = (resource) => resource ? diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 1617e46b58..9ed7264830 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -177,6 +177,7 @@ export const HelpLinks = { entitiesImport: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/bulk-provisioning`, rulechains: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/rule-chains`, lwm2mResourceLibrary: `${helpBaseUrl}/docs${docPlatformPrefix}/reference/lwm2m-api`, + jsExtension: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/contribution/ui/advanced-development`, dashboards: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/dashboards`, otaUpdates: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ota-updates`, widgetTypes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/widget-library/#widget-types`, From e05583cd2bb66950e9c03981a63b558b585d731d Mon Sep 17 00:00:00 2001 From: LeoMorgan113 Date: Wed, 10 Sep 2025 13:14:35 +0300 Subject: [PATCH 117/216] Changed text for translation --- .../modules/home/pages/dashboard/dashboard-form.component.html | 2 +- .../pages/dashboard/import-dashboard-file-dialog.component.html | 2 +- ui-ngx/src/assets/locale/locale.constant-en_US.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html index 2e185d66ad..755fb45db1 100644 --- a/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html +++ b/ui-ngx/src/app/modules/home/pages/dashboard/dashboard-form.component.html @@ -32,7 +32,7 @@ [disabled]="(isLoading$ | async)" (click)="onEntityAction($event, 'import')" [class.!hidden]="isEdit || dashboardScope !== 'tenant'"> - {{'dashboard.import' | translate }} + {{'dashboard.update-new-version' | translate }} - -
- + + +
+
+ @if (configFormGroup.get('expressionSIMPLE').errors && configFormGroup.get('expressionSIMPLE').touched) { + + @if (configFormGroup.get('expressionSIMPLE').hasError('required')) { + {{ 'calculated-fields.hint.expression-required' | translate }} + } @else if (configFormGroup.get('expressionSIMPLE').hasError('pattern')) { + {{ 'calculated-fields.hint.expression-invalid' | translate }} + } @else if (configFormGroup.get('expressionSIMPLE').hasError('maxLength')) { + {{ 'calculated-fields.hint.expression-max-length' | translate }} + } + + } @else { + {{ 'calculated-fields.hint.expression' | translate }} + } +
+
+ +
{{ 'api-usage.tbel' | translate }}
+ +
+
+ +
-
+ } @else { +
+
+ {{ 'calculated-fields.entity-coordinates' | translate }} +
+
+ + +
+
+ +
+
+ {{ 'calculated-fields.geofencing-zone-groups' | translate }} +
+ +
+
{{ 'calculated-fields.zone-group-refresh-interval' | translate }}
+
+ + +
+
+
+ }
{{ 'calculated-fields.output' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index 975744b2c6..3037b3a4ce 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -22,19 +22,22 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { DialogComponent } from '@shared/components/dialog.component'; import { + ArgumentEntityType, CalculatedField, CalculatedFieldConfiguration, calculatedFieldDefaultScript, + CalculatedFieldGeofencing, CalculatedFieldTestScriptFn, CalculatedFieldType, CalculatedFieldTypeTranslations, getCalculatedFieldArgumentsEditorCompleter, getCalculatedFieldArgumentsHighlights, + getCalculatedFieldCurrentEntityFilter, OutputType, OutputTypeTranslations } from '@shared/models/calculated-field.models'; import { digitsRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants'; -import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; +import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { EntityType } from '@shared/models/entity-type.models'; import { map, startWith, switchMap } from 'rxjs/operators'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -43,6 +46,8 @@ import { CalculatedFieldsService } from '@core/http/calculated-fields.service'; import { Observable } from 'rxjs'; import { EntityId } from '@shared/models/id/entity-id'; import { AdditionalDebugActionConfig } from '@home/components/entity/debug/entity-debug-settings.model'; +import { EntityFilter } from '@shared/models/query/query.models'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; export interface CalculatedFieldDialogData { value?: CalculatedField; @@ -63,12 +68,20 @@ export interface CalculatedFieldDialogData { }) export class CalculatedFieldDialogComponent extends DialogComponent { + readonly minAllowedScheduledUpdateIntervalInSecForCF = getCurrentAuthState(this.store).minAllowedScheduledUpdateIntervalInSecForCF; + fieldFormGroup = this.fb.group({ name: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], type: [CalculatedFieldType.SIMPLE], debugSettings: [], configuration: this.fb.group({ + entityCoordinates: this.fb.group({ + latitudeKeyName: [null, [Validators.required]], + longitudeKeyName: [null, [Validators.required]], + }), arguments: this.fb.control({}), + zoneGroups: this.fb.control({}), + scheduledUpdateInterval: [this.minAllowedScheduledUpdateIntervalInSecForCF], expressionSIMPLE: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex), Validators.maxLength(255)]], expressionSCRIPT: [calculatedFieldDefaultScript], output: this.fb.group({ @@ -104,6 +117,10 @@ export class CalculatedFieldDialogComponent extends DialogComponent this.data.additionalDebugActionConfig.action({ id: this.data.value.id, ...this.fromGroupValue }), } : null; + currentEntityFilter: EntityFilter; + + isRelatedEntity: boolean; + readonly OutputTypeTranslations = OutputTypeTranslations; readonly OutputType = OutputType; readonly AttributeScope = AttributeScope; @@ -113,6 +130,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent, protected router: Router, @@ -125,6 +143,8 @@ export class CalculatedFieldDialogComponent extends DialogComponent this.toggleKeyByCalculatedFieldType(type)); } + private observeZoneChanges(): void { + this.configFormGroup.get('zoneGroups').valueChanges + .pipe(takeUntilDestroyed()) + .subscribe((zoneGroups: CalculatedFieldGeofencing) => + this.checkRelatedEntity(zoneGroups) + ); + this.checkRelatedEntity(this.configFormGroup.get('zoneGroups').value); + } + + private checkRelatedEntity(zoneGroups: CalculatedFieldGeofencing) { + this.isRelatedEntity = Object.values(zoneGroups).some(zone => zone.refDynamicSourceConfiguration?.type === ArgumentEntityType.RelationQuery); + } + private toggleScopeByOutputType(type: OutputType): void { if (type === OutputType.Attribute) { this.outputFormGroup.get('scope').enable({emitEvent: false}); @@ -222,20 +270,36 @@ export class CalculatedFieldDialogComponent extends DialogComponent +
+
+ + + +
{{ 'common.name' | translate }}
+
+ +
+
{{ geofenceZone.name }}
+ +
+
+
+ + + {{ 'entity.entity-type' | translate }} + + +
+ @if (geofenceZone.refEntityId?.entityType === ArgumentEntityType.Tenant) { + {{ 'calculated-fields.argument-current-tenant' | translate }} + } @else if (geofenceZone.refDynamicSourceConfiguration?.type === ArgumentEntityType.RelationQuery) { + {{ 'calculated-fields.argument-relation-query' | translate }} + } @else if (geofenceZone.refEntityId?.id) { + {{ entityTypeTranslations.get(geofenceZone.refEntityId.entityType).type | translate }} + } @else { + {{ 'calculated-fields.argument-current' | translate }} + } +
+
+
+ + + {{ 'calculated-fields.target-zone' | translate }} + + +
+ @if (geofenceZone.refEntityId?.id && geofenceZone.refEntityId?.entityType !== ArgumentEntityType.Tenant) { + + {{ entityNameMap.get(geofenceZone.refEntityId.id) ?? '' }} + + } +
+
+
+ + + + {{ 'calculated-fields.perimeter-key' | translate }} + + + +
{{ geofenceZone.perimeterKeyName }}
+
+
+
+ + + + {{ 'calculated-fields.report-strategy' | translate }} + + +
{{ GeofencingReportStrategyTranslations.get(geofenceZone.reportStrategy) | translate }}
+
+
+ + + + +
+ + +
+
+
+ + +
+
+ {{ 'calculated-fields.no-zone-configured' | translate }} +
+ @if (errorText) { + + } +
+
+ + @if (maxArgumentsPerCF && zoneGroupsFormArray.length >= maxArgumentsPerCF) { +
+ warning + {{ 'calculated-fields.hint.max-geofencing-zone' | translate }} +
+ } +
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.scss new file mode 100644 index 0000000000..430958d0f4 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.scss @@ -0,0 +1,76 @@ +/** + * 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. + */ +:host { + .arguments-table { + min-height: 108px; + + &-with-error { + min-height: 150px; + } + + .mat-mdc-table { + table-layout: fixed; + } + + .key-text { + font-size: 13px; + } + + .copy-argument-name { + visibility: hidden; + transition: visibility 0.1s; + } + + .argument-name-cell:hover { + .copy-argument-name { + visibility: visible; + } + } + } + + .max-args-warning { + .mat-icon { + color: #FAA405; + } + } + + .tb-form-table-row-cell-buttons { + --mat-badge-legacy-small-size-container-size: 8px; + --mat-badge-small-size-container-overlap-offset: -5px; + --mat-badge-small-size-text-size: 0; + } +} + +:host ::ng-deep { + .arguments-table:not(.arguments-table-with-error) { + .mdc-data-table__row:last-child .mat-mdc-cell { + border-bottom: none; + } + } + + .arguments-table { + .mat-mdc-header-row.mat-row-select .mat-mdc-header-cell.entity-type-header { + padding: 0 28px 0 0; + } + } + + .copy-argument-name { + .mat-icon { + font-size: 16px; + padding: 4px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.ts new file mode 100644 index 0000000000..a11ae4cea5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.ts @@ -0,0 +1,307 @@ +/// +/// 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. +/// + +import { + AfterViewInit, + ChangeDetectorRef, + Component, + DestroyRef, + forwardRef, + Input, + Renderer2, + ViewChild, + ViewContainerRef, +} from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, +} from '@angular/forms'; +import { + ArgumentEntityType, + CalculatedFieldGeofencing, + CalculatedFieldGeofencingValue, + CalculatedFieldType, + GeofencingReportStrategyTranslations, +} from '@shared/models/calculated-field.models'; +import { MatButton } from '@angular/material/button'; +import { TbPopoverService } from '@shared/components/popover.service'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { EntityId } from '@shared/models/id/entity-id'; +import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models'; +import { getEntityDetailsPageURL, isEqual } from '@core/utils'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { TbTableDatasource } from '@shared/components/table/table-datasource.abstract'; +import { EntityService } from '@core/http/entity.service'; +import { MatSort } from '@angular/material/sort'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { forkJoin, Observable } from 'rxjs'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { BaseData } from '@shared/models/base-data'; +import { + CalculatedFieldGeofencingZoneGroupsPanelComponent +} from '@home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component'; + +@Component({ + selector: 'tb-calculated-field-geofencing-zone-groups-table', + templateUrl: './calculated-field-geofencing-zone-groups-table.component.html', + styleUrls: [`calculated-field-geofencing-zone-groups-table.component.scss`], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => CalculatedFieldGeofencingZoneGroupsTableComponent), + multi: true + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => CalculatedFieldGeofencingZoneGroupsTableComponent), + multi: true + } + ], +}) +export class CalculatedFieldGeofencingZoneGroupsTableComponent implements ControlValueAccessor, Validator, AfterViewInit { + + @Input() entityId: EntityId; + @Input() tenantId: string; + @Input() entityName: string; + + @ViewChild(MatSort, { static: true }) sort: MatSort; + + errorText = ''; + zoneGroupsFormArray = this.fb.array([]); + entityNameMap = new Map(); + sortOrder = { direction: 'asc', property: '' }; + dataSource = new CalculatedFieldZoneDatasource(); + + readonly GeofencingReportStrategyTranslations = GeofencingReportStrategyTranslations; + readonly entityTypeTranslations = entityTypeTranslations; + readonly ArgumentEntityType = ArgumentEntityType; + readonly maxArgumentsPerCF = getCurrentAuthState(this.store).maxArgumentsPerCF - 2; + readonly NULL_UUID = NULL_UUID; + + private popoverComponent: TbPopoverComponent; + private propagateChange: (zonesObj: Record) => void = () => {}; + + constructor( + private fb: FormBuilder, + private popoverService: TbPopoverService, + private viewContainerRef: ViewContainerRef, + private cd: ChangeDetectorRef, + private renderer: Renderer2, + private entityService: EntityService, + private destroyRef: DestroyRef, + private store: Store + ) { + this.zoneGroupsFormArray.valueChanges.pipe(takeUntilDestroyed()).subscribe(value => { + this.updateDataSource(value); + this.propagateChange(this.getZonesObject(value)); + }); + } + + ngAfterViewInit(): void { + this.sort.sortChange.asObservable().pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { + this.sortOrder.property = this.sort.active; + this.sortOrder.direction = this.sort.direction; + this.updateDataSource(this.zoneGroupsFormArray.value); + }); + } + + registerOnChange(fn: (zonesObj: Record) => void): void { + this.propagateChange = fn; + } + + registerOnTouched(_): void {} + + validate(): ValidationErrors | null { + this.updateErrorText(); + return this.errorText ? { zonesFormArray: false } : null; + } + + onDelete($event: Event, zone: CalculatedFieldGeofencingValue): void { + $event.stopPropagation(); + const index = this.zoneGroupsFormArray.controls.findIndex(control => isEqual(control.value, zone)); + this.zoneGroupsFormArray.removeAt(index); + this.zoneGroupsFormArray.markAsDirty(); + } + + manageZone($event: Event, matButton: MatButton, zone = {} as CalculatedFieldGeofencingValue): void { + $event?.stopPropagation(); + if (this.popoverComponent && !this.popoverComponent.tbHidden) { + this.popoverComponent.hide(); + } + const trigger = matButton._elementRef.nativeElement; + if (this.popoverService.hasPopover(trigger)) { + this.popoverService.hidePopover(trigger); + } else { + const index = this.zoneGroupsFormArray.controls.findIndex(control => isEqual(control.value, zone)); + const isExists = index !== -1; + const ctx = { + index, + zone, + entityId: this.entityId, + calculatedFieldType: CalculatedFieldType.GEOFENCING, + buttonTitle: isExists ? 'action.apply' : 'action.add', + tenantId: this.tenantId, + entityName: this.entityName, + usedNames: this.zoneGroupsFormArray.value.map(({ name }) => name).filter(name => name !== zone.name), + }; + this.popoverComponent = this.popoverService.displayPopover({ + trigger, + renderer: this.renderer, + componentType: CalculatedFieldGeofencingZoneGroupsPanelComponent, + hostView: this.viewContainerRef, + preferredPlacement: isExists ? ['left', 'leftTop', 'leftBottom'] : ['topRight', 'right', 'rightTop'], + context: ctx, + isModal: true + }); + this.popoverComponent.tbComponentRef.instance.geofencingDataApplied.subscribe(({ entityName, ...value }) => { + this.popoverComponent.hide(); + if (entityName) { + this.entityNameMap.set(value.refEntityId.id, entityName); + } + if (isExists) { + this.zoneGroupsFormArray.at(index).setValue(value); + } else { + this.zoneGroupsFormArray.push(this.fb.control(value)); + } + this.cd.markForCheck(); + }); + } + } + + private updateDataSource(value: CalculatedFieldGeofencingValue[]): void { + const sortedValue = this.sortData(value); + this.dataSource.loadData(sortedValue); + } + + private updateErrorText(): void { + if (this.zoneGroupsFormArray.controls.some(control => control.value.refEntityId?.id === NULL_UUID)) { + this.errorText = 'calculated-fields.hint.geofencing-entity-not-found'; + } else if (!this.zoneGroupsFormArray.controls.length) { + this.errorText = 'calculated-fields.hint.geofencing-empty'; + } else { + this.errorText = ''; + } + } + + private getZonesObject(value: CalculatedFieldGeofencingValue[]): Record { + return value.reduce((acc, zoneValue) => { + const { name, ...zone } = zoneValue as CalculatedFieldGeofencingValue; + acc[name] = zone; + return acc; + }, {} as Record); + } + + writeValue(zonesObj: Record): void { + this.zoneGroupsFormArray.clear(); + this.populateZonesFormArray(zonesObj); + this.updateEntityNameMap(this.zoneGroupsFormArray.value); + } + + getEntityDetailsPageURL(id: string, type: EntityType): string { + return getEntityDetailsPageURL(id, type); + } + + private populateZonesFormArray(zonesObj: Record): void { + Object.keys(zonesObj).forEach(key => { + const value: CalculatedFieldGeofencingValue = { + ...zonesObj[key], + name: key + }; + this.zoneGroupsFormArray.push(this.fb.control(value), { emitEvent: false }); + }); + this.zoneGroupsFormArray.updateValueAndValidity(); + } + + private updateEntityNameMap(values: CalculatedFieldGeofencingValue[]): void { + const entitiesByType = values.reduce((acc, { refEntityId = {}}) => { + if (refEntityId.id && refEntityId.entityType !== ArgumentEntityType.Tenant) { + const { id, entityType } = refEntityId as EntityId; + acc[entityType] = acc[entityType] ?? []; + acc[entityType].push(id); + } + return acc; + }, {} as Record); + const tasks = Object.entries(entitiesByType).map(([entityType, ids]) => + this.entityService.getEntities(entityType as EntityType, ids) + ); + if (!tasks.length) { + return; + } + this.fetchEntityNames(tasks, values); + } + + private fetchEntityNames(tasks: Observable[]>[], values: CalculatedFieldGeofencingValue[]): void { + forkJoin(tasks as Observable[]>[]) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((result: Array>[]) => { + result.forEach((entities: BaseData[]) => entities.forEach((entity: BaseData) => this.entityNameMap.set(entity.id.id, entity.name))); + let updateTable = false; + values.forEach(({ refEntityId }) => { + if (refEntityId?.id && !this.entityNameMap.has(refEntityId.id) && refEntityId.entityType !== ArgumentEntityType.Tenant) { + updateTable = true; + const control = this.zoneGroupsFormArray.controls.find(control => control.value.refEntityId?.id === refEntityId.id); + const value = control.value; + value.refEntityId.id = NULL_UUID; + control.setValue(value, { emitEvent: false }); + } + }); + if (updateTable) { + this.zoneGroupsFormArray.updateValueAndValidity(); + } + }); + } + + private getSortValue(zone: CalculatedFieldGeofencingValue, column: string): string { + switch (column) { + case 'entityType': + if (zone.refEntityId?.entityType === ArgumentEntityType.Tenant) { + return 'calculated-fields.argument-current-tenant'; + } else if (zone.refDynamicSourceConfiguration.type === ArgumentEntityType.RelationQuery) { + return 'calculated-fields.argument-relation-query'; + } else if (zone.refEntityId?.id) { + return entityTypeTranslations.get((zone.refEntityId)?.entityType as unknown as EntityType).type; + } else { + return 'calculated-fields.argument-current'; + } + case 'key': + return zone.perimeterKeyName; + case 'reportStrategy': + return GeofencingReportStrategyTranslations.get(zone.reportStrategy); + default: + return zone.name; + } + } + + private sortData(data: CalculatedFieldGeofencingValue[]): CalculatedFieldGeofencingValue[] { + return data.sort((a, b) => { + const valA = this.getSortValue(a, this.sortOrder.property) ?? ''; + const valB = this.getSortValue(b, this.sortOrder.property) ?? ''; + return (this.sortOrder.direction === 'asc' ? 1 : -1) * valA.localeCompare(valB); + }); + } +} + +class CalculatedFieldZoneDatasource extends TbTableDatasource { + constructor() { + super(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts index 6f46e47af9..8ccaa4fe49 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-argument-panel.component.ts @@ -86,7 +86,7 @@ export class CalculatedFieldArgumentPanelComponent implements OnInit, AfterViewI entityFilter: EntityFilter; entityNameSubject = new BehaviorSubject(null); - readonly argumentEntityTypes = Object.values(ArgumentEntityType) as ArgumentEntityType[]; + readonly argumentEntityTypes = Object.values(ArgumentEntityType).filter(value => value !== ArgumentEntityType.RelationQuery) as ArgumentEntityType[]; readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations; readonly ArgumentType = ArgumentType; readonly DataKeyType = DataKeyType; diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.html new file mode 100644 index 0000000000..a660158ab9 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.html @@ -0,0 +1,224 @@ + +
+
+
{{ 'calculated-fields.geofencing-zone-groups-settings' | translate }}
+
+
+
{{ 'calculated-fields.name' | translate }}
+ + + @if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('required')) { + + warning + + } @else if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('duplicateName')) { + + warning + + } @else if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('pattern')) { + + warning + + } @else if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('maxlength')) { + + warning + + } @else if (geofencingFormGroup.get('name').touched && geofencingFormGroup.get('name').hasError('forbiddenName')) { + + warning + + } + +
+ +
+
{{ 'entity.entity-type' | translate }}
+ + + @for (type of argumentEntityTypes; track type) { + {{ ArgumentEntityTypeTranslations.get(type) | translate }} + } + + +
+ @if (ArgumentEntityTypeParamsMap.has(entityType)) { +
+
{{ ArgumentEntityTypeParamsMap.get(entityType).title | translate }}
+ +
+ } +
+ +
+ + {{ 'calculated-fields.relation-query' | translate }}* + +
+
{{ 'calculated-fields.direction' | translate }}
+ + + @for (direction of GeofencingDirectionList; track direction) { + {{ GeofencingDirectionTranslations.get(direction) | translate }} + } + + +
+
+
{{ 'calculated-fields.relation-type' | translate }}
+ + +
+
+
{{ 'calculated-fields.relation-level' | translate }}
+ + + @if (refDynamicSourceFormGroup.get('maxLevel').touched && refDynamicSourceFormGroup.get('maxLevel').hasError('required')) { + + warning + + } @else if (refDynamicSourceFormGroup.get('maxLevel').touched && refDynamicSourceFormGroup.get('maxLevel').hasError('min')) { + + warning + + } @else if (refDynamicSourceFormGroup.get('maxLevel').touched && refDynamicSourceFormGroup.get('maxLevel').hasError('max')) { + + warning + + } + +
+
+ + {{ 'calculated-fields.fetch-last-available-level' | translate }} + +
+
+
+
+ +
+
+ {{ 'calculated-fields.perimeter-attribute-key' | translate }} +
+ +
+
+
{{ 'calculated-fields.report-strategy' | translate }}
+ + + @for (strategy of GeofencingReportStrategyList; track strategy) { + {{ GeofencingReportStrategyTranslations.get(strategy) | translate }} + } + + +
+
+
+ +
+ {{ 'calculated-fields.create-relation-with-matched-zones' | translate }} +
+
+
+
{{ 'calculated-fields.direction' | translate }}
+ + + @for (direction of GeofencingDirectionList; track direction) { + {{ GeofencingDirectionTranslations.get(direction) | translate }} + } + + +
+
+
{{ 'calculated-fields.relation-type' | translate }}
+ + +
+
+
+
+
+ + +
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.scss new file mode 100644 index 0000000000..bedaf2eeb0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.scss @@ -0,0 +1,51 @@ +/** + * 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. + */ +@import '../../../../../../../scss/constants'; + +$panel-width: 520px; + +:host { + display: flex; + width: $panel-width; + max-width: 100%; + max-height: 80vh; + + .fixed-title-width { + @media #{$mat-xs} { + min-width: 120px; + } + } + + .limit-field-row { + @media screen and (max-width: $panel-width) { + display: flex; + flex-direction: column; + + .fixed-title-width { + align-self: flex-start; + padding-top: 8px; + } + } + } +} + +:host ::ng-deep { + .time-interval-field { + .advanced-input { + flex-direction: column; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.ts new file mode 100644 index 0000000000..72ea58919f --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.ts @@ -0,0 +1,291 @@ +/// +/// 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. +/// + +import { AfterViewInit, ChangeDetectorRef, Component, Input, OnInit, output, ViewChild } from '@angular/core'; +import { TbPopoverComponent } from '@shared/components/popover.component'; +import { FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; +import { charsWithNumRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants'; +import { + ArgumentEntityType, + ArgumentEntityTypeParamsMap, + ArgumentEntityTypeTranslations, + CalculatedFieldGeofencing, + CalculatedFieldGeofencingValue, + CalculatedFieldType, + GeofencingDirectionTranslations, + GeofencingReportStrategy, + GeofencingReportStrategyTranslations, + getCalculatedFieldCurrentEntityFilter +} from '@shared/models/calculated-field.models'; +import { debounceTime, delay, distinctUntilChanged, filter, map } from 'rxjs/operators'; +import { EntityType } from '@shared/models/entity-type.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { EntityId } from '@shared/models/id/entity-id'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { EntityFilter } from '@shared/models/query/query.models'; +import { AliasFilterType } from '@shared/models/alias.models'; +import { BehaviorSubject, merge, Observable, of } from 'rxjs'; +import { getCurrentAuthState } from '@core/auth/auth.selectors'; +import { AppState } from '@core/core.state'; +import { Store } from '@ngrx/store'; +import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component'; +import { NULL_UUID } from '@shared/models/id/has-uuid'; +import { EntitySearchDirection } from '@shared/models/relation.models'; + +@Component({ + selector: 'tb-calculated-field-geofencing-zone-groups-panel', + templateUrl: './calculated-field-geofencing-zone-groups-panel.component.html', + styleUrls: ['./calculated-field-geofencing-zone-groups-panel.component.scss'] +}) +export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit, AfterViewInit { + + @Input() buttonTitle: string; + @Input() zone: CalculatedFieldGeofencing; + @Input() entityId: EntityId; + @Input() tenantId: string; + @Input() entityName: string; + @Input() calculatedFieldType: CalculatedFieldType; + @Input() usedNames: string[]; + + @ViewChild('entityAutocomplete') entityAutocomplete: EntityAutocompleteComponent; + + geofencingDataApplied = output(); + + readonly maxRelationLevelPerCfArgument = getCurrentAuthState(this.store).maxRelationLevelPerCfArgument; + + geofencingFormGroup = this.fb.group({ + name: ['', [Validators.required, this.uniqNameRequired(), this.forbiddenNameValidator(), Validators.pattern(charsWithNumRegex), Validators.maxLength(255)]], + refEntityId: this.fb.group({ + entityType: [ArgumentEntityType.Current], + id: [''] + }), + refDynamicSourceConfiguration: this.fb.group({ + direction: [EntitySearchDirection.TO], + relationType: ['', [Validators.required]], + maxLevel: [1, [Validators.required, Validators.min(1), Validators.max(this.maxRelationLevelPerCfArgument)]], + fetchLastLevelOnly: [false], + }), + perimeterKeyName: ['', [Validators.pattern(oneSpaceInsideRegex)]], + reportStrategy: [GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS], + createRelationsWithMatchedZones: [false], + direction: [EntitySearchDirection.TO], + relationType: ['', [Validators.required]] + }); + + entityFilter: EntityFilter; + entityNameSubject = new BehaviorSubject(null); + + readonly ArgumentEntityType = ArgumentEntityType; + readonly argumentEntityTypes = Object.values(ArgumentEntityType) as ArgumentEntityType[]; + readonly ArgumentEntityTypeTranslations = ArgumentEntityTypeTranslations; + readonly DataKeyType = DataKeyType; + readonly ArgumentEntityTypeParamsMap = ArgumentEntityTypeParamsMap; + readonly GeofencingReportStrategyList = Object.values(GeofencingReportStrategy) as Array; + readonly GeofencingReportStrategyTranslations = GeofencingReportStrategyTranslations; + readonly GeofencingDirectionList = Object.values(EntitySearchDirection) as Array; + readonly GeofencingDirectionTranslations = GeofencingDirectionTranslations; + + private currentEntityFilter: EntityFilter; + + constructor( + private fb: FormBuilder, + private cd: ChangeDetectorRef, + private popover: TbPopoverComponent, + private store: Store + ) { + + this.observeMaxLevelChanges(); + this.observeEntityFilterChanges(); + this.observeEntityTypeChanges(); + this.observeUpdatePosition(); + this.observeCreateRelationZonesChanges(); + } + + get entityType(): ArgumentEntityType { + return this.geofencingFormGroup.get('refEntityId').get('entityType').value; + } + + get refEntityIdFormGroup(): FormGroup { + return this.geofencingFormGroup.get('refEntityId') as FormGroup; + } + + get refDynamicSourceFormGroup(): FormGroup { + return this.geofencingFormGroup.get('refDynamicSourceConfiguration') as FormGroup; + } + + ngOnInit(): void { + this.geofencingFormGroup.patchValue(this.zone, {emitEvent: false}); + if (this.zone.refDynamicSourceConfiguration?.type) { + this.refEntityIdFormGroup.get('entityType').setValue(this.zone.refDynamicSourceConfiguration.type, {emitEvent: false}); + } + this.validateFetchLastLevelOnly(this.zone?.refDynamicSourceConfiguration?.maxLevel); + this.validateDirectionAndRelationType(this.zone?.createRelationsWithMatchedZones); + this.validateRefDynamicSourceConfiguration(this.zone?.refEntityId?.entityType || this.zone?.refDynamicSourceConfiguration?.type); + + this.currentEntityFilter = getCalculatedFieldCurrentEntityFilter(this.entityName, this.entityId); + this.updateEntityFilter(this.zone.refEntityId?.entityType); + } + + fetchOptions(searchText: string): Observable> { + const search = searchText ? searchText?.toLowerCase() : ''; + return of(['Contains', 'Manages']).pipe(map(name => name?.filter(option => option.toLowerCase().includes(search)))); + } + + private observeMaxLevelChanges(): void { + this.refDynamicSourceFormGroup.get('maxLevel').valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(value => this.validateFetchLastLevelOnly(value)); + } + + private observeCreateRelationZonesChanges(): void { + this.geofencingFormGroup.get('createRelationsWithMatchedZones').valueChanges + .pipe(takeUntilDestroyed()) + .subscribe(value => this.validateDirectionAndRelationType(value)); + } + + private validateFetchLastLevelOnly(maxLevel = 1): void { + if (maxLevel > 1) { + this.refDynamicSourceFormGroup.get('fetchLastLevelOnly').enable({emitEvent: false}); + } else { + this.refDynamicSourceFormGroup.get('fetchLastLevelOnly').disable({emitEvent: false}); + } + } + + private validateDirectionAndRelationType(createRelation = false): void { + if (createRelation) { + this.geofencingFormGroup.get('direction').enable({emitEvent: false}); + this.geofencingFormGroup.get('relationType').enable({emitEvent: false}); + } else { + this.geofencingFormGroup.get('direction').disable({emitEvent: false}); + this.geofencingFormGroup.get('relationType').disable({emitEvent: false}); + } + } + + private validateRefDynamicSourceConfiguration(type: ArgumentEntityType = ArgumentEntityType.Current): void { + if (type === ArgumentEntityType.RelationQuery) { + this.refDynamicSourceFormGroup.enable({emitEvent: false}); + } else { + this.refDynamicSourceFormGroup.disable({emitEvent: false}); + } + } + + ngAfterViewInit(): void { + if (this.zone.refEntityId?.id === NULL_UUID) { + this.entityAutocomplete.selectEntityFormGroup.get('entity').markAsTouched(); + } + } + + saveZone(): void { + const value = this.geofencingFormGroup.value as CalculatedFieldGeofencingValue; + const argumentType = value.refEntityId.entityType; + switch (argumentType) { + case ArgumentEntityType.Current: + delete value.refEntityId; + break; + case ArgumentEntityType.RelationQuery: + delete value.refEntityId; + value.refDynamicSourceConfiguration.type = ArgumentEntityType.RelationQuery; + break; + case ArgumentEntityType.Tenant: + value.refEntityId.id = this.tenantId; + break + default: + value.entityName = this.entityNameSubject.value; + } + this.geofencingDataApplied.emit(value); + } + + cancel(): void { + this.popover.hide(); + } + + private updateEntityFilter(entityType: ArgumentEntityType = ArgumentEntityType.Current): void { + let entityFilter: EntityFilter; + switch (entityType) { + case ArgumentEntityType.Current: + case ArgumentEntityType.RelationQuery: + entityFilter = this.currentEntityFilter; + break; + case ArgumentEntityType.Tenant: + entityFilter = { + type: AliasFilterType.singleEntity, + singleEntity: { + id: this.tenantId, + entityType: EntityType.TENANT + }, + }; + break; + default: + entityFilter = { + type: AliasFilterType.singleEntity, + singleEntity: this.geofencingFormGroup.get('refEntityId').value as unknown as EntityId, + }; + } + this.entityFilter = entityFilter; + this.cd.markForCheck(); + } + + private observeEntityFilterChanges(): void { + merge( + this.refEntityIdFormGroup.get('entityType').valueChanges, + this.refEntityIdFormGroup.get('id').valueChanges.pipe(filter(Boolean)), + ) + .pipe(debounceTime(50), takeUntilDestroyed()) + .subscribe(() => this.updateEntityFilter(this.entityType)); + } + + private observeEntityTypeChanges(): void { + this.refEntityIdFormGroup.get('entityType').valueChanges + .pipe(distinctUntilChanged(), takeUntilDestroyed()) + .subscribe(type => { + this.geofencingFormGroup.get('refEntityId').get('id').setValue(''); + const isEntityWithId = type !== ArgumentEntityType.Tenant && type !== ArgumentEntityType.Current && type !== ArgumentEntityType.RelationQuery; + this.geofencingFormGroup.get('refEntityId') + .get('id')[isEntityWithId ? 'enable' : 'disable'](); + if (!isEntityWithId) { + this.entityNameSubject.next(null); + } + this.validateRefDynamicSourceConfiguration(type); + }); + } + + private uniqNameRequired(): ValidatorFn { + return (control: FormControl) => { + const newName = control.value.trim().toLowerCase(); + const isDuplicate = this.usedNames?.some(name => name.toLowerCase() === newName); + + return isDuplicate ? { duplicateName: true } : null; + }; + } + + private forbiddenNameValidator(): ValidatorFn { + return (control: FormControl) => { + const trimmedValue = control.value.trim().toLowerCase(); + const forbiddenNames = ['ctx', 'e', 'pi']; + return forbiddenNames.includes(trimmedValue) ? { forbiddenName: true } : null; + }; + } + + private observeUpdatePosition(): void { + merge( + this.refEntityIdFormGroup.get('entityType').valueChanges, + this.geofencingFormGroup.get('createRelationsWithMatchedZones').valueChanges + ) + .pipe(delay(50), takeUntilDestroyed()) + .subscribe(() => this.popover.updatePosition()); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 31a3066edf..7298038bd0 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -205,6 +205,12 @@ import { } from '@home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component'; import { CheckConnectivityDialogComponent } from '@home/components/ai-model/check-connectivity-dialog.component'; import { AIModelDialogComponent } from '@home/components/ai-model/ai-model-dialog.component'; +import { + CalculatedFieldGeofencingZoneGroupsTableComponent +} from '@home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component'; +import { + CalculatedFieldGeofencingZoneGroupsPanelComponent +} from '@home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component'; @NgModule({ declarations: @@ -356,6 +362,8 @@ import { AIModelDialogComponent } from '@home/components/ai-model/ai-model-dialo CalculatedFieldDebugDialogComponent, CalculatedFieldScriptTestDialogComponent, CalculatedFieldTestArgumentsComponent, + CalculatedFieldGeofencingZoneGroupsTableComponent, + CalculatedFieldGeofencingZoneGroupsPanelComponent, CheckConnectivityDialogComponent, AIModelDialogComponent, ], @@ -503,6 +511,8 @@ import { AIModelDialogComponent } from '@home/components/ai-model/ai-model-dialo CalculatedFieldDebugDialogComponent, CalculatedFieldScriptTestDialogComponent, CalculatedFieldTestArgumentsComponent, + CalculatedFieldGeofencingZoneGroupsTableComponent, + CalculatedFieldGeofencingZoneGroupsPanelComponent, CheckConnectivityDialogComponent, AIModelDialogComponent, ], diff --git a/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.html b/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.html index 839a2e00cd..efc90ec048 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.html @@ -16,7 +16,7 @@ --> - warning diff --git a/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts index 84d723d114..a86fc70115 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts @@ -14,7 +14,17 @@ /// limitations under the License. /// -import { Component, effect, ElementRef, forwardRef, input, OnChanges, SimpleChanges, ViewChild, } from '@angular/core'; +import { + Component, + effect, + ElementRef, + forwardRef, + Input, + input, + OnChanges, + SimpleChanges, + ViewChild, +} from '@angular/core'; import { ControlValueAccessor, FormBuilder, @@ -32,6 +42,7 @@ import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry. import { EntitiesKeysByQuery } from '@shared/models/entity.models'; import { EntityFilter } from '@shared/models/query/query.models'; import { isEqual } from '@core/utils'; +import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'tb-entity-key-autocomplete', @@ -53,6 +64,9 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val @ViewChild('keyInput', {static: true}) keyInput: ElementRef; + @Input() placeholder = this.translate.instant('action.set'); + @Input() requiredText = this.translate.instant('common.hint.key-required'); + entityFilter = input.required(); dataKeyType = input.required(); keyScopeType = input(); @@ -96,6 +110,7 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val constructor( private fb: FormBuilder, private entityService: EntityService, + private translate: TranslateService, ) { this.keyControl.valueChanges .pipe(takeUntilDestroyed()) diff --git a/ui-ngx/src/app/shared/components/time-unit-input.component.html b/ui-ngx/src/app/shared/components/time-unit-input.component.html index 0da1cf4023..d5f6319576 100644 --- a/ui-ngx/src/app/shared/components/time-unit-input.component.html +++ b/ui-ngx/src/app/shared/components/time-unit-input.component.html @@ -28,19 +28,18 @@
@if (inlineField) { - warning - - } @else { - - - {{ hasError }} - + matTooltipPosition="above" + matTooltipClass="tb-error-tooltip" + [matTooltip]="hasError" + *ngIf="hasError" + class="tb-error"> + warning + } + + + {{ hasError }} + + Validators.min(Math.ceil(this.minTime / this.timeIntervalsInSec.get(this.timeInputForm.get('timeUnit').value)))(control) + ); } timeControl.setValidators(validators); diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index 76b775b2fd..a86943c86e 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -14,11 +14,7 @@ /// limitations under the License. /// -import { - HasEntityDebugSettings, - HasTenantId, - HasVersion -} from '@shared/models/entity.models'; +import { HasEntityDebugSettings, HasTenantId, HasVersion } from '@shared/models/entity.models'; import { BaseData, ExportableEntity } from '@shared/models/base-data'; import { CalculatedFieldId } from '@shared/models/id/calculated-field-id'; import { EntityId } from '@shared/models/id/entity-id'; @@ -33,6 +29,7 @@ import { dotOperatorHighlightRule, endGroupHighlightRule } from '@shared/models/ace/ace.models'; +import { EntitySearchDirection } from '@shared/models/relation.models'; export interface CalculatedField extends Omit, 'label'>, HasVersion, HasEntityDebugSettings, HasTenantId, ExportableEntity { configuration: CalculatedFieldConfiguration; @@ -43,19 +40,23 @@ export interface CalculatedField extends Omit, 'labe export enum CalculatedFieldType { SIMPLE = 'SIMPLE', SCRIPT = 'SCRIPT', + GEOFENCING = 'GEOFENCING' } export const CalculatedFieldTypeTranslations = new Map( [ [CalculatedFieldType.SIMPLE, 'calculated-fields.type.simple'], [CalculatedFieldType.SCRIPT, 'calculated-fields.type.script'], + [CalculatedFieldType.GEOFENCING, 'calculated-fields.type.geofencing'], ] ) export interface CalculatedFieldConfiguration { type: CalculatedFieldType; - expression: string; - arguments: Record; + expression?: string; + arguments?: Record; + zoneGroups?: Record; + scheduledUpdateInterval?: number; output: CalculatedFieldOutput; } @@ -72,6 +73,7 @@ export enum ArgumentEntityType { Asset = 'ASSET', Customer = 'CUSTOMER', Tenant = 'TENANT', + RelationQuery = 'RELATION_QUERY', } export const ArgumentEntityTypeTranslations = new Map( @@ -81,6 +83,28 @@ export const ArgumentEntityTypeTranslations = new Map( + [ + [GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, 'calculated-fields.report-transition-event-and-presence'], + [GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_ONLY, 'calculated-fields.report-transition-event-only'], + [GeofencingReportStrategy.REPORT_PRESENCE_STATUS_ONLY, 'calculated-fields.report-presence-status-only'] + ] +) + +export const GeofencingDirectionTranslations = new Map( + [ + [EntitySearchDirection.FROM, 'calculated-fields.direction-from'], + [EntitySearchDirection.TO, 'calculated-fields.direction-to'], ] ) @@ -131,6 +155,29 @@ export interface CalculatedFieldArgument { timeWindow?: number; } +export interface CalculatedFieldGeofencing { + perimeterKeyName: string; + reportStrategy: GeofencingReportStrategy; + refEntityId?: RefEntityId; + refDynamicSourceConfiguration: RefDynamicSourceConfiguration; + createRelationsWithMatchedZones: boolean; + relationType: string; + direction: EntitySearchDirection; +} + +export interface RefDynamicSourceConfiguration { + type?: ArgumentEntityType.RelationQuery; + direction: EntitySearchDirection; + relationType: string; + maxLevel: number; + fetchLastLevelOnly?: boolean; +} + +export interface CalculatedFieldGeofencingValue extends CalculatedFieldGeofencing { + name: string; + entityName?: string; +} + export interface RefEntityKey { key: string; type: ArgumentType; diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index 94ada91556..3e414577ec 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -228,7 +228,7 @@ import { JsFuncModuleRowComponent } from '@shared/components/js-func-module-row. import { EntityKeyAutocompleteComponent } from '@shared/components/entity/entity-key-autocomplete.component'; import { DurationLeftPipe } from '@shared/pipe/duration-left.pipe'; import { MqttVersionSelectComponent } from '@shared/components/mqtt-version-select.component'; -import { TimeUnitInputComponent } from "@shared/components/time-unit-input.component"; +import { TimeUnitInputComponent } from '@shared/components/time-unit-input.component'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index fd368e2853..d0844ad85e 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1021,12 +1021,14 @@ "selected-fields": "{ count, plural, =1 {1 calculated field} other {# calculated fields} } selected", "type": { "simple": "Simple", - "script": "Script" + "script": "Script", + "geofencing" : "Geofencing" }, "arguments": "Arguments", "decimals-by-default": "Decimals by default", "debugging": "Calculated field debugging", "argument-name": "Argument name", + "name": "Name", "datasource": "Datasource", "add-argument": "Add argument", "test-script-function": "Test script function", @@ -1038,6 +1040,7 @@ "argument-asset": "Asset", "argument-customer": "Customer", "argument-tenant": "Current tenant", + "argument-relation-query": "Related entities", "argument-type": "Argument type", "see-debug-events": "See debug events", "attribute": "Attribute", @@ -1071,6 +1074,34 @@ "delete-multiple-text": "Be careful, after the confirmation all selected calculated fields will be removed and all related data will become unrecoverable.", "test-with-this-message": "Test with this message", "use-latest-timestamp": "Use latest timestamp", + "entity-coordinates": "Entity coordinates", + "latitude-time-series-key": "Latitude time series key", + "latitude-time-series-key-required": "Latitude time series key is required.", + "longitude-time-series-key": "Longitude time series key", + "longitude-time-series-key-required": "Longitude time series key is required.", + "geofencing-zone-groups": "Geofencing zone groups", + "geofencing-zone-groups-settings": "Geofencing zone group settings", + "target-zone": "Target zone", + "perimeter-key": "Perimeter key", + "report-strategy": "Report strategy", + "no-zone-configured": "No zone group configured", + "no-zone-configured-required": "At least one zone group must be configured.", + "add-zone-group": "Add zone group", + "report-transition-event-only": "Transition events only", + "report-presence-status-only": "Presence status only", + "report-transition-event-and-presence": "Presence status and transition events", + "perimeter-attribute-key": "Perimeter attribute key", + "relation-query": "Relations query", + "direction": "Direction", + "direction-from": "From source entity", + "direction-to": "To source entity", + "relation-type": "Relation type", + "create-relation-with-matched-zones": "Create relations with matched zones", + "relation-level": "Relation level", + "fetch-last-available-level": "Fetch last available level only", + "zone-group-refresh-interval": "Zone groups refresh interval", + "copy-zone-group-name": "Copy zone group name", + "open-details-page": "Open entity details page", "hint": { "arguments-simple-with-rolling": "Simple type calculated field should not contain keys with time series rolling type.", "arguments-empty": "Arguments should not be empty.", @@ -1082,12 +1113,32 @@ "argument-name-duplicate": "Argument with such name already exists.", "argument-name-max-length": "Argument name should be less than 256 characters.", "argument-name-forbidden": "Argument name is reserved and cannot be used.", + "name-required": "Mame is required.", + "name-pattern": "Name is invalid.", + "name-duplicate": "Name with such name already exists.", + "name-max-length": "Name should be less than 256 characters.", + "name-forbidden": "Name is reserved and cannot be used.", "argument-type-required": "Argument type is required.", "max-args": "Maximum number of arguments reached.", "decimals-range": "Decimals by default should be a number between 0 and 15.", "expression": "Default expression demonstrates how to transform a temperature from Fahrenheit to Celsius.", "arguments-entity-not-found": "Argument target entity not found.", - "use-latest-timestamp": "If enabled, the calculated value will be persisted using the most recent timestamp from the arguments telemetry, instead of the server time." + "use-latest-timestamp": "If enabled, the calculated value will be persisted using the most recent timestamp from the arguments telemetry, instead of the server time.", + "entity-coordinates": "Specify the time series keys that provide entity GPS coordinates (latitude and longitude).", + "geofencing-zone-groups": "Define one or more geofencing zones groups to check (e.g. 'allowedZones', 'restrictedZones'). Each group must have a unique name, which is used as a prefix for calculated field output telemetry keys.", + "perimeter-attribute-key": "Set the attribute key that contains the geofencing zone perimeter definition. The perimeter is always taken from server-side attributes of the zone entity.", + "report-strategy": "Presence status reports whether the entity is currently INSIDE or OUTSIDE the zone group.Transition events report when the entity ENTERED or LEFT the zone group.", + "create-relation-with-matched-zones": "Automatically create and maintain relations between the entity and the zones it is currently inside. Relations are removed when the entity leaves a zone and created when it enters a new one.", + "relation-type-required": "Relation type is required.", + "relation-level-required": "Relation level is required.", + "relation-level-min": "Minimum relation level value is 1.", + "relation-level-max": "Maximum relation level value is {{max}}.", + "geofencing-empty": "At least one zone group must be configured.", + "geofencing-entity-not-found": "Geofencing target entity not found.", + "max-geofencing-zone": "Maximum number of geofencing zones reached.", + "zone-group-refresh-interval": "Defines how often zone groups configured via related entities are refreshed. Set to 0 to disable scheduled refresh.", + "zone-group-refresh-interval-required": "Zone groups refresh interval is required.", + "zone-group-refresh-interval-min": "Zone group refresh interval is below the minimum allowed system interval." } }, "ai-models": { From 99350878125b1789ce9712e2aa73cdc575772ab5 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Mon, 15 Sep 2025 13:09:23 +0300 Subject: [PATCH 122/216] fixed timestamp translations --- .../widget/lib/timeseries-table-widget.component.html | 2 +- .../components/widget/lib/timeseries-table-widget.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html index 9f13f20331..4df580eda7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html @@ -47,7 +47,7 @@ - Timestamp + {{ 'audit-log.timestamp' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index 303593af26..90a74585fc 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -513,7 +513,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI let title = ''; const header = this.sources[index].header.find(column => column.index.toString() === value); if (value === '0') { - title = 'Timestamp'; + title = this.translate.instant('audit-log.timestamp'); } else if (value === 'actions') { title = 'Actions'; } else { From 41d90b8e1bde908f16357f9b1b7e292f7fe9fe33 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Mon, 15 Sep 2025 15:06:06 +0300 Subject: [PATCH 123/216] edit translation key --- .../widget/lib/timeseries-table-widget.component.html | 2 +- .../components/widget/lib/timeseries-table-widget.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html index 4df580eda7..6810055592 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html @@ -47,7 +47,7 @@
- {{ 'audit-log.timestamp' | translate }} + {{ 'widgets.table.display-timestamp' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index 90a74585fc..d475dd886d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -513,7 +513,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI let title = ''; const header = this.sources[index].header.find(column => column.index.toString() === value); if (value === '0') { - title = this.translate.instant('audit-log.timestamp'); + title = this.translate.instant('widgets.table.display-timestamp'); } else if (value === 'actions') { title = 'Actions'; } else { From b005b7961ec6b8dd3bbddea75b72972d7dc3273e Mon Sep 17 00:00:00 2001 From: deaflynx Date: Mon, 15 Sep 2025 15:11:43 +0300 Subject: [PATCH 124/216] Version control components: refactor ng-template syntax; outline appearance. --- .../vc/complex-version-create.component.html | 131 ++++++++------- .../vc/complex-version-load.component.html | 100 +++++------ ...entity-types-version-create.component.html | 157 +++++++++--------- .../entity-types-version-create.component.ts | 4 - .../entity-types-version-load.component.html | 127 +++++++------- .../vc/entity-version-create.component.html | 33 ++-- .../vc/entity-version-restore.component.html | 99 +++++------ ...move-other-entities-confirm.component.html | 2 +- .../vc/branch-autocomplete.component.html | 5 +- .../vc/branch-autocomplete.component.ts | 5 +- 10 files changed, 337 insertions(+), 326 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html b/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html index e97e02190a..1e1f89bfb8 100644 --- a/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/complex-version-create.component.html @@ -15,76 +15,79 @@ limitations under the License. --> -
- -

{{ 'version-control.create-entities-version' | translate }}

- -
- - - -
- - - - version-control.version-name - - - {{ 'version-control.version-name-required' | translate }} - +@if (!versionCreateResult$) { +
+ +

{{ 'version-control.create-entities-version' | translate }}

+ +
+ + + +
+ + + + version-control.version-name + + + {{ 'version-control.version-name-required' | translate }} + + +
+ + version-control.default-sync-strategy + + @for (strategy of syncStrategies; track strategy) { + + {{syncStrategyTranslations.get(strategy) | translate}} + + } + + + + + +
+ +
- - version-control.default-sync-strategy - - - {{syncStrategyTranslations.get(strategy) | translate}} - - - - - - - -
- - -
-
-
-
-
-
- -
- +} @else { +
+ @if ((versionCreateResult$ | async)?.done || hasError) { +
+ +
+ } @else {
version-control.creating-version
-
-
+ } +} diff --git a/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.html b/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.html index 17e6663a03..885058c9c9 100644 --- a/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/complex-version-load.component.html @@ -15,59 +15,63 @@ limitations under the License. --> -
- -

{{ 'version-control.restore-entities-from-version' | translate: {versionName} }}

- -
- - -
- - - -
- - {{ 'version-control.rollback-on-error' | translate }} - -
-
- - -
-
-
-
+@if (!versionLoadResult$) { +
+ +

{{ 'version-control.restore-entities-from-version' | translate: {versionName} }}

+ +
+ + +
+ + + +
+ + {{ 'version-control.rollback-on-error' | translate }} + +
+
+ + +
+
+} @else { +
{{ 'version-control.no-entities-restored' | translate }}
-
-
{{ entityTypeLoadResultMessage(entityTypeLoadResult) }}
-
- -
- +
+ @for (entityTypeLoadResult of entityTypeLoadResults; track entityTypeLoadResult.entityType) { +
{{ entityTypeLoadResultMessage(entityTypeLoadResult) }}
+ } + @if ((versionLoadResult$ | async)?.done || hasError) { +
+ +
+ } @else {
version-control.restoring-entities-from-version
-
-
+ } +} diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html index 8fdf22f6cd..343040e782 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html @@ -18,86 +18,89 @@
version-control.entities-to-export
-
- - -
- -
-
{{ entityTypeText(entityTypeFormGroup) }}
-
-
- - -
-
-
- -
- - -
- - version-control.sync-strategy - - - {{ 'version-control.default' | translate }} - - - {{syncStrategyTranslations.get(strategy) | translate}} - - - -
- - {{ 'version-control.export-credentials' | translate }} - - - {{ 'version-control.export-attributes' | translate }} - - - {{ 'version-control.export-relations' | translate }} - - - {{ 'version-control.export-calculated-fields' | translate }} - + @for (entityTypeFormGroup of entityTypesFormGroupArray(); track entityTypeFormGroup; let index = $index, isLast = $last) { +
+ + +
+ +
+
{{ entityTypeText(entityTypeFormGroup) }}
+
+
+ + +
+
+
+ +
+ + +
+ + version-control.sync-strategy + + + {{ 'version-control.default' | translate }} + + @for (strategy of syncStrategies; track strategy) { + + {{syncStrategyTranslations.get(strategy) | translate}} + + } + + +
+ + {{ 'version-control.export-credentials' | translate }} + + + {{ 'version-control.export-attributes' | translate }} + + + {{ 'version-control.export-relations' | translate }} + + + {{ 'version-control.export-calculated-fields' | translate }} + +
+
+ + {{ 'version-control.all-entities' | translate }} + + @if (!entityTypeFormGroup.get('config').get('allEntities').value) { + + + } +
-
- - {{ 'version-control.all-entities' | translate }} - - - -
-
- -
-
- version-control.no-entities-to-export-prompt -
+ +
+ } @empty { + version-control.no-entities-to-export-prompt + }
-
- -
- -
- - -
-
- - {{ 'version-control.remove-other-entities' | translate }} - - - {{ 'version-control.find-existing-entity-by-name' | translate }} - -
-
- - {{ 'version-control.load-credentials' | translate }} - - - {{ 'version-control.load-attributes' | translate }} - - - {{ 'version-control.load-relations' | translate }} - - - {{ 'version-control.load-calculated-fields' | translate }} - + @for (entityTypeFormGroup of entityTypesFormGroupArray(); track entityTypeFormGroup; let index = $index, isLast = $last) { +
+ + +
+ +
+
{{ entityTypeText(entityTypeFormGroup) }}
+
+
+ + +
+
+
+ +
+ + +
+
+ + {{ 'version-control.remove-other-entities' | translate }} + + + {{ 'version-control.find-existing-entity-by-name' | translate }} + +
+
+ + {{ 'version-control.load-credentials' | translate }} + + + {{ 'version-control.load-attributes' | translate }} + + + {{ 'version-control.load-relations' | translate }} + + + {{ 'version-control.load-calculated-fields' | translate }} + +
-
- -
-
- version-control.no-entities-to-restore-prompt -
+ +
+ } @empty { + version-control.no-entities-to-restore-prompt + }
-
-
-
-
+} @else { + @if ((versionCreateResult$ | async)?.done || resultMessage) { +
{{ resultMessage }}
-
- + } @else {
version-control.creating-version
-
-
+ } +} diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html index 1183223850..2936bbf3eb 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html @@ -15,51 +15,53 @@ limitations under the License. --> -
- -

{{ 'version-control.restore-entity-from-version' | translate: {versionName} }}

- -
- - - - -
-
- - {{ 'version-control.load-credentials' | translate }} - - - {{ 'version-control.load-attributes' | translate }} - - - {{ 'version-control.load-relations' | translate }} - - - {{ 'version-control.load-calculated-fields' | translate }} - -
-
- -
- - -
-
-
-
-
+@if (!versionLoadResult$) { + @if (entityDataInfo) { + +

{{ 'version-control.restore-entity-from-version' | translate: {versionName} }}

+ +
+ + +
+
+
+ + {{ 'version-control.load-credentials' | translate }} + + + {{ 'version-control.load-attributes' | translate }} + + + {{ 'version-control.load-relations' | translate }} + + + {{ 'version-control.load-calculated-fields' | translate }} + +
+
+ +
+ + +
+ } @else { + + } +} @else { + @if ((versionLoadResult$ | async)?.done || errorMessage) { +
-
- + } @else {
version-control.restoring-entity-version
-
-
+ } +} diff --git a/ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.html b/ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.html index 71796247dc..6bd46bb9c8 100644 --- a/ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/remove-other-entities-confirm.component.html @@ -19,7 +19,7 @@
- +
diff --git a/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html b/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html index 4515797a85..665b4d63ad 100644 --- a/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/vc/branch-autocomplete.component.html @@ -15,7 +15,10 @@ limitations under the License. --> - + {{ 'version-control.branch' | translate }} Date: Mon, 15 Sep 2025 17:29:13 +0300 Subject: [PATCH 125/216] Version control: minor form style enhancement. --- .../components/vc/entity-types-version-create.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html index 343040e782..b2160e8ab8 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-types-version-create.component.html @@ -89,6 +89,7 @@ From c6786343443b14765d797558c3b1e2921f6464bd Mon Sep 17 00:00:00 2001 From: dshvaika Date: Tue, 16 Sep 2025 10:37:19 +0300 Subject: [PATCH 126/216] fix validation logic of zoneGroupConfiguration after testing --- .../cf/configuration/geofencing/ZoneGroupConfiguration.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java index 43997c23fa..5328dbdbc3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java @@ -53,6 +53,9 @@ public class ZoneGroupConfiguration { if (reportStrategy == null) { throw new IllegalArgumentException("Report strategy must be specified for '" + name + "' zone group!"); } + if (hasDynamicSource()) { + refDynamicSourceConfiguration.validate(); + } if (!createRelationsWithMatchedZones) { return; } @@ -62,9 +65,6 @@ public class ZoneGroupConfiguration { if (direction == null) { throw new IllegalArgumentException("Relation direction must be specified for '" + name + "' zone group!"); } - if (hasDynamicSource()) { - refDynamicSourceConfiguration.validate(); - } } public boolean hasDynamicSource() { From 36f8c9ed554a5d0043bcead524119913f49c5104 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Mon, 15 Sep 2025 15:55:07 +0300 Subject: [PATCH 127/216] fixed bug with displaying decimals in liquid level widget --- .../lib/indicator/liquid-level-widget.component.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts index e3a23673f7..ec4a9a9beb 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts @@ -510,11 +510,10 @@ export class LiquidLevelWidgetComponent implements OnInit { let content: string; let container: JQuery; const jQueryContainerElement = $(this.liquidLevelContent.nativeElement); - let value = 'N/A'; - + let value: number | string = 'N/A'; if (isNumeric(data)) { - value = this.widgetUnitsConvertor(convertLiters(this.convertOutputData(percentage), this.widgetUnits as CapacityUnits, ConversionType.from)) - .toFixed(this.settings.decimals || 0); + value = +this.widgetUnitsConvertor(convertLiters(this.convertOutputData(percentage), this.widgetUnits as CapacityUnits, ConversionType.from)).toFixed(this.ctx.widgetConfig.decimals || 0) + } this.valueColor.update(value); const valueTextStyle = cssTextFromInlineStyle({...inlineTextStyle(this.settings.valueFont), @@ -528,10 +527,9 @@ export class LiquidLevelWidgetComponent implements OnInit { let volume: number | string; if (this.widgetUnits !== CapacityUnits.percent) { const volumeInLiters: number = convertLiters(this.volume, this.volumeUnits as CapacityUnits, ConversionType.to); - volume = this.widgetUnitsConvertor(convertLiters(volumeInLiters, this.widgetUnits as CapacityUnits, ConversionType.from)) - .toFixed(this.settings.decimals || 0); + volume = +this.widgetUnitsConvertor(convertLiters(volumeInLiters, this.widgetUnits as CapacityUnits, ConversionType.from)).toFixed(this.ctx.widgetConfig.decimals || 0) } else { - volume = this.widgetUnitsConvertor(this.volume).toFixed(this.settings.decimals || 0); + volume = +this.widgetUnitsConvertor(this.volume).toFixed(this.ctx.widgetConfig.decimals || 0) } const volumeTextStyle = cssTextFromInlineStyle({...inlineTextStyle(this.settings.volumeFont), From 3d0f643e7a6538708cc5898199b540399fbfeee4 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Tue, 16 Sep 2025 12:59:25 +0300 Subject: [PATCH 128/216] UI: Add deffinition for api usage --- .../lib/cards/api-usage-widget.component.scss | 3 + .../lib/cards/api-usage-widget.component.ts | 32 +- .../api-usage-settings.component.models.ts | 24 +- .../api-usage-widget-settings.component.ts | 24 +- .../api-usage-model.definition.ts | 88 + .../models/widget/widget-model.definition.ts | 4 +- ui-ngx/src/assets/dashboard/api_usage.json | 2440 +++-------------- .../assets/locale/locale.constant-en_US.json | 3 +- 8 files changed, 475 insertions(+), 2143 deletions(-) create mode 100644 ui-ngx/src/app/shared/models/widget/home-widgets/api-usage-model.definition.ts diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.scss index 7cbd58f5f7..fc1247c4f7 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.scss @@ -50,6 +50,7 @@ $warning-color: #FAA405; color: $enabled-color; } .mat-mdc-progress-bar { + --mdc-linear-progress-track-color: #{rgba($enabled-color, 0.06)}; --mdc-linear-progress-active-indicator-color: #{$enabled-color}; } } @@ -58,6 +59,7 @@ $warning-color: #FAA405; color: $disabled-color; } .mat-mdc-progress-bar { + --mdc-linear-progress-track-color: #{rgba($disabled-color, 0.06)}; --mdc-linear-progress-active-indicator-color: #{$disabled-color}; } } @@ -66,6 +68,7 @@ $warning-color: #FAA405; color: $warning-color; } .mat-mdc-progress-bar { + --mdc-linear-progress-track-color: #{rgba($warning-color, 0.06)}; --mdc-linear-progress-active-indicator-color: #{$warning-color}; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts index 1401104352..69f328a006 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/cards/api-usage-widget.component.ts @@ -20,16 +20,16 @@ import { backgroundStyle, ComponentStyle, overlayStyle } from '@shared/models/wi import { Observable } from 'rxjs'; import { ImagePipe } from '@shared/pipe/image.pipe'; import { DomSanitizer } from '@angular/platform-browser'; -import { DataKey, DatasourceType, widgetType } from "@shared/models/widget.models"; -import { WidgetSubscriptionOptions } from "@core/api/widget-api.models"; -import { formattedDataFormDatasourceData } from "@core/utils"; +import { DatasourceType, widgetType } from '@shared/models/widget.models'; +import { WidgetSubscriptionOptions } from '@core/api/widget-api.models'; +import { formattedDataFormDatasourceData } from '@core/utils'; -import { UtilsService } from "@core/services/utils.service"; +import { UtilsService } from '@core/services/utils.service'; import { - ApiUsageDataKeysSettings, apiUsageDefaultSettings, - ApiUsageWidgetSettings -} from "@home/components/widget/lib/settings/cards/api-usage-settings.component.models"; + ApiUsageWidgetSettings, + getUniqueDataKeys +} from '@home/components/widget/lib/settings/cards/api-usage-settings.component.models'; @Component({ selector: 'tb-api-usage-widget', @@ -80,7 +80,7 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy { type: DatasourceType.entity, name: '', entityAliasId: this.settings.dsEntityAliasId, - dataKeys: this.getUniqueDataKeys(this.settings.dataKeys) + dataKeys: getUniqueDataKeys(this.settings.apiUsageDataKeys) } const apiUsageSubscriptionOptions: WidgetSubscriptionOptions = { @@ -122,7 +122,7 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy { } parseApiUsages() { - this.settings.dataKeys.forEach((key) => { + this.settings.apiUsageDataKeys.forEach((key) => { this.apiUsages.push({ label: this.utils.customTranslation(key.label, key.label), state: key.state, @@ -134,20 +134,6 @@ export class ApiUsageWidgetComponent implements OnInit, OnDestroy { }) } - getUniqueDataKeys(data: ApiUsageDataKeysSettings[]): DataKey[] { - const seenNames = new Set(); - return data - .flatMap(item => [item.status, item.maxLimit, item.current]) - .filter(key => { - if (seenNames.has(key.name)) { - return false; - } - seenNames.add(key.name); - return true; - }); - }; - - ngOnDestroy() { if (this.contentResize$) { this.contentResize$.disconnect(); diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-settings.component.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-settings.component.models.ts index 7af2711e8f..7f4312cef9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-settings.component.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-settings.component.models.ts @@ -17,10 +17,10 @@ import { IAliasController } from '@core/api/widget-api.models'; import { WidgetConfigCallbacks } from '@home/components/widget/config/widget-config.component.models'; import { DataKey, Widget, widgetType } from '@shared/models/widget.models'; -import { Observable } from "rxjs"; -import { BackgroundSettings, BackgroundType } from "@shared/models/widget-settings.models"; -import { DataKeyType } from "@shared/models/telemetry/telemetry.models"; -import { materialColors } from "@shared/models/material.models"; +import { Observable } from 'rxjs'; +import { BackgroundSettings, BackgroundType } from '@shared/models/widget-settings.models'; +import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { materialColors } from '@shared/models/material.models'; export interface ApiUsageSettingsContext { aliasController: IAliasController; @@ -33,7 +33,7 @@ export interface ApiUsageSettingsContext { export interface ApiUsageWidgetSettings { dsEntityAliasId: string; - dataKeys: ApiUsageDataKeysSettings[]; + apiUsageDataKeys: ApiUsageDataKeysSettings[]; targetDashboardState: string; background: BackgroundSettings; padding: string; @@ -80,7 +80,7 @@ const generateDataKey = (label: string, status: string, maxLimit: string, curren export const apiUsageDefaultSettings: ApiUsageWidgetSettings = { dsEntityAliasId: '', - dataKeys: [ + apiUsageDataKeys: [ generateDataKey('{i18n:api-usage.transport-messages}', 'transportApiState', 'transportMsgLimit', 'transportMsgCount'), generateDataKey('{i18n:api-usage.transport-data-points}', 'transportApiState', 'transportDataPointsLimit', 'transportDataPointsCount'), generateDataKey('{i18n:api-usage.rule-engine-executions}', 'ruleEngineApiState', 'ruleEngineExecutionLimit', 'ruleEngineExecutionCount'), @@ -104,3 +104,15 @@ export const apiUsageDefaultSettings: ApiUsageWidgetSettings = { padding: '0' }; +export const getUniqueDataKeys = (data: ApiUsageDataKeysSettings[]): DataKey[] => { + const seenNames = new Set(); + return data + .flatMap(item => [item.status, item.maxLimit, item.current]) + .filter(key => { + if (seenNames.has(key.name)) { + return false; + } + seenNames.add(key.name); + return true; + }); +}; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts index 16fca5a94b..1d42b1503c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/api-usage-widget-settings.component.ts @@ -37,15 +37,15 @@ import { ApiUsageDataKeysSettings, apiUsageDefaultSettings, ApiUsageSettingsContext -} from "@home/components/widget/lib/settings/cards/api-usage-settings.component.models"; -import { deepClone } from "@core/utils"; -import { Observable, of } from "rxjs"; +} from '@home/components/widget/lib/settings/cards/api-usage-settings.component.models'; +import { deepClone } from '@core/utils'; +import { Observable, of } from 'rxjs'; import { DataKeyConfigDialogComponent, DataKeyConfigDialogData -} from "@home/components/widget/lib/settings/common/key/data-key-config-dialog.component"; -import { MatDialog } from "@angular/material/dialog"; -import { CdkDragDrop } from "@angular/cdk/drag-drop"; +} from '@home/components/widget/lib/settings/common/key/data-key-config-dialog.component'; +import { MatDialog } from '@angular/material/dialog'; +import { CdkDragDrop } from '@angular/cdk/drag-drop'; @Component({ selector: 'tb-api-usage-widget-settings', @@ -82,11 +82,11 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent { } protected doUpdateSettings(settingsForm: UntypedFormGroup, settings: WidgetSettings) { - settingsForm.setControl('dataKeys', this.prepareDataKeysFormArray(settings?.dataKeys), {emitEvent: false}); + settingsForm.setControl('apiUsageDataKeys', this.prepareDataKeysFormArray(settings?.apiUsageDataKeys), {emitEvent: false}); } dataKeysFormArray(): UntypedFormArray { - return this.apiUsageWidgetSettingsForm.get('dataKeys') as UntypedFormArray; + return this.apiUsageWidgetSettingsForm.get('apiUsageDataKeys') as UntypedFormArray; } trackByDataKey(index: number): any { @@ -104,7 +104,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent { } removeDataKey(index: number) { - (this.apiUsageWidgetSettingsForm.get('dataKeys') as UntypedFormArray).removeAt(index); + (this.apiUsageWidgetSettingsForm.get('apiUsageDataKeys') as UntypedFormArray).removeAt(index); } addDataKey() { @@ -115,7 +115,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent { maxLimit: null, current: null }; - const dataKeysArray = this.apiUsageWidgetSettingsForm.get('dataKeys') as UntypedFormArray; + const dataKeysArray = this.apiUsageWidgetSettingsForm.get('apiUsageDataKeys') as UntypedFormArray; const dataKeyControl = this.fb.control(dataKey, [this.apiUsageDataKeyValidator()]); dataKeysArray.push(dataKeyControl); } @@ -131,7 +131,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent { protected prepareInputSettings(settings: WidgetSettings): WidgetSettings { return { dsEntityAliasId: settings?.dsEntityAliasId, - dataKeys: settings?.dataKeys, + apiUsageDataKeys: settings?.apiUsageDataKeys, targetDashboardState: settings?.targetDashboardState, background: settings?.background, padding: settings.padding @@ -141,7 +141,7 @@ export class ApiUsageWidgetSettingsComponent extends WidgetSettingsComponent { protected onSettingsSet(settings: WidgetSettings) { this.apiUsageWidgetSettingsForm = this.fb.group({ dsEntityAliasId: [settings?.dsEntityAliasId], - dataKeys: this.prepareDataKeysFormArray(settings?.dataKeys), + apiUsageDataKeys: this.prepareDataKeysFormArray(settings?.apiUsageDataKeys), targetDashboardState: [settings?.targetDashboardState], background: [settings?.background, []], padding: [settings.padding, []] diff --git a/ui-ngx/src/app/shared/models/widget/home-widgets/api-usage-model.definition.ts b/ui-ngx/src/app/shared/models/widget/home-widgets/api-usage-model.definition.ts new file mode 100644 index 0000000000..5c92b84cbe --- /dev/null +++ b/ui-ngx/src/app/shared/models/widget/home-widgets/api-usage-model.definition.ts @@ -0,0 +1,88 @@ +/// +/// 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. +/// + +import { EntityAliases, EntityAliasInfo, getEntityAliasId } from '@shared/models/alias.models'; +import { FilterInfo, Filters } from '@shared/models/query/query.models'; +import { Dashboard } from '@shared/models/dashboard.models'; +import { Datasource, DatasourceType, Widget } from '@shared/models/widget.models'; +import { WidgetModelDefinition } from '@shared/models/widget/widget-model.definition'; +import { + ApiUsageWidgetSettings, + getUniqueDataKeys +} from '@home/components/widget/lib/settings/cards/api-usage-settings.component.models'; + +interface AliasFilterPair { + alias?: EntityAliasInfo; + filter?: FilterInfo; +} + +interface ApiUsageDatasourcesInfo { + ds?: AliasFilterPair; +} + +export const ApiUsageModelDefinition: WidgetModelDefinition = { + testWidget(widget: Widget): boolean { + if (widget?.config?.settings) { + const settings = widget.config.settings; + if (settings.apiUsageDataKeys && Array.isArray(settings.apiUsageDataKeys)) { + return true; + } + } + return false; + }, + prepareExportInfo(dashboard: Dashboard, widget: Widget): ApiUsageDatasourcesInfo { + const settings: ApiUsageWidgetSettings = widget.config.settings as ApiUsageWidgetSettings; + const info: ApiUsageDatasourcesInfo = {}; + if (settings.dsEntityAliasId) { + info.ds = prepareExportDataSourcesInfo(dashboard, settings.dsEntityAliasId); + } + return info; + }, + updateFromExportInfo(widget: Widget, entityAliases: EntityAliases, filters: Filters, info: ApiUsageDatasourcesInfo): void { + const settings: ApiUsageWidgetSettings = widget.config.settings as ApiUsageWidgetSettings; + if (info?.ds?.alias) { + settings.dsEntityAliasId = getEntityAliasId(entityAliases, info.ds.alias); + } + }, + datasources(widget: Widget): Datasource[] { + const settings: ApiUsageWidgetSettings = widget.config.settings as ApiUsageWidgetSettings; + const datasources: Datasource[] = []; + if (settings.apiUsageDataKeys?.length && settings.dsEntityAliasId) { + datasources.push({ + type: DatasourceType.entity, + name: '', + entityAliasId: settings.dsEntityAliasId, + dataKeys: getUniqueDataKeys(settings.apiUsageDataKeys) + }); + } + return datasources; + }, + hasTimewindow(): boolean { + return false; + } +}; + +const prepareExportDataSourcesInfo = (dashboard: Dashboard, settings: string): AliasFilterPair => { + const aliasAndFilter: AliasFilterPair = {}; + const entityAlias = dashboard.configuration.entityAliases[settings]; + if (entityAlias) { + aliasAndFilter.alias = { + alias: entityAlias.alias, + filter: entityAlias.filter + }; + } + return aliasAndFilter; +} diff --git a/ui-ngx/src/app/shared/models/widget/widget-model.definition.ts b/ui-ngx/src/app/shared/models/widget/widget-model.definition.ts index 21dc801b3f..ccec658766 100644 --- a/ui-ngx/src/app/shared/models/widget/widget-model.definition.ts +++ b/ui-ngx/src/app/shared/models/widget/widget-model.definition.ts @@ -19,6 +19,7 @@ import { Dashboard } from '@shared/models/dashboard.models'; import { EntityAliases } from '@shared/models/alias.models'; import { Filters } from '@shared/models/query/query.models'; import { MapModelDefinition } from '@shared/models/widget/maps/map-model.definition'; +import { ApiUsageModelDefinition } from '@shared/models/widget/home-widgets/api-usage-model.definition'; export interface WidgetModelDefinition { testWidget(widget: Widget): boolean; @@ -29,7 +30,8 @@ export interface WidgetModelDefinition { } const widgetModelRegistry: WidgetModelDefinition[] = [ - MapModelDefinition + MapModelDefinition, + ApiUsageModelDefinition ]; export const findWidgetModelDefinition = (widget: Widget): WidgetModelDefinition => { diff --git a/ui-ngx/src/assets/dashboard/api_usage.json b/ui-ngx/src/assets/dashboard/api_usage.json index 3fc49e171f..92c9bdb054 100644 --- a/ui-ngx/src/assets/dashboard/api_usage.json +++ b/ui-ngx/src/assets/dashboard/api_usage.json @@ -1216,35 +1216,16 @@ } ], "timewindow": { - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, "selectedTab": 0, "realtime": { "realtimeType": 0, "interval": 3600000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": null, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false + "timewindowMs": 86400000 }, "aggregation": { - "type": "SUM", + "type": "NONE", "limit": 50000 - }, - "timezone": null + } }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -1589,35 +1570,16 @@ } ], "timewindow": { - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, "selectedTab": 0, "realtime": { "realtimeType": 0, "interval": 3600000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": null, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false + "timewindowMs": 86400000 }, "aggregation": { - "type": "SUM", + "type": "NONE", "limit": 50000 - }, - "timezone": null + } }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -1998,35 +1960,16 @@ } ], "timewindow": { - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, "selectedTab": 0, "realtime": { "realtimeType": 0, "interval": 3600000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_YEAR", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": null, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false + "timewindowMs": 86400000 }, "aggregation": { - "type": "SUM", + "type": "NONE", "limit": 50000 - }, - "timezone": null + } }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -2418,35 +2361,16 @@ } ], "timewindow": { - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, "selectedTab": 0, "realtime": { "realtimeType": 0, "interval": 3600000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": null, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false + "timewindowMs": 86400000 }, "aggregation": { - "type": "SUM", + "type": "NONE", "limit": 50000 - }, - "timezone": null + } }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -2680,7 +2604,7 @@ "tooltipHideZeroValues": null, "padding": "12px" }, - "title": "{i18n:api-usage.telemetry-persistence-hourly-activity}", + "title": "{i18n:api-usage.data-points-storage-days-hourly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -2733,7 +2657,7 @@ "col": 0, "id": "5d0f2f57-499d-1324-8e1b-cfbc0b3149d2" }, - "51608a74-f213-d8c9-8df8-b42238ef93a6": { + "fb155957-1af4-233e-e2fb-09e648e75d6e": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -2794,26 +2718,16 @@ "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, - "selectedTab": 0, - "realtime": { - "realtimeType": 0, - "interval": 3600000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, + "selectedTab": 1, "history": { "historyType": 0, - "interval": 86400000, "timewindowMs": 2592000000, - "fixedTimewindow": null, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { "type": "SUM", @@ -3053,7 +2967,7 @@ "tooltipHideZeroValues": null, "padding": "12px" }, - "title": "{i18n:api-usage.transport-msg-hourly-activity}", + "title": "{i18n:api-usage.transport-msg-daily-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -3102,9 +3016,9 @@ }, "row": 0, "col": 0, - "id": "51608a74-f213-d8c9-8df8-b42238ef93a6" + "id": "fb155957-1af4-233e-e2fb-09e648e75d6e" }, - "fb155957-1af4-233e-e2fb-09e648e75d6e": { + "4817e33b-87be-5be3-eaca-ca68a2eb4e0c": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -3153,12 +3067,7 @@ "usePostProcessing": null, "postFuncBody": null } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - } + ] } ], "timewindow": { @@ -3166,18 +3075,28 @@ "hideAggInterval": false, "hideTimezone": false, "selectedTab": 1, + "realtime": { + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, - "timewindowMs": 2592000000, - "interval": 86400000, - "fixedTimewindow": { - "startTimeMs": 1709729389667, - "endTimeMs": 1709815789667 - }, - "quickInterval": "CURRENT_DAY" + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { - "type": "SUM", + "type": "NONE", "limit": 25000 }, "timezone": null @@ -3414,7 +3333,7 @@ "tooltipHideZeroValues": null, "padding": "12px" }, - "title": "{i18n:api-usage.transport-msg-daily-activity}", + "title": "{i18n:api-usage.transport-msg-monthly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -3463,9 +3382,9 @@ }, "row": 0, "col": 0, - "id": "fb155957-1af4-233e-e2fb-09e648e75d6e" + "id": "4817e33b-87be-5be3-eaca-ca68a2eb4e0c" }, - "4817e33b-87be-5be3-eaca-ca68a2eb4e0c": { + "79056202-c92b-1dae-ce49-318ec52e2d3b": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -3479,10 +3398,10 @@ "filterId": null, "dataKeys": [ { - "name": "transportMsgCountHourly", + "name": "transportDataPointsCountHourly", "type": "timeseries", - "label": "{i18n:api-usage.transport-messages}", - "color": "#2196f3", + "label": "{i18n:api-usage.transport-data-points}", + "color": "#4CAF50", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -3505,16 +3424,23 @@ "comparisonSettings": { "showValuesForComparison": true }, - "type": "bar" + "type": "bar", + "yAxisId": "default" }, "_hash": 0.0661644137210089, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null + "postFuncBody": null, + "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { @@ -3522,28 +3448,18 @@ "hideAggInterval": false, "hideTimezone": false, "selectedTab": 1, - "realtime": { - "realtimeType": 0, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, "history": { "historyType": 0, - "interval": 2592000000, - "timewindowMs": 31536000000, - "fixedTimewindow": null, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { - "type": "NONE", + "type": "SUM", "limit": 25000 }, "timezone": null @@ -3780,7 +3696,7 @@ "tooltipHideZeroValues": null, "padding": "12px" }, - "title": "{i18n:api-usage.transport-msg-monthly-activity}", + "title": "{i18n:api-usage.transport-data-points-daily-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -3829,9 +3745,9 @@ }, "row": 0, "col": 0, - "id": "4817e33b-87be-5be3-eaca-ca68a2eb4e0c" + "id": "79056202-c92b-1dae-ce49-318ec52e2d3b" }, - "9e00cc90-520d-2108-1d2f-bba68ed5cbf1": { + "966ffee7-ba0d-8e54-f903-e8d015ca8cd2": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -3850,1599 +3766,9 @@ "label": "{i18n:api-usage.transport-data-points}", "color": "#4CAF50", "settings": { - "excludeFromStacking": false, - "hideDataByDefault": false, - "disableDataHiding": false, - "removeFromLegend": false, - "showLines": false, - "fillLines": false, - "showPoints": false, - "showPointShape": "circle", - "pointShapeFormatter": "", - "showPointsLineWidth": 5, - "showPointsRadius": 3, - "showSeparateAxis": false, - "axisPosition": "left", - "thresholds": [ - { - "thresholdValueSource": "predefinedValue" - } - ], - "comparisonSettings": { - "showValuesForComparison": true - }, - "type": "bar", - "yAxisId": "default" - }, - "_hash": 0.0661644137210089, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null, - "aggregationType": null - } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - } - } - ], - "timewindow": { - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, - "selectedTab": 0, - "realtime": { - "realtimeType": 0, - "interval": 3600000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 86400000, - "timewindowMs": 2592000000, - "fixedTimewindow": null, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "SUM", - "limit": 25000 - }, - "timezone": null - }, - "showTitle": true, - "backgroundColor": "#FFFFFF", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "0px", - "settings": { - "yAxes": { - "default": { - "units": null, - "decimals": 0, - "show": true, - "label": "", - "labelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "600", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.54)", - "position": "left", - "showTickLabels": true, - "tickLabelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "tickLabelColor": "rgba(0, 0, 0, 0.54)", - "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", - "showTicks": true, - "ticksColor": "rgba(0, 0, 0, 0.54)", - "showLine": true, - "lineColor": "rgba(0, 0, 0, 0.54)", - "showSplitLines": true, - "splitLinesColor": "rgba(0, 0, 0, 0.12)", - "id": "default", - "order": 0, - "min": null, - "max": null - } - }, - "thresholds": [], - "dataZoom": false, - "stack": false, - "xAxis": { - "show": true, - "label": "", - "labelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "600", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.54)", - "position": "bottom", - "showTickLabels": true, - "tickLabelFont": { - "family": "Roboto", - "size": 10, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "tickLabelColor": "rgba(0, 0, 0, 0.54)", - "ticksFormat": {}, - "showTicks": true, - "ticksColor": "rgba(0, 0, 0, 0.54)", - "showLine": true, - "lineColor": "rgba(0, 0, 0, 0.54)", - "showSplitLines": true, - "splitLinesColor": "rgba(0, 0, 0, 0.12)" - }, - "noAggregationBarWidthSettings": { - "strategy": "group", - "groupWidth": { - "relative": true, - "relativeWidth": 6, - "absoluteWidth": 1800000 - }, - "barWidth": { - "relative": true, - "relativeWidth": 2, - "absoluteWidth": 1000 - } - }, - "showLegend": true, - "legendLabelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "16px" - }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", - "legendConfig": { - "direction": "column", - "position": "bottom", - "sortDataKeys": false, - "showMin": false, - "showMax": false, - "showAvg": false, - "showTotal": true, - "showLatest": false, - "valueFormat": null - }, - "showTooltip": true, - "tooltipTrigger": "axis", - "tooltipValueFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "500", - "lineHeight": "16px" - }, - "tooltipValueColor": "rgba(0, 0, 0, 0.76)", - "tooltipShowDate": true, - "tooltipDateFormat": { - "format": "yyyy-MM-dd HH:mm:ss", - "lastUpdateAgo": false, - "custom": false, - "auto": true, - "autoDateFormatSettings": {} - }, - "tooltipDateFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "16px" - }, - "tooltipDateColor": "rgba(0, 0, 0, 0.76)", - "tooltipDateInterval": true, - "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", - "tooltipBackgroundBlur": 4, - "animation": { - "animation": true, - "animationThreshold": 2000, - "animationDuration": 1000, - "animationEasing": "cubicOut", - "animationDelay": 0, - "animationDurationUpdate": 300, - "animationEasingUpdate": "cubicOut", - "animationDelayUpdate": 0 - }, - "background": { - "type": "color", - "color": "#fff", - "overlay": { - "enabled": false, - "color": "rgba(255,255,255,0.72)", - "blur": 3 - } - }, - "comparisonEnabled": false, - "timeForComparison": "previousInterval", - "comparisonCustomIntervalValue": 7200000, - "comparisonXAxis": { - "show": true, - "label": "", - "labelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "600", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.54)", - "position": "top", - "showTickLabels": true, - "tickLabelFont": { - "family": "Roboto", - "size": 10, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "tickLabelColor": "rgba(0, 0, 0, 0.54)", - "ticksFormat": {}, - "showTicks": true, - "ticksColor": "rgba(0, 0, 0, 0.54)", - "showLine": true, - "lineColor": "rgba(0, 0, 0, 0.54)", - "showSplitLines": true, - "splitLinesColor": "rgba(0, 0, 0, 0.12)" - }, - "grid": { - "show": false, - "backgroundColor": null, - "borderWidth": 1, - "borderColor": "#ccc" - }, - "legendColumnTitleFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "16px" - }, - "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", - "legendValueFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "500", - "lineHeight": "16px" - }, - "legendValueColor": "rgba(0, 0, 0, 0.87)", - "tooltipLabelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "16px" - }, - "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", - "tooltipHideZeroValues": null, - "padding": "12px" - }, - "title": "{i18n:api-usage.transport-data-points-hourly-activity}", - "dropShadow": true, - "enableFullscreen": true, - "titleStyle": null, - "configMode": "basic", - "actions": {}, - "showTitleIcon": false, - "titleIcon": "thermostat", - "iconColor": "#1F6BDD", - "useDashboardTimewindow": false, - "displayTimewindow": true, - "titleFont": { - "size": 16, - "sizeUnit": "px", - "family": "Roboto", - "weight": "500", - "style": "normal", - "lineHeight": "24px" - }, - "titleColor": "rgba(0, 0, 0, 0.87)", - "titleTooltip": "", - "widgetStyle": {}, - "widgetCss": "", - "pageSize": 1024, - "units": "", - "decimals": null, - "noDataDisplayMessage": "", - "timewindowStyle": { - "showIcon": false, - "iconSize": "24px", - "icon": null, - "iconPosition": "left", - "font": { - "size": 12, - "sizeUnit": "px", - "family": "Roboto", - "weight": "400", - "style": "normal", - "lineHeight": "16px" - }, - "color": "rgba(0, 0, 0, 0.38)", - "displayTypePrefix": true - }, - "margin": "0px", - "borderRadius": "4px", - "iconSize": "0px" - }, - "row": 0, - "col": 0, - "id": "9e00cc90-520d-2108-1d2f-bba68ed5cbf1" - }, - "79056202-c92b-1dae-ce49-318ec52e2d3b": { - "typeFullFqn": "system.time_series_chart", - "type": "timeseries", - "sizeX": 8, - "sizeY": 5, - "config": { - "datasources": [ - { - "type": "entity", - "name": null, - "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", - "filterId": null, - "dataKeys": [ - { - "name": "transportDataPointsCountHourly", - "type": "timeseries", - "label": "{i18n:api-usage.transport-data-points}", - "color": "#4CAF50", - "settings": { - "excludeFromStacking": false, - "hideDataByDefault": false, - "disableDataHiding": false, - "removeFromLegend": false, - "showLines": false, - "fillLines": false, - "showPoints": false, - "showPointShape": "circle", - "pointShapeFormatter": "", - "showPointsLineWidth": 5, - "showPointsRadius": 3, - "showSeparateAxis": false, - "axisPosition": "left", - "thresholds": [ - { - "thresholdValueSource": "predefinedValue" - } - ], - "comparisonSettings": { - "showValuesForComparison": true - }, - "type": "bar", - "yAxisId": "default" - }, - "_hash": 0.0661644137210089, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null, - "aggregationType": null - } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - } - } - ], - "timewindow": { - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, - "selectedTab": 1, - "history": { - "historyType": 0, - "timewindowMs": 2592000000, - "interval": 86400000, - "fixedTimewindow": { - "startTimeMs": 1709729389667, - "endTimeMs": 1709815789667 - }, - "quickInterval": "CURRENT_DAY" - }, - "aggregation": { - "type": "SUM", - "limit": 25000 - }, - "timezone": null - }, - "showTitle": true, - "backgroundColor": "#FFFFFF", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "0px", - "settings": { - "yAxes": { - "default": { - "units": null, - "decimals": 0, - "show": true, - "label": "", - "labelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "600", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.54)", - "position": "left", - "showTickLabels": true, - "tickLabelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "tickLabelColor": "rgba(0, 0, 0, 0.54)", - "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", - "showTicks": true, - "ticksColor": "rgba(0, 0, 0, 0.54)", - "showLine": true, - "lineColor": "rgba(0, 0, 0, 0.54)", - "showSplitLines": true, - "splitLinesColor": "rgba(0, 0, 0, 0.12)", - "id": "default", - "order": 0, - "min": null, - "max": null - } - }, - "thresholds": [], - "dataZoom": false, - "stack": false, - "xAxis": { - "show": true, - "label": "", - "labelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "600", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.54)", - "position": "bottom", - "showTickLabels": true, - "tickLabelFont": { - "family": "Roboto", - "size": 10, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "tickLabelColor": "rgba(0, 0, 0, 0.54)", - "ticksFormat": {}, - "showTicks": true, - "ticksColor": "rgba(0, 0, 0, 0.54)", - "showLine": true, - "lineColor": "rgba(0, 0, 0, 0.54)", - "showSplitLines": true, - "splitLinesColor": "rgba(0, 0, 0, 0.12)" - }, - "noAggregationBarWidthSettings": { - "strategy": "group", - "groupWidth": { - "relative": true, - "relativeWidth": 6, - "absoluteWidth": 1800000 - }, - "barWidth": { - "relative": true, - "relativeWidth": 2, - "absoluteWidth": 1000 - } - }, - "showLegend": true, - "legendLabelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "16px" - }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", - "legendConfig": { - "direction": "column", - "position": "bottom", - "sortDataKeys": false, - "showMin": false, - "showMax": false, - "showAvg": false, - "showTotal": true, - "showLatest": false, - "valueFormat": null - }, - "showTooltip": true, - "tooltipTrigger": "axis", - "tooltipValueFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "500", - "lineHeight": "16px" - }, - "tooltipValueColor": "rgba(0, 0, 0, 0.76)", - "tooltipShowDate": true, - "tooltipDateFormat": { - "format": "yyyy-MM-dd HH:mm:ss", - "lastUpdateAgo": false, - "custom": false, - "auto": true, - "autoDateFormatSettings": {} - }, - "tooltipDateFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "16px" - }, - "tooltipDateColor": "rgba(0, 0, 0, 0.76)", - "tooltipDateInterval": true, - "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", - "tooltipBackgroundBlur": 4, - "animation": { - "animation": true, - "animationThreshold": 2000, - "animationDuration": 1000, - "animationEasing": "cubicOut", - "animationDelay": 0, - "animationDurationUpdate": 300, - "animationEasingUpdate": "cubicOut", - "animationDelayUpdate": 0 - }, - "background": { - "type": "color", - "color": "#fff", - "overlay": { - "enabled": false, - "color": "rgba(255,255,255,0.72)", - "blur": 3 - } - }, - "comparisonEnabled": false, - "timeForComparison": "previousInterval", - "comparisonCustomIntervalValue": 7200000, - "comparisonXAxis": { - "show": true, - "label": "", - "labelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "600", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.54)", - "position": "top", - "showTickLabels": true, - "tickLabelFont": { - "family": "Roboto", - "size": 10, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "tickLabelColor": "rgba(0, 0, 0, 0.54)", - "ticksFormat": {}, - "showTicks": true, - "ticksColor": "rgba(0, 0, 0, 0.54)", - "showLine": true, - "lineColor": "rgba(0, 0, 0, 0.54)", - "showSplitLines": true, - "splitLinesColor": "rgba(0, 0, 0, 0.12)" - }, - "grid": { - "show": false, - "backgroundColor": null, - "borderWidth": 1, - "borderColor": "#ccc" - }, - "legendColumnTitleFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "16px" - }, - "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", - "legendValueFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "500", - "lineHeight": "16px" - }, - "legendValueColor": "rgba(0, 0, 0, 0.87)", - "tooltipLabelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "16px" - }, - "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", - "tooltipHideZeroValues": null, - "padding": "12px" - }, - "title": "{i18n:api-usage.transport-data-points-daily-activity}", - "dropShadow": true, - "enableFullscreen": true, - "titleStyle": null, - "configMode": "basic", - "actions": {}, - "showTitleIcon": false, - "titleIcon": "thermostat", - "iconColor": "#1F6BDD", - "useDashboardTimewindow": false, - "displayTimewindow": true, - "titleFont": { - "size": 16, - "sizeUnit": "px", - "family": "Roboto", - "weight": "500", - "style": "normal", - "lineHeight": "24px" - }, - "titleColor": "rgba(0, 0, 0, 0.87)", - "titleTooltip": "", - "widgetStyle": {}, - "widgetCss": "", - "pageSize": 1024, - "units": "", - "decimals": null, - "noDataDisplayMessage": "", - "timewindowStyle": { - "showIcon": false, - "iconSize": "24px", - "icon": null, - "iconPosition": "left", - "font": { - "size": 12, - "sizeUnit": "px", - "family": "Roboto", - "weight": "400", - "style": "normal", - "lineHeight": "16px" - }, - "color": "rgba(0, 0, 0, 0.38)", - "displayTypePrefix": true - }, - "margin": "0px", - "borderRadius": "4px", - "iconSize": "0px" - }, - "row": 0, - "col": 0, - "id": "79056202-c92b-1dae-ce49-318ec52e2d3b" - }, - "966ffee7-ba0d-8e54-f903-e8d015ca8cd2": { - "typeFullFqn": "system.time_series_chart", - "type": "timeseries", - "sizeX": 8, - "sizeY": 5, - "config": { - "datasources": [ - { - "type": "entity", - "name": null, - "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", - "filterId": null, - "dataKeys": [ - { - "name": "transportDataPointsCountHourly", - "type": "timeseries", - "label": "{i18n:api-usage.transport-data-points}", - "color": "#4CAF50", - "settings": { - "yAxisId": "default", - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "bar", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "enablePointLabelBackground": false, - "pointLabelBackground": "rgba(255,255,255,0.56)", - "pointShape": "emptyCircle", - "pointSize": 4, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "enableLabelBackground": false, - "labelBackground": "rgba(255,255,255,0.56)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - }, - "comparisonSettings": { - "showValuesForComparison": false, - "comparisonValuesLabel": "", - "color": "" - } - }, - "_hash": 0.12814821361119078, - "aggregationType": null, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - } - } - ], - "timewindow": { - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, - "selectedTab": 1, - "realtime": { - "realtimeType": 0, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 2592000000, - "timewindowMs": 31536000000, - "fixedTimewindow": null, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "NONE", - "limit": 25000 - }, - "timezone": null - }, - "showTitle": true, - "backgroundColor": "#FFFFFF", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "0px", - "settings": { - "yAxes": { - "default": { - "units": null, - "decimals": 0, - "show": true, - "label": "", - "labelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "600", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.54)", - "position": "left", - "showTickLabels": true, - "tickLabelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "tickLabelColor": "rgba(0, 0, 0, 0.54)", - "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", - "showTicks": true, - "ticksColor": "rgba(0, 0, 0, 0.54)", - "showLine": true, - "lineColor": "rgba(0, 0, 0, 0.54)", - "showSplitLines": true, - "splitLinesColor": "rgba(0, 0, 0, 0.12)", - "id": "default", - "order": 0, - "min": null, - "max": null - } - }, - "thresholds": [], - "dataZoom": false, - "stack": false, - "xAxis": { - "show": true, - "label": "", - "labelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "600", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.54)", - "position": "bottom", - "showTickLabels": true, - "tickLabelFont": { - "family": "Roboto", - "size": 10, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "tickLabelColor": "rgba(0, 0, 0, 0.54)", - "ticksFormat": {}, - "showTicks": true, - "ticksColor": "rgba(0, 0, 0, 0.54)", - "showLine": true, - "lineColor": "rgba(0, 0, 0, 0.54)", - "showSplitLines": true, - "splitLinesColor": "rgba(0, 0, 0, 0.12)" - }, - "noAggregationBarWidthSettings": { - "strategy": "group", - "groupWidth": { - "relative": true, - "relativeWidth": 6, - "absoluteWidth": 1800000 - }, - "barWidth": { - "relative": true, - "relativeWidth": 2, - "absoluteWidth": 1000 - } - }, - "showLegend": true, - "legendLabelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "16px" - }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", - "legendConfig": { - "direction": "column", - "position": "bottom", - "sortDataKeys": false, - "showMin": false, - "showMax": false, - "showAvg": false, - "showTotal": true, - "showLatest": false, - "valueFormat": null - }, - "showTooltip": true, - "tooltipTrigger": "axis", - "tooltipValueFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "500", - "lineHeight": "16px" - }, - "tooltipValueColor": "rgba(0, 0, 0, 0.76)", - "tooltipShowDate": true, - "tooltipDateFormat": { - "format": "yyyy-MM-dd HH:mm:ss", - "lastUpdateAgo": false, - "custom": false, - "auto": true, - "autoDateFormatSettings": {} - }, - "tooltipDateFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "16px" - }, - "tooltipDateColor": "rgba(0, 0, 0, 0.76)", - "tooltipDateInterval": true, - "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", - "tooltipBackgroundBlur": 4, - "animation": { - "animation": true, - "animationThreshold": 2000, - "animationDuration": 1000, - "animationEasing": "cubicOut", - "animationDelay": 0, - "animationDurationUpdate": 300, - "animationEasingUpdate": "cubicOut", - "animationDelayUpdate": 0 - }, - "background": { - "type": "color", - "color": "#fff", - "overlay": { - "enabled": false, - "color": "rgba(255,255,255,0.72)", - "blur": 3 - } - }, - "comparisonEnabled": false, - "timeForComparison": "previousInterval", - "comparisonCustomIntervalValue": 7200000, - "comparisonXAxis": { - "show": true, - "label": "", - "labelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "600", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.54)", - "position": "top", - "showTickLabels": true, - "tickLabelFont": { - "family": "Roboto", - "size": 10, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "tickLabelColor": "rgba(0, 0, 0, 0.54)", - "ticksFormat": {}, - "showTicks": true, - "ticksColor": "rgba(0, 0, 0, 0.54)", - "showLine": true, - "lineColor": "rgba(0, 0, 0, 0.54)", - "showSplitLines": true, - "splitLinesColor": "rgba(0, 0, 0, 0.12)" - }, - "grid": { - "show": false, - "backgroundColor": null, - "borderWidth": 1, - "borderColor": "#ccc" - }, - "legendColumnTitleFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "16px" - }, - "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", - "legendValueFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "500", - "lineHeight": "16px" - }, - "legendValueColor": "rgba(0, 0, 0, 0.87)", - "tooltipLabelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "16px" - }, - "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", - "tooltipHideZeroValues": null, - "padding": "12px" - }, - "title": "{i18n:api-usage.transport-data-points-monthly-activity}", - "dropShadow": true, - "enableFullscreen": true, - "titleStyle": null, - "configMode": "basic", - "actions": {}, - "showTitleIcon": false, - "titleIcon": "thermostat", - "iconColor": "#1F6BDD", - "useDashboardTimewindow": false, - "displayTimewindow": true, - "titleFont": { - "size": 16, - "sizeUnit": "px", - "family": "Roboto", - "weight": "500", - "style": "normal", - "lineHeight": "24px" - }, - "titleColor": "rgba(0, 0, 0, 0.87)", - "titleTooltip": "", - "widgetStyle": {}, - "widgetCss": "", - "pageSize": 1024, - "units": "", - "decimals": null, - "noDataDisplayMessage": "", - "timewindowStyle": { - "showIcon": false, - "iconSize": "24px", - "icon": null, - "iconPosition": "left", - "font": { - "size": 12, - "sizeUnit": "px", - "family": "Roboto", - "weight": "400", - "style": "normal", - "lineHeight": "16px" - }, - "color": "rgba(0, 0, 0, 0.38)", - "displayTypePrefix": true - }, - "margin": "0px", - "borderRadius": "4px", - "iconSize": "0px" - }, - "row": 0, - "col": 0, - "id": "966ffee7-ba0d-8e54-f903-e8d015ca8cd2" - }, - "b1a9a51f-e5a6-9d5f-ef5c-25c2a68af1b0": { - "typeFullFqn": "system.time_series_chart", - "type": "timeseries", - "sizeX": 8, - "sizeY": 5, - "config": { - "datasources": [ - { - "type": "entity", - "name": null, - "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", - "filterId": null, - "dataKeys": [ - { - "name": "ruleEngineExecutionCountHourly", - "type": "timeseries", - "label": "{i18n:api-usage.rule-engine-executions}", - "color": "#AB00FF", - "settings": { - "yAxisId": "default", - "showInLegend": true, - "dataHiddenByDefault": false, - "type": "bar", - "lineSettings": { - "showLine": true, - "step": false, - "stepType": "start", - "smooth": false, - "lineType": "solid", - "lineWidth": 2, - "showPoints": false, - "showPointLabel": false, - "pointLabelPosition": "top", - "pointLabelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "pointLabelColor": "rgba(0, 0, 0, 0.76)", - "enablePointLabelBackground": false, - "pointLabelBackground": "rgba(255,255,255,0.56)", - "pointShape": "emptyCircle", - "pointSize": 4, - "fillAreaSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - }, - "barSettings": { - "showBorder": false, - "borderWidth": 2, - "borderRadius": 0, - "showLabel": false, - "labelPosition": "top", - "labelFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.76)", - "enableLabelBackground": false, - "labelBackground": "rgba(255,255,255,0.56)", - "backgroundSettings": { - "type": "none", - "opacity": 0.4, - "gradient": { - "start": 100, - "end": 0 - } - } - }, - "comparisonSettings": { - "showValuesForComparison": false, - "comparisonValuesLabel": "", - "color": "" - } - }, - "_hash": 0.5078724779454146, - "aggregationType": null, - "units": null, - "decimals": null, - "funcBody": null, - "usePostProcessing": null, - "postFuncBody": null - } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - } - } - ], - "timewindow": { - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, - "selectedTab": 0, - "realtime": { - "realtimeType": 0, - "interval": 3600000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 86400000, - "timewindowMs": 2592000000, - "fixedTimewindow": null, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "SUM", - "limit": 25000 - }, - "timezone": null - }, - "showTitle": true, - "backgroundColor": "#FFFFFF", - "color": "rgba(0, 0, 0, 0.87)", - "padding": "0px", - "settings": { - "yAxes": { - "default": { - "units": null, - "decimals": 0, - "show": true, - "label": "", - "labelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "600", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.54)", - "position": "left", - "showTickLabels": true, - "tickLabelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "tickLabelColor": "rgba(0, 0, 0, 0.54)", - "ticksFormatter": "var rounder = Math.pow(10, 1);\nvar powers = [\n {key: 'Q', value: Math.pow(10, 15)},\n {key: 'T', value: Math.pow(10, 12)},\n {key: 'B', value: Math.pow(10, 9)},\n {key: 'M', value: Math.pow(10, 6)},\n {key: 'K', value: 1000}\n];\n\nvar key = '';\n\nfor (var i = 0; i < powers.length; i++) {\n var reduced = value / powers[i].value;\n reduced = Math.round(reduced * rounder) / rounder;\n if (reduced >= 1) {\n value = reduced;\n key = powers[i].key;\n break;\n }\n}\nreturn value + key;", - "showTicks": true, - "ticksColor": "rgba(0, 0, 0, 0.54)", - "showLine": true, - "lineColor": "rgba(0, 0, 0, 0.54)", - "showSplitLines": true, - "splitLinesColor": "rgba(0, 0, 0, 0.12)", - "id": "default", - "order": 0, - "min": null, - "max": null - } - }, - "thresholds": [], - "dataZoom": false, - "stack": false, - "xAxis": { - "show": true, - "label": "", - "labelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "600", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.54)", - "position": "bottom", - "showTickLabels": true, - "tickLabelFont": { - "family": "Roboto", - "size": 10, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "tickLabelColor": "rgba(0, 0, 0, 0.54)", - "ticksFormat": {}, - "showTicks": true, - "ticksColor": "rgba(0, 0, 0, 0.54)", - "showLine": true, - "lineColor": "rgba(0, 0, 0, 0.54)", - "showSplitLines": true, - "splitLinesColor": "rgba(0, 0, 0, 0.12)" - }, - "noAggregationBarWidthSettings": { - "strategy": "group", - "groupWidth": { - "relative": true, - "relativeWidth": 6, - "absoluteWidth": 1800000 - }, - "barWidth": { - "relative": true, - "relativeWidth": 2, - "absoluteWidth": 1000 - } - }, - "showLegend": true, - "legendLabelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "16px" - }, - "legendLabelColor": "rgba(0, 0, 0, 0.76)", - "legendConfig": { - "direction": "column", - "position": "bottom", - "sortDataKeys": false, - "showMin": false, - "showMax": false, - "showAvg": false, - "showTotal": true, - "showLatest": false, - "valueFormat": null - }, - "showTooltip": true, - "tooltipTrigger": "axis", - "tooltipValueFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "500", - "lineHeight": "16px" - }, - "tooltipValueColor": "rgba(0, 0, 0, 0.76)", - "tooltipShowDate": true, - "tooltipDateFormat": { - "format": "yyyy-MM-dd HH:mm:ss", - "lastUpdateAgo": false, - "custom": false, - "auto": true, - "autoDateFormatSettings": {} - }, - "tooltipDateFont": { - "family": "Roboto", - "size": 11, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "16px" - }, - "tooltipDateColor": "rgba(0, 0, 0, 0.76)", - "tooltipDateInterval": true, - "tooltipBackgroundColor": "rgba(255, 255, 255, 0.76)", - "tooltipBackgroundBlur": 4, - "animation": { - "animation": true, - "animationThreshold": 2000, - "animationDuration": 1000, - "animationEasing": "cubicOut", - "animationDelay": 0, - "animationDurationUpdate": 300, - "animationEasingUpdate": "cubicOut", - "animationDelayUpdate": 0 - }, - "background": { - "type": "color", - "color": "#fff", - "overlay": { - "enabled": false, - "color": "rgba(255,255,255,0.72)", - "blur": 3 - } - }, - "comparisonEnabled": false, - "timeForComparison": "previousInterval", - "comparisonCustomIntervalValue": 7200000, - "comparisonXAxis": { - "show": true, - "label": "", - "labelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "600", - "lineHeight": "1" - }, - "labelColor": "rgba(0, 0, 0, 0.54)", - "position": "top", - "showTickLabels": true, - "tickLabelFont": { - "family": "Roboto", - "size": 10, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "1" - }, - "tickLabelColor": "rgba(0, 0, 0, 0.54)", - "ticksFormat": {}, - "showTicks": true, - "ticksColor": "rgba(0, 0, 0, 0.54)", - "showLine": true, - "lineColor": "rgba(0, 0, 0, 0.54)", - "showSplitLines": true, - "splitLinesColor": "rgba(0, 0, 0, 0.12)" - }, - "grid": { - "show": false, - "backgroundColor": null, - "borderWidth": 1, - "borderColor": "#ccc" - }, - "legendColumnTitleFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "16px" - }, - "legendColumnTitleColor": "rgba(0, 0, 0, 0.38)", - "legendValueFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "500", - "lineHeight": "16px" - }, - "legendValueColor": "rgba(0, 0, 0, 0.87)", - "tooltipLabelFont": { - "family": "Roboto", - "size": 12, - "sizeUnit": "px", - "style": "normal", - "weight": "400", - "lineHeight": "16px" - }, - "tooltipLabelColor": "rgba(0, 0, 0, 0.76)", - "tooltipHideZeroValues": null, - "padding": "12px" - }, - "title": "{i18n:api-usage.rule-engine-hourly-activity}", - "dropShadow": true, - "enableFullscreen": true, - "titleStyle": null, - "configMode": "basic", - "actions": { - "headerButton": [ - { - "name": "{i18n:api-usage.view-statistics}", - "buttonType": "icon", - "icon": "show_chart", - "buttonColor": "rgba(0, 0, 0, 0.87)", - "customButtonStyle": {}, - "useShowWidgetActionFunction": null, - "showWidgetActionFunction": "return true;", - "type": "openDashboardState", - "targetDashboardStateId": "rule_engine_statistics", - "setEntityId": true, - "stateEntityParamName": null, - "openRightLayout": false, - "openInSeparateDialog": false, - "openInPopover": false, - "id": "8b57e118-84fc-4add-2536-d3cfde018b83" - } - ] - }, - "showTitleIcon": false, - "titleIcon": "thermostat", - "iconColor": "#1F6BDD", - "useDashboardTimewindow": false, - "displayTimewindow": true, - "titleFont": { - "size": 16, - "sizeUnit": "px", - "family": "Roboto", - "weight": "500", - "style": "normal", - "lineHeight": "24px" - }, - "titleColor": "rgba(0, 0, 0, 0.87)", - "titleTooltip": "", - "widgetStyle": {}, - "widgetCss": "", - "pageSize": 1024, - "units": "", - "decimals": null, - "noDataDisplayMessage": "", - "timewindowStyle": { - "showIcon": false, - "iconSize": "24px", - "icon": null, - "iconPosition": "left", - "font": { - "size": 12, - "sizeUnit": "px", - "family": "Roboto", - "weight": "400", - "style": "normal", - "lineHeight": "16px" - }, - "color": "rgba(0, 0, 0, 0.38)", - "displayTypePrefix": true - }, - "margin": "0px", - "borderRadius": "4px", - "iconSize": "0px" - }, - "row": 0, - "col": 0, - "id": "b1a9a51f-e5a6-9d5f-ef5c-25c2a68af1b0" - }, - "84fbe63a-bcb6-7bc1-8af0-46b3b1ee5adc": { - "typeFullFqn": "system.time_series_chart", - "type": "timeseries", - "sizeX": 8, - "sizeY": 5, - "config": { - "datasources": [ - { - "type": "entity", - "name": null, - "entityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", - "filterId": null, - "dataKeys": [ - { - "name": "ruleEngineExecutionCountHourly", - "type": "timeseries", - "label": "{i18n:api-usage.rule-engine-executions}", - "color": "#AB00FF", - "settings": { - "yAxisId": "default", - "showInLegend": true, - "dataHiddenByDefault": false, + "yAxisId": "default", + "showInLegend": true, + "dataHiddenByDefault": false, "type": "bar", "lineSettings": { "showLine": true, @@ -5508,7 +3834,7 @@ "color": "" } }, - "_hash": 0.01948850513940492, + "_hash": 0.12814821361119078, "aggregationType": null, "units": null, "decimals": null, @@ -5529,18 +3855,28 @@ "hideAggInterval": false, "hideTimezone": false, "selectedTab": 1, + "realtime": { + "realtimeType": 0, + "interval": 1000, + "timewindowMs": 60000, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideQuickInterval": false + }, "history": { "historyType": 0, - "timewindowMs": 2592000000, - "interval": 86400000, - "fixedTimewindow": { - "startTimeMs": 1709729389667, - "endTimeMs": 1709815789667 - }, - "quickInterval": "CURRENT_DAY" + "interval": 2592000000, + "timewindowMs": 31536000000, + "fixedTimewindow": null, + "quickInterval": "CURRENT_DAY", + "hideInterval": false, + "hideLastInterval": false, + "hideFixedInterval": false, + "hideQuickInterval": false }, "aggregation": { - "type": "SUM", + "type": "NONE", "limit": 25000 }, "timezone": null @@ -5777,32 +4113,12 @@ "tooltipHideZeroValues": null, "padding": "12px" }, - "title": "{i18n:api-usage.rule-engine-daily-activity}", + "title": "{i18n:api-usage.transport-data-points-monthly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", - "actions": { - "headerButton": [ - { - "name": "{i18n:api-usage.view-statistics}", - "buttonType": "icon", - "icon": "show_chart", - "buttonColor": "rgba(0, 0, 0, 0.87)", - "customButtonStyle": {}, - "useShowWidgetActionFunction": null, - "showWidgetActionFunction": "return true;", - "type": "openDashboardState", - "targetDashboardStateId": "rule_engine_statistics", - "setEntityId": true, - "stateEntityParamName": null, - "openRightLayout": false, - "openInSeparateDialog": false, - "openInPopover": false, - "id": "2592147a-3f62-987a-78c0-cdb775fb4233" - } - ] - }, + "actions": {}, "showTitleIcon": false, "titleIcon": "thermostat", "iconColor": "#1F6BDD", @@ -5846,9 +4162,9 @@ }, "row": 0, "col": 0, - "id": "84fbe63a-bcb6-7bc1-8af0-46b3b1ee5adc" + "id": "966ffee7-ba0d-8e54-f903-e8d015ca8cd2" }, - "43a2b982-6c02-d9bd-71ee-34e8e6cf8893": { + "84fbe63a-bcb6-7bc1-8af0-46b3b1ee5adc": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -5862,7 +4178,7 @@ "filterId": null, "dataKeys": [ { - "name": "ruleEngineExecutionCount", + "name": "ruleEngineExecutionCountHourly", "type": "timeseries", "label": "{i18n:api-usage.rule-engine-executions}", "color": "#AB00FF", @@ -5935,7 +4251,7 @@ "color": "" } }, - "_hash": 0.5125470598651091, + "_hash": 0.01948850513940492, "aggregationType": null, "units": null, "decimals": null, @@ -5955,29 +4271,19 @@ "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, - "selectedTab": 1, - "realtime": { - "realtimeType": 0, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 2592000000, - "timewindowMs": 31536000000, - "fixedTimewindow": null, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false + "selectedTab": 1, + "history": { + "historyType": 0, + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { - "type": "NONE", + "type": "SUM", "limit": 25000 }, "timezone": null @@ -6214,7 +4520,7 @@ "tooltipHideZeroValues": null, "padding": "12px" }, - "title": "{i18n:api-usage.rule-engine-monthly-activity}", + "title": "{i18n:api-usage.rule-engine-daily-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -6236,7 +4542,7 @@ "openRightLayout": false, "openInSeparateDialog": false, "openInPopover": false, - "id": "b6ba96cf-48b8-f40f-f010-10b95e7dc819" + "id": "2592147a-3f62-987a-78c0-cdb775fb4233" } ] }, @@ -6283,9 +4589,9 @@ }, "row": 0, "col": 0, - "id": "43a2b982-6c02-d9bd-71ee-34e8e6cf8893" + "id": "84fbe63a-bcb6-7bc1-8af0-46b3b1ee5adc" }, - "76fe83c9-c30f-00a5-6299-40c759ca6705": { + "43a2b982-6c02-d9bd-71ee-34e8e6cf8893": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -6299,42 +4605,86 @@ "filterId": null, "dataKeys": [ { - "name": "jsExecutionCountHourly", + "name": "ruleEngineExecutionCount", "type": "timeseries", - "label": "{i18n:api-usage.javascript-function-executions}", - "color": "#FF9900", + "label": "{i18n:api-usage.rule-engine-executions}", + "color": "#AB00FF", "settings": { - "excludeFromStacking": false, - "hideDataByDefault": false, - "disableDataHiding": false, - "removeFromLegend": false, - "showLines": false, - "fillLines": false, - "showPoints": false, - "showPointShape": "circle", - "pointShapeFormatter": "", - "showPointsLineWidth": 5, - "showPointsRadius": 3, - "showSeparateAxis": false, - "axisPosition": "left", - "thresholds": [ - { - "thresholdValueSource": "predefinedValue" + "yAxisId": "default", + "showInLegend": true, + "dataHiddenByDefault": false, + "type": "bar", + "lineSettings": { + "showLine": true, + "step": false, + "stepType": "start", + "smooth": false, + "lineType": "solid", + "lineWidth": 2, + "showPoints": false, + "showPointLabel": false, + "pointLabelPosition": "top", + "pointLabelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "pointLabelColor": "rgba(0, 0, 0, 0.76)", + "enablePointLabelBackground": false, + "pointLabelBackground": "rgba(255,255,255,0.56)", + "pointShape": "emptyCircle", + "pointSize": 4, + "fillAreaSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } } - ], - "comparisonSettings": { - "showValuesForComparison": true }, - "type": "bar", - "yAxisId": "default" + "barSettings": { + "showBorder": false, + "borderWidth": 2, + "borderRadius": 0, + "showLabel": false, + "labelPosition": "top", + "labelFont": { + "family": "Roboto", + "size": 11, + "sizeUnit": "px", + "style": "normal", + "weight": "400", + "lineHeight": "1" + }, + "labelColor": "rgba(0, 0, 0, 0.76)", + "enableLabelBackground": false, + "labelBackground": "rgba(255,255,255,0.56)", + "backgroundSettings": { + "type": "none", + "opacity": 0.4, + "gradient": { + "start": 100, + "end": 0 + } + } + }, + "comparisonSettings": { + "showValuesForComparison": false, + "comparisonValuesLabel": "", + "color": "" + } }, - "_hash": 0.0661644137210089, + "_hash": 0.5125470598651091, + "aggregationType": null, "units": null, "decimals": null, "funcBody": null, "usePostProcessing": null, - "postFuncBody": null, - "aggregationType": null + "postFuncBody": null } ], "alarmFilterConfig": { @@ -6348,11 +4698,11 @@ "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, - "selectedTab": 0, + "selectedTab": 1, "realtime": { "realtimeType": 0, - "interval": 3600000, - "timewindowMs": 86400000, + "interval": 1000, + "timewindowMs": 60000, "quickInterval": "CURRENT_DAY", "hideInterval": false, "hideLastInterval": false, @@ -6360,8 +4710,8 @@ }, "history": { "historyType": 0, - "interval": 86400000, - "timewindowMs": 2592000000, + "interval": 2592000000, + "timewindowMs": 31536000000, "fixedTimewindow": null, "quickInterval": "CURRENT_DAY", "hideInterval": false, @@ -6370,7 +4720,7 @@ "hideQuickInterval": false }, "aggregation": { - "type": "SUM", + "type": "NONE", "limit": 25000 }, "timezone": null @@ -6607,12 +4957,32 @@ "tooltipHideZeroValues": null, "padding": "12px" }, - "title": "{i18n:api-usage.javascript-function-executions-hourly-activity}", + "title": "{i18n:api-usage.rule-engine-monthly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, "configMode": "basic", - "actions": {}, + "actions": { + "headerButton": [ + { + "name": "{i18n:api-usage.view-statistics}", + "buttonType": "icon", + "icon": "show_chart", + "buttonColor": "rgba(0, 0, 0, 0.87)", + "customButtonStyle": {}, + "useShowWidgetActionFunction": null, + "showWidgetActionFunction": "return true;", + "type": "openDashboardState", + "targetDashboardStateId": "rule_engine_statistics", + "setEntityId": true, + "stateEntityParamName": null, + "openRightLayout": false, + "openInSeparateDialog": false, + "openInPopover": false, + "id": "b6ba96cf-48b8-f40f-f010-10b95e7dc819" + } + ] + }, "showTitleIcon": false, "titleIcon": "thermostat", "iconColor": "#1F6BDD", @@ -6656,9 +5026,9 @@ }, "row": 0, "col": 0, - "id": "76fe83c9-c30f-00a5-6299-40c759ca6705" + "id": "43a2b982-6c02-d9bd-71ee-34e8e6cf8893" }, - "a43598d1-7bfd-f329-ee61-c343f34f069f": { + "76fe83c9-c30f-00a5-6299-40c759ca6705": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -6718,25 +5088,16 @@ } ], "timewindow": { - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, - "selectedTab": 1, - "history": { - "historyType": 0, - "timewindowMs": 2592000000, - "interval": 86400000, - "fixedTimewindow": { - "startTimeMs": 1709729389667, - "endTimeMs": 1709815789667 - }, - "quickInterval": "CURRENT_DAY" + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000 }, "aggregation": { - "type": "SUM", - "limit": 25000 - }, - "timezone": null + "type": "NONE", + "limit": 50000 + } }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -6970,7 +5331,7 @@ "tooltipHideZeroValues": null, "padding": "12px" }, - "title": "{i18n:api-usage.javascript-function-executions-daily-activity}", + "title": "{i18n:api-usage.javascript-function-executions-hourly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -7019,9 +5380,9 @@ }, "row": 0, "col": 0, - "id": "a43598d1-7bfd-f329-ee61-c343f34f069f" + "id": "76fe83c9-c30f-00a5-6299-40c759ca6705" }, - "3ebd62a8-dcb7-c96b-8571-e61084248f5b": { + "a43598d1-7bfd-f329-ee61-c343f34f069f": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -7035,7 +5396,7 @@ "filterId": null, "dataKeys": [ { - "name": "jsExecutionCount", + "name": "jsExecutionCountHourly", "type": "timeseries", "label": "{i18n:api-usage.javascript-function-executions}", "color": "#FF9900", @@ -7072,7 +5433,12 @@ "postFuncBody": null, "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { @@ -7080,28 +5446,18 @@ "hideAggInterval": false, "hideTimezone": false, "selectedTab": 1, - "realtime": { - "realtimeType": 0, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, "history": { "historyType": 0, - "interval": 2592000000, - "timewindowMs": 31536000000, - "fixedTimewindow": null, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { - "type": "NONE", + "type": "SUM", "limit": 25000 }, "timezone": null @@ -7338,7 +5694,7 @@ "tooltipHideZeroValues": null, "padding": "12px" }, - "title": "{i18n:api-usage.javascript-function-executions-monthly-activity}", + "title": "{i18n:api-usage.javascript-function-executions-daily-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -7387,9 +5743,9 @@ }, "row": 0, "col": 0, - "id": "3ebd62a8-dcb7-c96b-8571-e61084248f5b" + "id": "a43598d1-7bfd-f329-ee61-c343f34f069f" }, - "88e25971-e5cb-eebb-3c7c-1ce33a8a38f4": { + "3ebd62a8-dcb7-c96b-8571-e61084248f5b": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -7403,10 +5759,10 @@ "filterId": null, "dataKeys": [ { - "name": "tbelExecutionCountHourly", + "name": "jsExecutionCount", "type": "timeseries", - "label": "{i18n:api-usage.tbel-function-executions}", - "color": "#4CAF50", + "label": "{i18n:api-usage.javascript-function-executions}", + "color": "#FF9900", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -7440,23 +5796,18 @@ "postFuncBody": null, "aggregationType": null } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - } + ] } ], "timewindow": { "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, - "selectedTab": 0, + "selectedTab": 1, "realtime": { "realtimeType": 0, - "interval": 3600000, - "timewindowMs": 86400000, + "interval": 1000, + "timewindowMs": 60000, "quickInterval": "CURRENT_DAY", "hideInterval": false, "hideLastInterval": false, @@ -7464,8 +5815,8 @@ }, "history": { "historyType": 0, - "interval": 86400000, - "timewindowMs": 2592000000, + "interval": 2592000000, + "timewindowMs": 31536000000, "fixedTimewindow": null, "quickInterval": "CURRENT_DAY", "hideInterval": false, @@ -7474,7 +5825,7 @@ "hideQuickInterval": false }, "aggregation": { - "type": "SUM", + "type": "NONE", "limit": 25000 }, "timezone": null @@ -7711,7 +6062,7 @@ "tooltipHideZeroValues": null, "padding": "12px" }, - "title": "{i18n:api-usage.tbel-function-executions-hourly-activity}", + "title": "{i18n:api-usage.javascript-function-executions-monthly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -7760,9 +6111,9 @@ }, "row": 0, "col": 0, - "id": "88e25971-e5cb-eebb-3c7c-1ce33a8a38f4" + "id": "3ebd62a8-dcb7-c96b-8571-e61084248f5b" }, - "a1b5731c-e3b3-8cfb-7c50-3abcdce891d2": { + "88e25971-e5cb-eebb-3c7c-1ce33a8a38f4": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -7822,25 +6173,16 @@ } ], "timewindow": { - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, - "selectedTab": 1, - "history": { - "historyType": 0, - "timewindowMs": 2592000000, - "interval": 86400000, - "fixedTimewindow": { - "startTimeMs": 1709729389667, - "endTimeMs": 1709815789667 - }, - "quickInterval": "CURRENT_DAY" + "selectedTab": 0, + "realtime": { + "realtimeType": 0, + "interval": 3600000, + "timewindowMs": 86400000 }, "aggregation": { - "type": "SUM", - "limit": 25000 - }, - "timezone": null + "type": "NONE", + "limit": 50000 + } }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -8074,7 +6416,7 @@ "tooltipHideZeroValues": null, "padding": "12px" }, - "title": "{i18n:api-usage.tbel-function-executions-daily-activity}", + "title": "{i18n:api-usage.tbel-function-executions-hourly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -8123,9 +6465,9 @@ }, "row": 0, "col": 0, - "id": "a1b5731c-e3b3-8cfb-7c50-3abcdce891d2" + "id": "88e25971-e5cb-eebb-3c7c-1ce33a8a38f4" }, - "efc8d4e9-dee2-b677-c378-c1a666543bf4": { + "a1b5731c-e3b3-8cfb-7c50-3abcdce891d2": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -8139,7 +6481,7 @@ "filterId": null, "dataKeys": [ { - "name": "tbelExecutionCount", + "name": "tbelExecutionCountHourly", "type": "timeseries", "label": "{i18n:api-usage.tbel-function-executions}", "color": "#4CAF50", @@ -8176,7 +6518,12 @@ "postFuncBody": null, "aggregationType": null } - ] + ], + "alarmFilterConfig": { + "statusList": [ + "ACTIVE" + ] + } } ], "timewindow": { @@ -8184,28 +6531,18 @@ "hideAggInterval": false, "hideTimezone": false, "selectedTab": 1, - "realtime": { - "realtimeType": 0, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, "history": { "historyType": 0, - "interval": 2592000000, - "timewindowMs": 31536000000, - "fixedTimewindow": null, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false + "timewindowMs": 2592000000, + "interval": 86400000, + "fixedTimewindow": { + "startTimeMs": 1709729389667, + "endTimeMs": 1709815789667 + }, + "quickInterval": "CURRENT_DAY" }, "aggregation": { - "type": "NONE", + "type": "SUM", "limit": 25000 }, "timezone": null @@ -8442,7 +6779,7 @@ "tooltipHideZeroValues": null, "padding": "12px" }, - "title": "{i18n:api-usage.tbel-function-executions-monthly-activity}", + "title": "{i18n:api-usage.tbel-function-executions-daily-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -8491,9 +6828,9 @@ }, "row": 0, "col": 0, - "id": "efc8d4e9-dee2-b677-c378-c1a666543bf4" + "id": "a1b5731c-e3b3-8cfb-7c50-3abcdce891d2" }, - "61a23bd5-329f-aae7-3168-8a14a51dc10b": { + "efc8d4e9-dee2-b677-c378-c1a666543bf4": { "typeFullFqn": "system.time_series_chart", "type": "timeseries", "sizeX": 8, @@ -8507,10 +6844,10 @@ "filterId": null, "dataKeys": [ { - "name": "storageDataPointsCountHourly", + "name": "tbelExecutionCount", "type": "timeseries", - "label": "{i18n:api-usage.data-points-storage-days}", - "color": "#1039EE", + "label": "{i18n:api-usage.tbel-function-executions}", + "color": "#4CAF50", "settings": { "excludeFromStacking": false, "hideDataByDefault": false, @@ -8544,23 +6881,18 @@ "postFuncBody": null, "aggregationType": null } - ], - "alarmFilterConfig": { - "statusList": [ - "ACTIVE" - ] - } + ] } ], "timewindow": { "hideAggregation": false, "hideAggInterval": false, "hideTimezone": false, - "selectedTab": 0, + "selectedTab": 1, "realtime": { "realtimeType": 0, - "interval": 3600000, - "timewindowMs": 86400000, + "interval": 1000, + "timewindowMs": 60000, "quickInterval": "CURRENT_DAY", "hideInterval": false, "hideLastInterval": false, @@ -8568,8 +6900,8 @@ }, "history": { "historyType": 0, - "interval": 86400000, - "timewindowMs": 2592000000, + "interval": 2592000000, + "timewindowMs": 31536000000, "fixedTimewindow": null, "quickInterval": "CURRENT_DAY", "hideInterval": false, @@ -8578,7 +6910,7 @@ "hideQuickInterval": false }, "aggregation": { - "type": "SUM", + "type": "NONE", "limit": 25000 }, "timezone": null @@ -8815,7 +7147,7 @@ "tooltipHideZeroValues": null, "padding": "12px" }, - "title": "{i18n:api-usage.data-points-storage-days-hourly-activity}", + "title": "{i18n:api-usage.tbel-function-executions-monthly-activity}", "dropShadow": true, "enableFullscreen": true, "titleStyle": null, @@ -8864,7 +7196,7 @@ }, "row": 0, "col": 0, - "id": "61a23bd5-329f-aae7-3168-8a14a51dc10b" + "id": "efc8d4e9-dee2-b677-c378-c1a666543bf4" }, "1249d3e2-6b3a-4e4a-65e9-6ed22959871e": { "typeFullFqn": "system.time_series_chart", @@ -9662,35 +7994,16 @@ } ], "timewindow": { - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, "selectedTab": 0, "realtime": { "realtimeType": 0, "interval": 3600000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 86400000, - "timewindowMs": 2592000000, - "fixedTimewindow": null, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false + "timewindowMs": 86400000 }, "aggregation": { - "type": "SUM", - "limit": 25000 - }, - "timezone": null + "type": "NONE", + "limit": 50000 + } }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -10771,35 +9084,16 @@ } ], "timewindow": { - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, "selectedTab": 0, "realtime": { "realtimeType": 0, "interval": 3600000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 86400000, - "timewindowMs": 2592000000, - "fixedTimewindow": null, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false + "timewindowMs": 86400000 }, "aggregation": { - "type": "SUM", - "limit": 25000 - }, - "timezone": null + "type": "NONE", + "limit": 50000 + } }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -11875,35 +10169,16 @@ } ], "timewindow": { - "hideAggregation": false, - "hideAggInterval": false, - "hideTimezone": false, "selectedTab": 0, "realtime": { "realtimeType": 0, "interval": 3600000, - "timewindowMs": 86400000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 86400000, - "timewindowMs": 2592000000, - "fixedTimewindow": null, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false + "timewindowMs": 86400000 }, "aggregation": { - "type": "SUM", - "limit": 25000 - }, - "timezone": null + "type": "NONE", + "limit": 50000 + } }, "showTitle": true, "backgroundColor": "#FFFFFF", @@ -12932,44 +11207,13 @@ "dataKeys": [] } ], - "timewindow": { - "displayValue": "", - "selectedTab": 0, - "realtime": { - "realtimeType": 1, - "interval": 1000, - "timewindowMs": 60000, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideQuickInterval": false - }, - "history": { - "historyType": 0, - "interval": 1000, - "timewindowMs": 60000, - "fixedTimewindow": { - "startTimeMs": 1756302747649, - "endTimeMs": 1756389147649 - }, - "quickInterval": "CURRENT_DAY", - "hideInterval": false, - "hideLastInterval": false, - "hideFixedInterval": false, - "hideQuickInterval": false - }, - "aggregation": { - "type": "AVG", - "limit": 25000 - } - }, "showTitle": true, "backgroundColor": "#fff", "color": "rgba(0, 0, 0, 0.87)", "padding": "0", "settings": { "dsEntityAliasId": "40193437-33ac-3172-eefd-0b08eb849062", - "dataKeys": [ + "apiUsageDataKeys": [ { "label": "{i18n:api-usage.transport-messages}", "state": "transport_messages", @@ -13222,9 +11466,9 @@ "actions": { "headerButton": [ { - "name": "Go back", + "name": "{i18n:widgets.api-usage.go-to-main-state}", "buttonType": "stroked", - "showIcon": true, + "showIcon": false, "icon": "undo", "buttonColor": "#305680", "buttonBorderColor": "#0000001F", @@ -13432,14 +11676,6 @@ }, "right": { "widgets": { - "51608a74-f213-d8c9-8df8-b42238ef93a6": { - "sizeX": 12, - "sizeY": 4, - "row": 0, - "col": 0, - "resizable": true, - "mobileHeight": 6 - }, "fb155957-1af4-233e-e2fb-09e648e75d6e": { "sizeX": 6, "sizeY": 4, @@ -13455,6 +11691,13 @@ "col": 6, "resizable": true, "mobileHeight": 6 + }, + "85240e8c-7af7-90a9-ad0a-726013c479a6": { + "sizeX": 12, + "sizeY": 4, + "resizable": true, + "row": 0, + "col": 0 } }, "gridSettings": { @@ -13511,14 +11754,6 @@ }, "right": { "widgets": { - "9e00cc90-520d-2108-1d2f-bba68ed5cbf1": { - "sizeX": 12, - "sizeY": 4, - "row": 0, - "col": 0, - "resizable": true, - "mobileHeight": 6 - }, "79056202-c92b-1dae-ce49-318ec52e2d3b": { "sizeX": 6, "sizeY": 4, @@ -13534,6 +11769,13 @@ "col": 6, "resizable": true, "mobileHeight": 6 + }, + "d0a10a8f-8f48-f9d6-8306-d12af9b49690": { + "sizeX": 12, + "sizeY": 4, + "resizable": true, + "row": 0, + "col": 0 } }, "gridSettings": { @@ -13590,14 +11832,6 @@ }, "right": { "widgets": { - "b1a9a51f-e5a6-9d5f-ef5c-25c2a68af1b0": { - "sizeX": 12, - "sizeY": 4, - "row": 0, - "col": 0, - "resizable": true, - "mobileHeight": 6 - }, "84fbe63a-bcb6-7bc1-8af0-46b3b1ee5adc": { "sizeX": 6, "sizeY": 4, @@ -13613,6 +11847,13 @@ "col": 6, "resizable": true, "mobileHeight": 6 + }, + "4544080d-9b6f-b592-9cd4-0e0335d33857": { + "sizeX": 12, + "sizeY": 4, + "resizable": true, + "row": 0, + "col": 0 } }, "gridSettings": { @@ -13827,14 +12068,6 @@ }, "right": { "widgets": { - "61a23bd5-329f-aae7-3168-8a14a51dc10b": { - "sizeX": 12, - "sizeY": 4, - "row": 0, - "col": 0, - "resizable": true, - "mobileHeight": 6 - }, "1249d3e2-6b3a-4e4a-65e9-6ed22959871e": { "sizeX": 6, "sizeY": 4, @@ -13850,6 +12083,13 @@ "col": 6, "resizable": true, "mobileHeight": 6 + }, + "5d0f2f57-499d-1324-8e1b-cfbc0b3149d2": { + "sizeX": 12, + "sizeY": 4, + "resizable": true, + "row": 0, + "col": 0 } }, "gridSettings": { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 9ee555a838..c6d6c3b23b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -9544,7 +9544,8 @@ "add-key": "Add key", "no-key": "No key", "delete-key": "Delete key", - "target-dashboard-state": "Target dashboard state" + "target-dashboard-state": "Target dashboard state", + "go-to-main-state": "Go to default view" } }, "color": { From da1252d8689433bb24220840392864713ec473ef Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 16 Sep 2025 14:42:17 +0300 Subject: [PATCH 129/216] added new translation key --- .../widget/lib/timeseries-table-widget.component.html | 2 +- .../components/widget/lib/timeseries-table-widget.component.ts | 2 +- ui-ngx/src/assets/locale/locale.constant-en_US.json | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html index 6810055592..73ea953a15 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.html @@ -47,7 +47,7 @@
- {{ 'widgets.table.display-timestamp' | translate }} + {{ 'widgets.table.timestamp-column-name' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts index d475dd886d..47582d9855 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/timeseries-table-widget.component.ts @@ -513,7 +513,7 @@ export class TimeseriesTableWidgetComponent extends PageComponent implements OnI let title = ''; const header = this.sources[index].header.find(column => column.index.toString() === value); if (value === '0') { - title = this.translate.instant('widgets.table.display-timestamp'); + title = this.translate.instant('widgets.table.timestamp-column-name'); } else if (value === 'actions') { title = 'Actions'; } else { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 9ee555a838..f4200563e7 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -8973,6 +8973,7 @@ "show-empty-space-hidden-action": "Show empty space instead of hidden cell button action", "dont-reserve-space-hidden-action": "Don't reserve space for hidden action buttons", "display-timestamp": "Timestamp", + "timestamp-column-name":"Timestamp", "display-pagination": "Display pagination", "default-page-size": "Default page size", "page-step-settings": "Page step settings", From 0b85473147440a5c59dcde4df6cbda033d862382 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Tue, 16 Sep 2025 18:22:03 +0300 Subject: [PATCH 130/216] UI: Add tenant profice configs for cf geofencing --- ...enant-profile-configuration.component.html | 28 +++++++++++++++++++ ...-tenant-profile-configuration.component.ts | 2 ++ .../assets/locale/locale.constant-en_US.json | 6 ++++ 3 files changed, 36 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html index 3c5c8774d5..1540a3c87f 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html @@ -275,6 +275,34 @@ + + tenant-profile.max-related-level-per-argument + + + {{ 'tenant-profile.max-related-level-per-argument-required' | translate}} + + + {{ 'tenant-profile.max-related-level-per-argument-range' | translate}} + + + + +
+ + tenant-profile.min-allowed-scheduled-update-interval + + + {{ 'tenant-profile.min-allowed-scheduled-update-interval-required' | translate}} + + + {{ 'tenant-profile.min-allowed-scheduled-update-interval-range' | translate}} + + +
diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts index c6efd9dee3..0dd1453648 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts +++ b/ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts @@ -124,6 +124,8 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA edgeUplinkMessagesRateLimitsPerEdge: [null, []], maxCalculatedFieldsPerEntity: [null, [Validators.required, Validators.min(0)]], maxArgumentsPerCF: [null, [Validators.required, Validators.min(0)]], + maxRelationLevelPerCfArgument: [null, [Validators.required, Validators.min(1)]], + minAllowedScheduledUpdateIntervalInSecForCF: [null, [Validators.required, Validators.min(0)]], maxDataPointsPerRollingArg: [null, [Validators.required, Validators.min(0)]], maxStateSizeInKBytes: [null, [Validators.required, Validators.min(0)]], calculatedFieldDebugEventsRateLimit: [null, []], diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index d0844ad85e..07c995bf19 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -5779,6 +5779,12 @@ "max-arguments-per-cf": "Arguments per calculated field max number", "max-arguments-per-cf-range": "Arguments per calculated field max number can't be negative", "max-arguments-per-cf-required": "Arguments per calculated field max number is required", + "max-related-level-per-argument": "Maximum relation level per 'Related entities' argument", + "max-related-level-per-argument-range": "Relation level per 'Related entities' argument max number can't be less than '1'", + "max-related-level-per-argument-required": "Relation level per 'Related entities' argument max number is required", + "min-allowed-scheduled-update-interval": "Min allowed update interval for 'Related entities' arguments (seconds)", + "min-allowed-scheduled-update-interval-range": "Min allowed update interval min number can't be negative", + "min-allowed-scheduled-update-interval-required": "Min allowed update interval min number is required", "max-state-size": "State maximum size in KB", "max-state-size-range": "State maximum size in KB can't be negative", "max-state-size-required": "State maximum size in KB is required", From 995ca2e4a87f2e0588512a8e0c271f89fdbae2cf Mon Sep 17 00:00:00 2001 From: dshvaika Date: Tue, 16 Sep 2025 18:40:02 +0300 Subject: [PATCH 131/216] fixed updates of non-dynamic zone group arguments --- .../CalculatedFieldEntityMessageProcessor.java | 17 ++++++++++++----- .../calculatedField/MultipleTbCallback.java | 2 +- .../cf/ctx/state/CalculatedFieldCtx.java | 17 +++++++++++++++++ .../geofencing/GeofencingArgumentEntry.java | 6 ++++++ .../geofencing/ZoneGroupConfiguration.java | 14 ++++++++++++++ 5 files changed, 50 insertions(+), 6 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index fa51cd2e3d..f3431390a3 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -48,6 +48,7 @@ import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; import java.util.ArrayList; import java.util.Collection; @@ -390,7 +391,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } private Map mapToArguments(CalculatedFieldCtx ctx, AttributeScopeProto scope, List attrDataList) { - return mapToArguments(ctx.getMainEntityArguments(), scope, attrDataList); + return mapToArguments(ctx.getEntityId(), ctx.getMainEntityArguments(), ctx.getMainEntityGeofencingArgumentNames(), scope, attrDataList); } private Map mapToArguments(CalculatedFieldCtx ctx, EntityId entityId, AttributeScopeProto scope, List attrDataList) { @@ -398,17 +399,23 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM if (argNames.isEmpty()) { return Collections.emptyMap(); } - return mapToArguments(argNames, scope, attrDataList); + List geofencingArgumentNames = ctx.getLinkedEntityGeofencingArgumentNames(); + return mapToArguments(entityId, argNames, geofencingArgumentNames, scope, attrDataList); } - private Map mapToArguments(Map argNames, AttributeScopeProto scope, List attrDataList) { + private Map mapToArguments(EntityId entityId, Map argNames, List geoArgNames, AttributeScopeProto scope, List attrDataList) { Map arguments = new HashMap<>(); for (AttributeValueProto item : attrDataList) { ReferencedEntityKey key = new ReferencedEntityKey(item.getKey(), ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name())); String argName = argNames.get(key); - if (argName != null) { - arguments.put(argName, new SingleValueArgumentEntry(item)); + if (argName == null) { + continue; + } + if (geoArgNames.contains(argName)) { + arguments.put(argName, new GeofencingArgumentEntry(entityId, item)); + continue; } + arguments.put(argName, new SingleValueArgumentEntry(item)); } return arguments; } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/MultipleTbCallback.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/MultipleTbCallback.java index d1f4c9092e..493985c97a 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/MultipleTbCallback.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/MultipleTbCallback.java @@ -50,7 +50,7 @@ public class MultipleTbCallback implements TbCallback { @Override public void onFailure(Throwable t) { - log.warn("[{}][{}] onFailure.", id, callback.getId()); + log.warn("[{}][{}] onFailure.", id, callback.getId(), t); callback.onFailure(t); } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 5c8177c074..c2cc083853 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -77,6 +78,9 @@ public class CalculatedFieldCtx { private long maxStateSize; private long maxSingleValueArgumentSize; + private List mainEntityGeofencingArgumentNames; + private List linkedEntityGeofencingArgumentNames; + public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService, ApiLimitService apiLimitService, RelationService relationService) { this.calculatedField = calculatedField; @@ -88,6 +92,8 @@ public class CalculatedFieldCtx { this.mainEntityArguments = new HashMap<>(); this.linkedEntityArguments = new HashMap<>(); this.argNames = new ArrayList<>(); + this.mainEntityGeofencingArgumentNames = new ArrayList<>(); + this.linkedEntityGeofencingArgumentNames = new ArrayList<>(); this.output = calculatedField.getConfiguration().getOutput(); if (calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argBasedConfig) { this.arguments.putAll(argBasedConfig.getArguments()); @@ -108,6 +114,17 @@ public class CalculatedFieldCtx { this.expression = expressionBasedConfig.getExpression(); this.useLatestTs = CalculatedFieldType.SIMPLE.equals(calculatedField.getType()) && ((SimpleCalculatedFieldConfiguration) argBasedConfig).isUseLatestTs(); } + if (calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration geofencingConfig) { + geofencingConfig.getZoneGroups().forEach((zoneGroupName, config) -> { + if (config.isCfEntitySource(entityId)) { + mainEntityGeofencingArgumentNames.add(zoneGroupName); + return; + } + if (config.isLinkedCfEntitySource(entityId)) { + linkedEntityGeofencingArgumentNames.add(zoneGroupName); + } + }); + } } this.tbelInvokeService = tbelInvokeService; this.relationService = relationService; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java index 7f610aaf48..d6b7aefed4 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java @@ -21,6 +21,8 @@ import org.thingsboard.script.api.tbel.TbelCfArg; import org.thingsboard.script.api.tbel.TbelCfTsGeofencingArg; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.KvEntry; +import org.thingsboard.server.common.util.ProtoUtils; +import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntryType; @@ -38,6 +40,10 @@ public class GeofencingArgumentEntry implements ArgumentEntry { public GeofencingArgumentEntry() { } + public GeofencingArgumentEntry(EntityId entityId, TransportProtos.AttributeValueProto entry) { + this.zoneStates = toZones(Map.of(entityId, ProtoUtils.fromProto(entry))); + } + public GeofencingArgumentEntry(Map entityIdkvEntryMap) { this.zoneStates = toZones(entityIdkvEntryMap); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java index 5328dbdbc3..2feb6e49d0 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfiguration.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.cf.configuration.geofencing; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Data; import org.springframework.lang.Nullable; @@ -71,6 +72,19 @@ public class ZoneGroupConfiguration { return refDynamicSourceConfiguration != null; } + @JsonIgnore + public boolean isCfEntitySource(EntityId cfEntityId) { + if (refEntityId == null && refDynamicSourceConfiguration == null) { + return true; + } + return refEntityId != null && refEntityId.equals(cfEntityId); + } + + @JsonIgnore + public boolean isLinkedCfEntitySource(EntityId cfEntityId) { + return refEntityId != null && !refEntityId.equals(cfEntityId); + } + public Argument toArgument() { var argument = new Argument(); argument.setRefEntityId(refEntityId); From c56f82497c26c2f35ca9ffe4cd8d9b4720d69993 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Wed, 17 Sep 2025 17:48:15 +0300 Subject: [PATCH 132/216] Replaced required JSON data type for geozone arguments --- .../cf/ctx/state/geofencing/GeofencingArgumentEntry.java | 1 - .../service/cf/ctx/state/geofencing/GeofencingZoneState.java | 2 +- .../cf/ctx/state/GeofencingValueArgumentEntryTest.java | 5 +++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java index d6b7aefed4..f526cc00ab 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java @@ -84,7 +84,6 @@ public class GeofencingArgumentEntry implements ArgumentEntry { private Map toZones(Map entityIdKvEntryMap) { return entityIdKvEntryMap.entrySet().stream() - .filter(entry -> entry.getValue().getJsonValue().isPresent()) .collect(Collectors.toMap(Map.Entry::getKey, entry -> new GeofencingZoneState(entry.getKey(), entry.getValue()))); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java index 348c1ba9f1..c849f5d169 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingZoneState.java @@ -50,7 +50,7 @@ public class GeofencingZoneState { } this.ts = attributeKvEntry.getLastUpdateTs(); this.version = attributeKvEntry.getVersion(); - this.perimeterDefinition = JacksonUtil.fromString(entry.getJsonValue().orElseThrow(), PerimeterDefinition.class); + this.perimeterDefinition = JacksonUtil.fromString(entry.getValueAsString(), PerimeterDefinition.class); } public GeofencingZoneState(GeofencingZoneProto proto) { diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java index b3487f0e83..6da4bdc882 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingValueArgumentEntryTest.java @@ -168,8 +168,9 @@ public class GeofencingValueArgumentEntryTest { @Test void testInvalidKvEntryDataTypeForZoneResultInEmptyArgument() { BaseAttributeKvEntry invalidZoneEntry = new BaseAttributeKvEntry(new StringDataEntry("zone", "someString"), 363L, 155L); - GeofencingArgumentEntry geofencingArgumentEntry = new GeofencingArgumentEntry(Map.of(ZONE_1_ID, invalidZoneEntry)); - assertThat(geofencingArgumentEntry.isEmpty()).isTrue(); + assertThatThrownBy(() -> new GeofencingArgumentEntry(Map.of(ZONE_1_ID, invalidZoneEntry))) + .isExactlyInstanceOf(IllegalArgumentException.class) + .hasMessage("The given string value cannot be transformed to Json object: someString"); } @Test From f6a108521414ac8749e210b0a23a9eff348dc4f1 Mon Sep 17 00:00:00 2001 From: Ekaterina Chantsova Date: Wed, 10 Sep 2025 19:12:50 +0300 Subject: [PATCH 133/216] Refactoring --- .../widget/maps/map-model.definition.ts | 63 +++++-------------- .../shared/models/widget/maps/map.models.ts | 35 +++++++++-- 2 files changed, 45 insertions(+), 53 deletions(-) diff --git a/ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts b/ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts index 1be4ecd5c6..a5dc5e3ff4 100644 --- a/ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts +++ b/ui-ngx/src/app/shared/models/widget/maps/map-model.definition.ts @@ -17,21 +17,17 @@ import { EntityAliases, EntityAliasInfo, getEntityAliasId } from '@shared/models/alias.models'; import { FilterInfo, Filters, getFilterId } from '@shared/models/query/query.models'; import { Dashboard } from '@shared/models/dashboard.models'; -import { DataKey, Datasource, datasourcesHasAggregation, DatasourceType, Widget } from '@shared/models/widget.models'; +import { Datasource, datasourcesHasAggregation, DatasourceType, Widget } from '@shared/models/widget.models'; import { additionalMapDataSourcesToDatasources, BaseMapSettings, - CirclesDataLayerSettings, MapDataLayerSettings, MapDataLayerType, MapDataSourceSettings, mapDataSourceSettingsToDatasource, - MapType, - MarkersDataLayerSettings, - PolygonsDataLayerSettings + MapType } from '@shared/models/widget/maps/map.models'; import { WidgetModelDefinition } from '@shared/models/widget/widget-model.definition'; -import { deepClone } from '@core/utils'; interface AliasFilterPair { alias?: EntityAliasInfo, @@ -127,7 +123,7 @@ export const MapModelDefinition: WidgetModelDefinition = { datasources.push(...getMapDataLayersDatasources(settings.circles)); } if (settings.additionalDataSources?.length) { - datasources.push(...getMapDataLayersDatasources(settings.additionalDataSources)); + datasources.push(...additionalMapDataSourcesToDatasources(settings.additionalDataSources)); } return datasources; }, @@ -138,13 +134,13 @@ export const MapModelDefinition: WidgetModelDefinition = { } else { const datasources: Datasource[] = []; if (settings.markers?.length) { - datasources.push(...getMapLatestDataLayersDatasources(settings.markers, 'markers')); + datasources.push(...getMapDataLayersDatasources(settings.markers, true, 'markers')); } if (settings.polygons?.length) { - datasources.push(...getMapLatestDataLayersDatasources(settings.polygons, 'polygons')); + datasources.push(...getMapDataLayersDatasources(settings.polygons, true, 'polygons')); } if (settings.circles?.length) { - datasources.push(...getMapLatestDataLayersDatasources(settings.circles, 'circles')); + datasources.push(...getMapDataLayersDatasources(settings.circles, true, 'circles')); } if (settings.additionalDataSources?.length) { datasources.push(...additionalMapDataSourcesToDatasources(settings.additionalDataSources)); @@ -238,52 +234,21 @@ const prepareAliasAndFilterPair = (dashboard: Dashboard, settings: MapDataSource } } -const getMapDataLayersDatasources = (settings: MapDataLayerSettings[] | MapDataSourceSettings[]): Datasource[] => { +const getMapDataLayersDatasources = (settings: MapDataLayerSettings[], + includeDataKeys = false, dataLayerType?: MapDataLayerType): Datasource[] => { const datasources: Datasource[] = []; settings.forEach((dsSettings) => { - datasources.push(mapDataSourceSettingsToDatasource(dsSettings)); - if ((dsSettings as MapDataLayerSettings).additionalDataSources?.length) { - (dsSettings as MapDataLayerSettings).additionalDataSources.forEach((ds) => { - datasources.push(mapDataSourceSettingsToDatasource(ds)); - }); - } - }); - return datasources; -}; - -const getMapLatestDataLayersDatasources = (settings: MapDataLayerSettings[], - dataLayerType: MapDataLayerType): Datasource[] => { - const datasources: Datasource[] = []; - settings.forEach((dsSettings) => { - const dataKeys: DataKey[] = getMapLatestDataLayerDatasourceDataKeys(dsSettings, dataLayerType); - const datasource: Datasource = mapDataSourceSettingsToDatasource(dsSettings); - datasource.dataKeys.push(...dataKeys); + const datasource: Datasource = mapDataSourceSettingsToDatasource(dsSettings, null, includeDataKeys, dataLayerType); datasources.push(datasource); - if ((dsSettings).additionalDataSources?.length) { - (dsSettings).additionalDataSources.forEach((ds) => { + if (dsSettings.additionalDataSources?.length) { + dsSettings.additionalDataSources.forEach((ds) => { const additionalDatasource: Datasource = mapDataSourceSettingsToDatasource(ds); - additionalDatasource.dataKeys.push(...dataKeys); + if (includeDataKeys) { + additionalDatasource.dataKeys.push(...datasource.dataKeys); + } datasources.push(additionalDatasource); }); } }); return datasources; }; - -const getMapLatestDataLayerDatasourceDataKeys = (settings: MapDataLayerSettings, - dataLayerType: MapDataLayerType): DataKey[] => { - const dataKeys = settings.additionalDataKeys?.length ? deepClone(settings.additionalDataKeys) : []; - switch (dataLayerType) { - case 'markers': - const markersSettings = settings as MarkersDataLayerSettings; - dataKeys.push(markersSettings.xKey, markersSettings.yKey); - break; - case 'polygons': - dataKeys.push((settings as PolygonsDataLayerSettings).polygonKey); - break; - case 'circles': - dataKeys.push((settings as CirclesDataLayerSettings).circleKey); - break; - } - return dataKeys; -}; diff --git a/ui-ngx/src/app/shared/models/widget/maps/map.models.ts b/ui-ngx/src/app/shared/models/widget/maps/map.models.ts index 0468fe1fab..8df70edae3 100644 --- a/ui-ngx/src/app/shared/models/widget/maps/map.models.ts +++ b/ui-ngx/src/app/shared/models/widget/maps/map.models.ts @@ -24,6 +24,7 @@ import { } from '@shared/models/widget.models'; import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { + deepClone, guid, hashCode, isDefinedAndNotNull, @@ -61,19 +62,44 @@ export interface TbMapDatasource extends Datasource { mapDataIds: string[]; } -export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSettings, id = guid()): TbMapDatasource => { +export const mapDataSourceSettingsToDatasource = (settings: MapDataSourceSettings | MapDataLayerSettings, + id = guid(), + includeDataKeys = false, dataLayerType?: MapDataLayerType): TbMapDatasource => { + const dataKeys = includeDataKeys ? mapDataLayerDatasourceDataKeys((settings as MapDataLayerSettings), dataLayerType) : []; return { type: settings.dsType, name: settings.dsLabel, deviceId: settings.dsDeviceId, entityAliasId: settings.dsEntityAliasId, filterId: settings.dsFilterId, - dataKeys: [], + dataKeys: dataKeys, latestDataKeys: [], mapDataIds: [id] }; }; +const mapDataLayerDatasourceDataKeys = (settings: MapDataLayerSettings, + dataLayerType: MapDataLayerType): DataKey[] => { + const dataKeys = settings.additionalDataKeys?.length ? deepClone(settings.additionalDataKeys) : []; + switch (dataLayerType) { + case 'trips': + const tripsSettings = settings as TripsDataLayerSettings; + dataKeys.push(tripsSettings.xKey, tripsSettings.yKey); + break; + case 'markers': + const markersSettings = settings as MarkersDataLayerSettings; + dataKeys.push(markersSettings.xKey, markersSettings.yKey); + break; + case 'polygons': + dataKeys.push((settings as PolygonsDataLayerSettings).polygonKey); + break; + case 'circles': + dataKeys.push((settings as CirclesDataLayerSettings).circleKey); + break; + } + return dataKeys; +}; + export enum DataLayerPatternType { pattern = 'pattern', @@ -658,10 +684,11 @@ export interface AdditionalMapDataSourceSettings extends MapDataSourceSettings { dataKeys: DataKey[]; } -export const additionalMapDataSourcesToDatasources = (additionalMapDataSources: AdditionalMapDataSourceSettings[]): TbMapDatasource[] => { +export const additionalMapDataSourcesToDatasources = (additionalMapDataSources: AdditionalMapDataSourceSettings[], + includeDataKeys = true): TbMapDatasource[] => { return additionalMapDataSources.map(addDs => { const res = mapDataSourceSettingsToDatasource(addDs); - res.dataKeys = addDs.dataKeys; + res.dataKeys = includeDataKeys ? addDs.dataKeys : []; return res; }); }; From 690e1ddacc0bd315dce8b143456906eda4b25952 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Thu, 18 Sep 2025 12:20:24 +0300 Subject: [PATCH 134/216] fixed main entity attributes update in case of profile entity used --- ...CalculatedFieldEntityMessageProcessor.java | 2 +- .../cf/CalculatedFieldIntegrationTest.java | 90 ++++++++++++++++++- 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index f3431390a3..7513ca41e2 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -391,7 +391,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } private Map mapToArguments(CalculatedFieldCtx ctx, AttributeScopeProto scope, List attrDataList) { - return mapToArguments(ctx.getEntityId(), ctx.getMainEntityArguments(), ctx.getMainEntityGeofencingArgumentNames(), scope, attrDataList); + return mapToArguments(entityId, ctx.getMainEntityArguments(), ctx.getMainEntityGeofencingArgumentNames(), scope, attrDataList); } private Map mapToArguments(CalculatedFieldCtx ctx, EntityId entityId, AttributeScopeProto scope, List attrDataList) { diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index 2ba900ba7b..b6a1faa1ed 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -621,6 +621,94 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); } + @Test + public void testGeofencingCalculatedField_withZonesCreatedOnDevice() throws Exception { + // --- Arrange entities --- + Device device = createDevice("GF Test Device", "sn-geo-2"); + + // Allowed zone polygon (square) + String allowedPolygon = "[[50.472000, 30.504000], [50.472000, 30.506000], [50.474000, 30.506000], [50.474000, 30.504000]]"; + // Restricted zone polygon (square) + String restrictedPolygon = "[[50.475000, 30.510000], [50.475000, 30.512000], [50.477000, 30.512000], [50.477000, 30.510000]]"; + + doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, + JacksonUtil.toJsonNode("{\"allowedZone\":" + allowedPolygon + "}")).andExpect(status().isOk()); + + doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, + JacksonUtil.toJsonNode("{\"restrictedZone\":" + restrictedPolygon + "}")).andExpect(status().isOk()); + + // Initial device coordinates (inside Allowed, outside Restricted) + doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", + JacksonUtil.toJsonNode("{\"latitude\":50.4730,\"longitude\":30.5050}")); + + // --- Build CF: GEOFENCING --- + CalculatedField cf = new CalculatedField(); + cf.setEntityId(device.getDeviceProfileId()); + cf.setType(CalculatedFieldType.GEOFENCING); + cf.setName("Geofencing CF"); + cf.setDebugSettings(DebugSettings.off()); + + GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); + + // Coordinates: TS_LATEST on the device + EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); + cfg.setEntityCoordinates(entityCoordinates); + + // Zone groups: ATTRIBUTE on the device + ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("allowedZone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("restrictedZone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + + cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup, "restrictedZones", restrictedZonesGroup)); + + // Output to server attributes + Output out = new Output(); + out.setType(OutputType.ATTRIBUTES); + out.setScope(AttributeScope.SERVER_SCOPE); + cfg.setOutput(out); + + cf.setConfiguration(cfg); + + doPost("/api/calculatedField", cf, CalculatedField.class); + + // --- Assert initial evaluation (ENTERED / OUTSIDE) --- + await().alias("initial geofencing evaluation") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = getServerAttributes(device.getId(), + "allowedZonesEvent", "allowedZonesStatus", "restrictedZonesStatus", "restrictedZonesEvent"); + // --- no restrictedZonesEvent as no transition happened yet + assertThat(attrs).isNotNull().isNotEmpty().hasSize(3); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZonesEvent", "ENTERED") + .containsEntry("allowedZonesStatus", "INSIDE") + .containsEntry("restrictedZonesStatus", "OUTSIDE"); + }); + + // --- delete attributes reported in previous evaluation + doDelete("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/SERVER_SCOPE?keys=allowedZonesEvent,allowedZonesStatus,restrictedZonesStatus", String.class); + + // --- Update restrictedZone by 'restrictedZone' attribute update + doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, + JacksonUtil.toJsonNode("{\"restrictedZone\":" + restrictedPolygon + "}")).andExpect(status().isOk()); + + // --- Assert no transition --- + // --- Assert attributes updated with the same values for restrictedZones --- + // --- Assert attributes updated with the new values for allowedZones --- + await().alias("evaluation after version bump of geo argument") + .atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ArrayNode attrs = getServerAttributes(device.getId(), + "allowedZonesEvent", "allowedZonesStatus", + "restrictedZonesEvent", "restrictedZonesStatus"); + assertThat(attrs).isNotNull().isNotEmpty().hasSize(2); + Map m = kv(attrs); + assertThat(m).containsEntry("allowedZonesStatus", "INSIDE") + .containsEntry("restrictedZonesStatus", "OUTSIDE"); + }); + } + @Test public void testGeofencingCalculatedField_withoutRelationsCreationAndDynamicRefresh() throws Exception { // --- Arrange entities --- @@ -634,12 +722,10 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes Asset allowedZoneAsset = createAsset("Allowed Zone", null); doPost("/api/plugins/telemetry/ASSET/" + allowedZoneAsset.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"zone\":" + allowedPolygon + "}")).andExpect(status().isOk()); - ; Asset restrictedZoneAsset = createAsset("Restricted Zone", null); doPost("/api/plugins/telemetry/ASSET/" + restrictedZoneAsset.getUuidId() + "/attributes/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"zone\":" + restrictedPolygon + "}")).andExpect(status().isOk()); - ; // Relations from device to zones EntityRelation deviceToAllowedZoneRelation = new EntityRelation(); From 0ddf8c3ed8f12a1e87a7e6febf2c0dbd73941584 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Thu, 18 Sep 2025 13:33:34 +0300 Subject: [PATCH 135/216] added semicolon --- .../widget/lib/indicator/liquid-level-widget.component.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts index ec4a9a9beb..b0ffeebf5b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/indicator/liquid-level-widget.component.ts @@ -512,8 +512,7 @@ export class LiquidLevelWidgetComponent implements OnInit { const jQueryContainerElement = $(this.liquidLevelContent.nativeElement); let value: number | string = 'N/A'; if (isNumeric(data)) { - value = +this.widgetUnitsConvertor(convertLiters(this.convertOutputData(percentage), this.widgetUnits as CapacityUnits, ConversionType.from)).toFixed(this.ctx.widgetConfig.decimals || 0) - + value = +this.widgetUnitsConvertor(convertLiters(this.convertOutputData(percentage), this.widgetUnits as CapacityUnits, ConversionType.from)).toFixed(this.ctx.widgetConfig.decimals || 0); } this.valueColor.update(value); const valueTextStyle = cssTextFromInlineStyle({...inlineTextStyle(this.settings.valueFont), @@ -527,9 +526,9 @@ export class LiquidLevelWidgetComponent implements OnInit { let volume: number | string; if (this.widgetUnits !== CapacityUnits.percent) { const volumeInLiters: number = convertLiters(this.volume, this.volumeUnits as CapacityUnits, ConversionType.to); - volume = +this.widgetUnitsConvertor(convertLiters(volumeInLiters, this.widgetUnits as CapacityUnits, ConversionType.from)).toFixed(this.ctx.widgetConfig.decimals || 0) + volume = +this.widgetUnitsConvertor(convertLiters(volumeInLiters, this.widgetUnits as CapacityUnits, ConversionType.from)).toFixed(this.ctx.widgetConfig.decimals || 0); } else { - volume = +this.widgetUnitsConvertor(this.volume).toFixed(this.ctx.widgetConfig.decimals || 0) + volume = +this.widgetUnitsConvertor(this.volume).toFixed(this.ctx.widgetConfig.decimals || 0); } const volumeTextStyle = cssTextFromInlineStyle({...inlineTextStyle(this.settings.volumeFont), From 03c7a01a9954ed1b7455cc67f385042f377b5835 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Thu, 18 Sep 2025 14:15:17 +0300 Subject: [PATCH 136/216] Added CF processing abstract class to CE to simplify merge with PE --- ...tractCalculatedFieldProcessingService.java | 257 ++++++++++++++++++ ...faultCalculatedFieldProcessingService.java | 205 ++------------ .../utils/CalculatedFieldArgumentUtils.java | 13 +- 3 files changed, 281 insertions(+), 194 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java new file mode 100644 index 0000000000..45305ca9e3 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java @@ -0,0 +1,257 @@ +/** + * 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.service.cf; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.ThingsBoardExecutors; +import org.thingsboard.server.common.data.cf.configuration.Argument; +import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.Aggregation; +import org.thingsboard.server.common.data.kv.AttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; +import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.ReadTsKvQuery; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.dao.attributes.AttributesService; +import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.timeseries.TimeseriesService; +import org.thingsboard.server.dao.usagerecord.ApiLimitService; +import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; +import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultKvEntry; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType; +import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformSingleValueArgument; + +@Data +@Slf4j +public abstract class AbstractCalculatedFieldProcessingService { + + protected final AttributesService attributesService; + protected final TimeseriesService timeseriesService; + protected final ApiLimitService apiLimitService; + protected final RelationService relationService; + + protected ListeningExecutorService calculatedFieldCallbackExecutor; + + @PostConstruct + public void init() { + calculatedFieldCallbackExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool( + Math.max(4, Runtime.getRuntime().availableProcessors()), getExecutorNamePrefix())); + } + + @PreDestroy + public void stop() { + if (calculatedFieldCallbackExecutor != null) { + calculatedFieldCallbackExecutor.shutdownNow(); + } + } + + protected abstract String getExecutorNamePrefix(); + + public ListenableFuture fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId) { + Map> argFutures = switch (ctx.getCalculatedField().getType()) { + case GEOFENCING -> fetchGeofencingCalculatedFieldArguments(ctx, entityId, false); + case SIMPLE, SCRIPT -> { + Map> futures = new HashMap<>(); + for (var entry : ctx.getArguments().entrySet()) { + var argEntityId = resolveEntityId(entityId, entry.getValue()); + var argValueFuture = fetchArgumentValue(ctx.getTenantId(), argEntityId, entry.getValue(), System.currentTimeMillis()); + futures.put(entry.getKey(), argValueFuture); + } + yield futures; + } + }; + return Futures.whenAllComplete(argFutures.values()).call(() -> { + var result = createStateByType(ctx); + result.updateState(ctx, resolveArgumentFutures(argFutures)); + return result; + }, MoreExecutors.directExecutor()); + } + + protected EntityId resolveEntityId(EntityId entityId, Argument argument) { + return argument.getRefEntityId() != null ? argument.getRefEntityId() : entityId; + } + + protected Map resolveArgumentFutures(Map> argFutures) { + return argFutures.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, // Keep the key as is + entry -> { + try { + return entry.getValue().get(); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + throw new RuntimeException("Failed to fetch " + entry.getKey() + ": " + cause.getMessage(), cause); + } catch (InterruptedException e) { + throw new RuntimeException("Failed to fetch" + entry.getKey(), e); + } + } + )); + } + + protected Map> fetchGeofencingCalculatedFieldArguments(CalculatedFieldCtx ctx, EntityId entityId, boolean dynamicArgumentsOnly) { + Map> argFutures = new HashMap<>(); + Set> entries = ctx.getArguments().entrySet(); + if (dynamicArgumentsOnly) { + entries = entries.stream() + .filter(entry -> entry.getValue().hasDynamicSource()) + .collect(Collectors.toSet()); + } + for (var entry : entries) { + switch (entry.getKey()) { + case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> + argFutures.put(entry.getKey(), fetchArgumentValue(ctx.getTenantId(), entityId, entry.getValue(), System.currentTimeMillis())); + default -> { + var resolvedEntityIdsFuture = resolveGeofencingEntityIds(ctx.getTenantId(), entityId, entry); + argFutures.put(entry.getKey(), Futures.transformAsync(resolvedEntityIdsFuture, resolvedEntityIds -> + fetchGeofencingKvEntry(ctx.getTenantId(), resolvedEntityIds, entry.getValue()), MoreExecutors.directExecutor())); + } + } + } + return argFutures; + } + + private ListenableFuture> resolveGeofencingEntityIds(TenantId tenantId, EntityId entityId, Map.Entry entry) { + Argument value = entry.getValue(); + if (value.getRefEntityId() != null) { + return Futures.immediateFuture(List.of(value.getRefEntityId())); + } + if (!value.hasDynamicSource()) { + return Futures.immediateFuture(List.of(entityId)); + } + var refDynamicSourceConfiguration = value.getRefDynamicSourceConfiguration(); + return switch (refDynamicSourceConfiguration.getType()) { + case RELATION_QUERY -> { + var configuration = (RelationQueryDynamicSourceConfiguration) refDynamicSourceConfiguration; + if (configuration.isSimpleRelation()) { + yield switch (configuration.getDirection()) { + case FROM -> + Futures.transform(relationService.findByFromAndTypeAsync(tenantId, entityId, configuration.getRelationType(), RelationTypeGroup.COMMON), + configuration::resolveEntityIds, calculatedFieldCallbackExecutor); + case TO -> + Futures.transform(relationService.findByToAndTypeAsync(tenantId, entityId, configuration.getRelationType(), RelationTypeGroup.COMMON), + configuration::resolveEntityIds, calculatedFieldCallbackExecutor); + }; + } + yield Futures.transform(relationService.findByQuery(tenantId, configuration.toEntityRelationsQuery(entityId)), + configuration::resolveEntityIds, calculatedFieldCallbackExecutor); + } + }; + } + + private ListenableFuture fetchGeofencingKvEntry(TenantId tenantId, List geofencingEntities, Argument argument) { + if (argument.getRefEntityKey().getType() != ArgumentType.ATTRIBUTE) { + throw new IllegalStateException("Unsupported argument key type: " + argument.getRefEntityKey().getType()); + } + List>> kvFutures = geofencingEntities.stream() + .map(entityId -> { + var attributesFuture = attributesService.find( + tenantId, + entityId, + argument.getRefEntityKey().getScope(), + argument.getRefEntityKey().getKey() + ); + return Futures.transform(attributesFuture, resultOpt -> + Map.entry(entityId, resultOpt.orElseGet(() -> + new BaseAttributeKvEntry(createDefaultKvEntry(argument), System.currentTimeMillis(), 0L))), + calculatedFieldCallbackExecutor + ); + }).collect(Collectors.toList()); + + ListenableFuture>> allFutures = Futures.allAsList(kvFutures); + + return Futures.transform(allFutures, entries -> ArgumentEntry.createGeofencingValueArgument(entries.stream() + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))), MoreExecutors.directExecutor()); + } + + protected ListenableFuture fetchArgumentValue(TenantId tenantId, EntityId entityId, Argument argument, long startTs) { + return switch (argument.getRefEntityKey().getType()) { + case TS_ROLLING -> fetchTsRolling(tenantId, entityId, argument, startTs); + case ATTRIBUTE -> fetchAttribute(tenantId, entityId, argument, startTs); + case TS_LATEST -> fetchTsLatest(tenantId, entityId, argument, startTs); + }; + } + + private ListenableFuture fetchTsRolling(TenantId tenantId, EntityId entityId, Argument argument, long queryEndTs) { + long argTimeWindow = argument.getTimeWindow() == 0 ? queryEndTs : argument.getTimeWindow(); + long startInterval = queryEndTs - argTimeWindow; + ReadTsKvQuery query = buildTsRollingQuery(tenantId, argument, startInterval, queryEndTs); + + log.trace("[{}][{}] Fetching timeseries for query {}", tenantId, entityId, query); + ListenableFuture> tsRollingFuture = timeseriesService.findAll(tenantId, entityId, List.of(query)); + return Futures.transform(tsRollingFuture, tsRolling -> { + log.debug("[{}][{}] Fetched {} timeseries for query {}", tenantId, entityId, tsRolling == null ? 0 : tsRolling.size(), query); + return ArgumentEntry.createTsRollingArgument(tsRolling, query.getLimit(), argTimeWindow); + }, calculatedFieldCallbackExecutor); + } + + private ListenableFuture fetchAttribute(TenantId tenantId, EntityId entityId, Argument argument, long defaultLastUpdateTs) { + log.trace("[{}][{}] Fetching attribute for key {}", tenantId, entityId, argument.getRefEntityKey()); + var attributeOptFuture = attributesService.find(tenantId, entityId, argument.getRefEntityKey().getScope(), argument.getRefEntityKey().getKey()); + + return Futures.transform(attributeOptFuture, attrOpt -> { + log.debug("[{}][{}] Fetched attribute for key {}: {}", tenantId, entityId, argument.getRefEntityKey(), attrOpt); + AttributeKvEntry attributeKvEntry = attrOpt.orElseGet(() -> new BaseAttributeKvEntry(createDefaultKvEntry(argument), defaultLastUpdateTs, 0L)); + return transformSingleValueArgument(Optional.of(attributeKvEntry)); + }, calculatedFieldCallbackExecutor); + } + + protected ListenableFuture fetchTsLatest(TenantId tenantId, EntityId entityId, Argument argument, long startTs) { + String timeseriesKey = argument.getRefEntityKey().getKey(); + log.trace("[{}][{}] Fetching latest timeseries {}", tenantId, entityId, timeseriesKey); + return transformSingleValueArgument( + Futures.transform( + timeseriesService.findLatest(tenantId, entityId, timeseriesKey), + result -> { + log.debug("[{}][{}] Fetched latest timeseries {}: {}", tenantId, entityId, timeseriesKey, result); + return result.or(() -> Optional.of(new BasicTsKvEntry(System.currentTimeMillis(), createDefaultKvEntry(argument), 0L))); + }, calculatedFieldCallbackExecutor)); + } + + private ReadTsKvQuery buildTsRollingQuery(TenantId tenantId, Argument argument, long startTs, long endTs) { + long maxDataPoints = apiLimitService.getLimit( + tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg); + int argumentLimit = argument.getLimit(); + int limit = argumentLimit == 0 || argumentLimit > maxDataPoints ? (int) maxDataPoints : argumentLimit; + return new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), startTs, endTs, 0, limit, Aggregation.NONE); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java index e4e6fce649..17dca5dd64 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/DefaultCalculatedFieldProcessingService.java @@ -15,16 +15,9 @@ */ package org.thingsboard.server.service.cf; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; -import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.actors.calculatedField.CalculatedFieldTelemetryMsg; import org.thingsboard.server.actors.calculatedField.MultipleTbCallback; import org.thingsboard.server.cluster.TbClusterService; @@ -32,22 +25,11 @@ import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.Argument; -import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.OutputType; -import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.kv.Aggregation; -import org.thingsboard.server.common.data.kv.AttributeKvEntry; -import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; -import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; -import org.thingsboard.server.common.data.kv.BasicTsKvEntry; -import org.thingsboard.server.common.data.kv.ReadTsKvQuery; -import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.data.msg.TbMsgType; -import org.thingsboard.server.common.data.relation.RelationTypeGroup; -import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.ServiceType; @@ -70,74 +52,43 @@ import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; -import org.thingsboard.server.service.cf.ctx.state.TsRollingArgumentEntry; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.Optional; -import java.util.Set; import java.util.UUID; -import java.util.concurrent.ExecutionException; -import java.util.stream.Collectors; import static org.thingsboard.server.common.data.DataConstants.SCOPE; -import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; -import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; -import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createDefaultKvEntry; -import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.createStateByType; -import static org.thingsboard.server.utils.CalculatedFieldArgumentUtils.transformSingleValueArgument; import static org.thingsboard.server.utils.CalculatedFieldUtils.toProto; @TbRuleEngineComponent @Service @Slf4j -@RequiredArgsConstructor -public class DefaultCalculatedFieldProcessingService implements CalculatedFieldProcessingService { +public class DefaultCalculatedFieldProcessingService extends AbstractCalculatedFieldProcessingService implements CalculatedFieldProcessingService { - private final AttributesService attributesService; - private final TimeseriesService timeseriesService; private final TbClusterService clusterService; - private final ApiLimitService apiLimitService; private final PartitionService partitionService; - private final RelationService relationService; - private ListeningExecutorService calculatedFieldCallbackExecutor; - - @PostConstruct - public void init() { - calculatedFieldCallbackExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool( - Math.max(4, Runtime.getRuntime().availableProcessors()), "calculated-field-callback")); + public DefaultCalculatedFieldProcessingService(AttributesService attributesService, + TimeseriesService timeseriesService, + ApiLimitService apiLimitService, + RelationService relationService, + TbClusterService clusterService, + PartitionService partitionService) { + super(attributesService, timeseriesService, apiLimitService, relationService); + this.clusterService = clusterService; + this.partitionService = partitionService; } - @PreDestroy - public void stop() { - if (calculatedFieldCallbackExecutor != null) { - calculatedFieldCallbackExecutor.shutdownNow(); - } + @Override + protected String getExecutorNamePrefix() { + return "calculated-field-callback"; } @Override public ListenableFuture fetchStateFromDb(CalculatedFieldCtx ctx, EntityId entityId) { - Map> argFutures = switch (ctx.getCalculatedField().getType()) { - case GEOFENCING -> fetchGeofencingCalculatedFieldArguments(ctx, entityId, false); - case SIMPLE, SCRIPT -> { - Map> futures = new HashMap<>(); - for (var entry : ctx.getArguments().entrySet()) { - var argEntityId = resolveEntityId(entityId, entry); - var argValueFuture = fetchKvEntry(ctx.getTenantId(), argEntityId, entry.getValue()); - futures.put(entry.getKey(), argValueFuture); - } - yield futures; - } - }; - return Futures.whenAllComplete(argFutures.values()).call(() -> { - var result = createStateByType(ctx); - result.updateState(ctx, resolveArgumentFutures(argFutures)); - return result; - }, MoreExecutors.directExecutor()); + return super.fetchStateFromDb(ctx, entityId); } @Override @@ -149,28 +100,6 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP return resolveArgumentFutures(fetchGeofencingCalculatedFieldArguments(ctx, entityId, true)); } - private Map> fetchGeofencingCalculatedFieldArguments(CalculatedFieldCtx ctx, EntityId entityId, boolean dynamicArgumentsOnly) { - Map> argFutures = new HashMap<>(); - Set> entries = ctx.getArguments().entrySet(); - if (dynamicArgumentsOnly) { - entries = entries.stream() - .filter(entry -> entry.getValue().hasDynamicSource()) - .collect(Collectors.toSet()); - } - for (var entry : entries) { - switch (entry.getKey()) { - case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> - argFutures.put(entry.getKey(), fetchKvEntry(ctx.getTenantId(), entityId, entry.getValue())); - default -> { - var resolvedEntityIdsFuture = resolveGeofencingEntityIds(ctx.getTenantId(), entityId, entry); - argFutures.put(entry.getKey(), Futures.transformAsync(resolvedEntityIdsFuture, resolvedEntityIds -> - fetchGeofencingKvEntry(ctx.getTenantId(), resolvedEntityIds, entry.getValue()), MoreExecutors.directExecutor())); - } - } - } - return argFutures; - } - @Override public Map fetchArgsFromDb(TenantId tenantId, EntityId entityId, Map arguments) { Map> argFutures = new HashMap<>(); @@ -178,28 +107,13 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP if (entry.getValue().hasDynamicSource()) { continue; } - var argEntityId = resolveEntityId(entityId, entry); - var argValueFuture = fetchKvEntry(tenantId, argEntityId, entry.getValue()); + var argEntityId = resolveEntityId(entityId, entry.getValue()); + var argValueFuture = fetchArgumentValue(tenantId, argEntityId, entry.getValue(), System.currentTimeMillis()); argFutures.put(entry.getKey(), argValueFuture); } return resolveArgumentFutures(argFutures); } - private Map resolveArgumentFutures(Map> argFutures) { - return argFutures.entrySet().stream() - .collect(Collectors.toMap( - Entry::getKey, // Keep the key as is - entry -> { - try { - // Resolve the future to get the value - return entry.getValue().get(); - } catch (ExecutionException | InterruptedException e) { - throw new RuntimeException("Error getting future result for key: " + entry.getKey(), e); - } - } - )); - } - @Override public void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, CalculatedFieldResult calculatedFieldResult, List cfIds, TbCallback callback) { try { @@ -278,93 +192,6 @@ public class DefaultCalculatedFieldProcessingService implements CalculatedFieldP return builder.build(); } - - private EntityId resolveEntityId(EntityId entityId, Entry entry) { - return entry.getValue().getRefEntityId() != null ? entry.getValue().getRefEntityId() : entityId; - } - - private ListenableFuture> resolveGeofencingEntityIds(TenantId tenantId, EntityId entityId, Entry entry) { - Argument value = entry.getValue(); - if (value.getRefEntityId() != null) { - return Futures.immediateFuture(List.of(value.getRefEntityId())); - } - if (!value.hasDynamicSource()) { - return Futures.immediateFuture(List.of(entityId)); - } - var refDynamicSourceConfiguration = value.getRefDynamicSourceConfiguration(); - return switch (refDynamicSourceConfiguration.getType()) { - case RELATION_QUERY -> { - var configuration = (RelationQueryDynamicSourceConfiguration) refDynamicSourceConfiguration; - if (configuration.isSimpleRelation()) { - yield switch (configuration.getDirection()) { - case FROM -> - Futures.transform(relationService.findByFromAndTypeAsync(tenantId, entityId, configuration.getRelationType(), RelationTypeGroup.COMMON), - configuration::resolveEntityIds, calculatedFieldCallbackExecutor); - case TO -> - Futures.transform(relationService.findByToAndTypeAsync(tenantId, entityId, configuration.getRelationType(), RelationTypeGroup.COMMON), - configuration::resolveEntityIds, calculatedFieldCallbackExecutor); - }; - } - yield Futures.transform(relationService.findByQuery(tenantId, configuration.toEntityRelationsQuery(entityId)), - configuration::resolveEntityIds, calculatedFieldCallbackExecutor); - } - }; - } - - private ListenableFuture fetchGeofencingKvEntry(TenantId tenantId, List geofencingEntities, Argument argument) { - if (argument.getRefEntityKey().getType() != ArgumentType.ATTRIBUTE) { - throw new IllegalStateException("Unsupported argument key type: " + argument.getRefEntityKey().getType()); - } - List>> kvFutures = geofencingEntities.stream() - .map(entityId -> { - var attributesFuture = attributesService.find( - tenantId, - entityId, - argument.getRefEntityKey().getScope(), - argument.getRefEntityKey().getKey() - ); - return Futures.transform(attributesFuture, resultOpt -> - Map.entry(entityId, resultOpt.orElseGet(() -> - new BaseAttributeKvEntry(createDefaultKvEntry(argument), System.currentTimeMillis(), 0L))), - calculatedFieldCallbackExecutor - ); - }).collect(Collectors.toList()); - - ListenableFuture>> allFutures = Futures.allAsList(kvFutures); - - return Futures.transform(allFutures, entries -> ArgumentEntry.createGeofencingValueArgument(entries.stream() - .collect(Collectors.toMap(Entry::getKey, Entry::getValue))), MoreExecutors.directExecutor()); - } - - private ListenableFuture fetchKvEntry(TenantId tenantId, EntityId entityId, Argument argument) { - return switch (argument.getRefEntityKey().getType()) { - case TS_ROLLING -> fetchTsRolling(tenantId, entityId, argument); - case ATTRIBUTE -> transformSingleValueArgument( - Futures.transform(attributesService.find(tenantId, entityId, argument.getRefEntityKey().getScope(), argument.getRefEntityKey().getKey()), - result -> result.or(() -> Optional.of(new BaseAttributeKvEntry(createDefaultKvEntry(argument), System.currentTimeMillis(), 0L))), - calculatedFieldCallbackExecutor)); - case TS_LATEST -> transformSingleValueArgument( - Futures.transform( - timeseriesService.findLatest(tenantId, entityId, argument.getRefEntityKey().getKey()), - result -> result.or(() -> Optional.of(new BasicTsKvEntry(System.currentTimeMillis(), createDefaultKvEntry(argument), 0L))), - calculatedFieldCallbackExecutor)); - }; - } - - private ListenableFuture fetchTsRolling(TenantId tenantId, EntityId entityId, Argument argument) { - long currentTime = System.currentTimeMillis(); - long timeWindow = argument.getTimeWindow() == 0 ? System.currentTimeMillis() : argument.getTimeWindow(); - long startTs = currentTime - timeWindow; - long maxDataPoints = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxDataPointsPerRollingArg); - int argumentLimit = argument.getLimit(); - int limit = argumentLimit == 0 || argumentLimit > maxDataPoints ? (int) maxDataPoints : argument.getLimit(); - - ReadTsKvQuery query = new BaseReadTsKvQuery(argument.getRefEntityKey().getKey(), startTs, currentTime, 0, limit, Aggregation.NONE); - ListenableFuture> tsRollingFuture = timeseriesService.findAll(tenantId, entityId, List.of(query)); - - return Futures.transform(tsRollingFuture, tsRolling -> tsRolling == null ? new TsRollingArgumentEntry(limit, timeWindow) : ArgumentEntry.createTsRollingArgument(tsRolling, limit, timeWindow), calculatedFieldCallbackExecutor); - } - private static class TbCallbackWrapper implements TbQueueCallback { private final TbCallback callback; diff --git a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java index 055c97efc3..934eadd98f 100644 --- a/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java +++ b/application/src/main/java/org/thingsboard/server/utils/CalculatedFieldArgumentUtils.java @@ -38,12 +38,15 @@ import java.util.Optional; public class CalculatedFieldArgumentUtils { public static ListenableFuture transformSingleValueArgument(ListenableFuture> kvEntryFuture) { - return Futures.transform(kvEntryFuture, kvEntry -> { - if (kvEntry.isPresent() && kvEntry.get().getValue() != null) { - return ArgumentEntry.createSingleValueArgument(kvEntry.get()); - } + return Futures.transform(kvEntryFuture, CalculatedFieldArgumentUtils::transformSingleValueArgument, MoreExecutors.directExecutor()); + } + + public static ArgumentEntry transformSingleValueArgument(Optional kvEntry) { + if (kvEntry.isPresent() && kvEntry.get().getValue() != null) { + return ArgumentEntry.createSingleValueArgument(kvEntry.get()); + } else { return new SingleValueArgumentEntry(); - }, MoreExecutors.directExecutor()); + } } public static KvEntry createDefaultKvEntry(Argument argument) { From 3f83e26d078c24b80986333c9c218770bb4bb3f6 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Tue, 16 Sep 2025 14:22:30 +0300 Subject: [PATCH 137/216] fix Entities Table widget column order --- .../widget-action-dialog.component.html | 2 +- .../widget/widget-config.component.ts | 21 ++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html index e5d006076e..48a1e17044 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/action/widget-action-dialog.component.html @@ -57,7 +57,7 @@ - {{ getCellClickColumnInfo($index, column) }} + {{ getCellClickColumnInfo($index, column) | customTranslate }} (); if (this.modelValue.config?.datasources[0]?.dataKeys?.length) { - configuredColumns.push(...this.keysToCellClickColumns(this.modelValue.config.datasources[0].dataKeys)); + const { + displayEntityLabel, + displayEntityName, + displayEntityType, + entityNameColumnTitle, + entityLabelColumnTitle + } = this.modelValue.config.settings; + const displayEntitiesArray = []; + if (isDefined(displayEntityName)) { + const displayName = entityNameColumnTitle ? entityNameColumnTitle : 'entityName'; + displayEntitiesArray.push({name: displayName, label: displayName}); + } + if (isDefined(displayEntityLabel)) { + const displayLabel = entityLabelColumnTitle ? entityLabelColumnTitle : 'entityLabel'; + displayEntitiesArray.push({name: displayLabel, label: displayLabel}); + } + if (isDefined(displayEntityType)) { + displayEntitiesArray.push({name: 'entityType', label: 'entityType'}); + } + configuredColumns.push(...displayEntitiesArray, ...this.keysToCellClickColumns(this.modelValue.config.datasources[0].dataKeys)); } if (this.modelValue.config?.alarmSource?.dataKeys?.length) { configuredColumns.push(...this.keysToCellClickColumns(this.modelValue.config.alarmSource.dataKeys)); From c9c46dce43d9002ee6e9aa6e6dafc1bcf01b5e9d Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Sun, 21 Sep 2025 15:41:49 +0300 Subject: [PATCH 138/216] Save to custom table node: remove redundant executor per node --- .../TbSaveToCustomCassandraTableNode.java | 36 ++----------------- ...CustomCassandraTableNodeConfiguration.java | 3 +- 2 files changed, 4 insertions(+), 35 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java index e56a3459f8..ebc274a9e0 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java @@ -16,21 +16,16 @@ package org.thingsboard.rule.engine.action; import com.datastax.oss.driver.api.core.ConsistencyLevel; -import com.datastax.oss.driver.api.core.cql.AsyncResultSet; import com.datastax.oss.driver.api.core.cql.BoundStatement; import com.datastax.oss.driver.api.core.cql.BoundStatementBuilder; import com.datastax.oss.driver.api.core.cql.PreparedStatement; import com.datastax.oss.driver.api.core.cql.Statement; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.base.Function; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; -import jakarta.annotation.Nullable; import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -50,8 +45,6 @@ import org.thingsboard.server.dao.nosql.TbResultSetFuture; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; import static org.thingsboard.common.util.DonAsynchron.withCallback; @@ -82,7 +75,6 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { private CassandraCluster cassandraCluster; private ConsistencyLevel defaultWriteLevel; private PreparedStatement saveStmt; - private ExecutorService readResultsProcessingExecutor; private Map fieldsMap; @Override @@ -95,31 +87,19 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { if (!isTableExists()) { throw new TbNodeException("Table '" + TABLE_PREFIX + config.getTableName() + "' does not exist in Cassandra cluster."); } - startExecutor(); saveStmt = getSaveStmt(); } @Override public void onMsg(TbContext ctx, TbMsg msg) { - withCallback(save(msg, ctx), aVoid -> ctx.tellSuccess(msg), e -> ctx.tellFailure(msg, e), ctx.getDbCallbackExecutor()); + withCallback(save(msg, ctx), success -> ctx.tellSuccess(msg), e -> ctx.tellFailure(msg, e), ctx.getDbCallbackExecutor()); } @Override public void destroy() { - stopExecutor(); saveStmt = null; } - private void startExecutor() { - readResultsProcessingExecutor = Executors.newCachedThreadPool(); - } - - private void stopExecutor() { - if (readResultsProcessingExecutor != null) { - readResultsProcessingExecutor.shutdownNow(); - } - } - private boolean isTableExists() { var keyspaceMdOpt = getSession().getMetadata().getKeyspace(cassandraCluster.getKeyspaceName()); return keyspaceMdOpt.map(keyspaceMetadata -> @@ -180,7 +160,7 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { return query.toString(); } - private ListenableFuture save(TbMsg msg, TbContext ctx) { + private TbResultSetFuture save(TbMsg msg, TbContext ctx) { JsonElement data = JsonParser.parseString(msg.getData()); if (!data.isJsonObject()) { throw new IllegalStateException("Invalid message structure, it is not a JSON Object: " + data); @@ -221,7 +201,7 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { if (config.getDefaultTtl() > 0) { stmtBuilder.setInt(i.get(), config.getDefaultTtl()); } - return getFuture(executeAsyncWrite(ctx, stmtBuilder.build()), rs -> null); + return executeAsyncWrite(ctx, stmtBuilder.build()); } } @@ -251,16 +231,6 @@ public class TbSaveToCustomCassandraTableNode implements TbNode { } } - private ListenableFuture getFuture(TbResultSetFuture future, java.util.function.Function transformer) { - return Futures.transform(future, new Function() { - @Nullable - @Override - public T apply(@Nullable AsyncResultSet input) { - return transformer.apply(input); - } - }, readResultsProcessingExecutor); - } - @Override public TbPair upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException { boolean hasChanges = false; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNodeConfiguration.java index 48e0f6d679..7b71c8f705 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNodeConfiguration.java @@ -24,12 +24,10 @@ import java.util.Map; @Data public class TbSaveToCustomCassandraTableNodeConfiguration implements NodeConfiguration { - private String tableName; private Map fieldsMapping; private int defaultTtl; - @Override public TbSaveToCustomCassandraTableNodeConfiguration defaultConfiguration() { TbSaveToCustomCassandraTableNodeConfiguration configuration = new TbSaveToCustomCassandraTableNodeConfiguration(); @@ -40,4 +38,5 @@ public class TbSaveToCustomCassandraTableNodeConfiguration implements NodeConfig configuration.setFieldsMapping(map); return configuration; } + } From 857aa2c648ace00741d3ee53c313f662287ce2f3 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 22 Sep 2025 15:18:45 +0300 Subject: [PATCH 139/216] fixed nextCycleTs calculation for api usages --- .../DefaultTbApiUsageStateService.java | 4 +- .../DefaultTbApiUsageStateServiceTest.java | 50 ++++++++++++++++++- .../common/msg/tools/SchedulerUtils.java | 12 ----- 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java index 3dedea999c..84025b3cee 100644 --- a/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java +++ b/application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java @@ -442,13 +442,13 @@ public class DefaultTbApiUsageStateService extends AbstractPartitionBasedService boolean check(long threshold, long warnThreshold, long value); } - private void checkStartOfNextCycle() { + public void checkStartOfNextCycle() { updateLock.lock(); try { long now = System.currentTimeMillis(); myUsageStates.values().forEach(state -> { if ((state.getNextCycleTs() < now) && (now - state.getNextCycleTs() < TimeUnit.HOURS.toMillis(1))) { - state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextNextMonth()); + state.setCycles(state.getNextCycleTs(), SchedulerUtils.getStartOfNextMonth()); if (log.isTraceEnabled()) { log.trace("[{}][{}] Updating state cycles (currentCycleTs={},nextCycleTs={})", state.getTenantId(), state.getEntityId(), state.getCurrentCycleTs(), state.getNextCycleTs()); } diff --git a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java index 20f8aaf881..969f6de9f5 100644 --- a/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateServiceTest.java @@ -20,9 +20,12 @@ import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.ApiUsageRecordKey; +import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageStateValue; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.ApiUsageStateId; +import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; @@ -33,8 +36,17 @@ import org.thingsboard.server.dao.usagerecord.ApiUsageStateService; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import java.lang.reflect.Field; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; +import java.util.concurrent.TimeUnit; +import static java.time.ZoneOffset.UTC; +import static java.time.temporal.ChronoField.DAY_OF_MONTH; +import static java.time.temporal.ChronoUnit.MONTHS; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; @DaoSqlTest @@ -48,6 +60,7 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest { private TenantId tenantId; private Tenant savedTenant; + private TenantProfile savedTenantProfile; private static final int MAX_ENABLE_VALUE = 5000; private static final long VALUE_WARNING = 4500L; @@ -59,7 +72,7 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest { loginSysAdmin(); TenantProfile tenantProfile = createTenantProfile(); - TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); + savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class); Assert.assertNotNull(savedTenantProfile); Tenant tenant = new Tenant(); @@ -109,6 +122,41 @@ public class DefaultTbApiUsageStateServiceTest extends AbstractControllerTest { assertEquals(ApiUsageStateValue.DISABLED, apiUsageStateService.findTenantApiUsageState(tenantId).getDbStorageState()); } + @Test + public void checkStartOfNextCycle_setsNextCycleToNextMonth() throws Exception { + ApiUsageState apiUsageState = new ApiUsageState(new ApiUsageStateId(UUID.randomUUID())); + apiUsageState.setDbStorageState(ApiUsageStateValue.ENABLED); + apiUsageState.setAlarmExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setSmsExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setTbelExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setReExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setTransportState(ApiUsageStateValue.ENABLED); + apiUsageState.setEmailExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setJsExecState(ApiUsageStateValue.ENABLED); + apiUsageState.setTenantId(tenantId); + apiUsageState.setEntityId(tenantId); + + long now = System.currentTimeMillis(); + long currentCycleTs = now - TimeUnit.DAYS.toMillis(30); + long nextCycleTs = now - TimeUnit.MINUTES.toMillis(5); // < 1h ago + TenantApiUsageState tenantApiUsageState = new TenantApiUsageState(savedTenantProfile, apiUsageState); + tenantApiUsageState.setCycles(currentCycleTs, nextCycleTs); + Map map = new HashMap<>(); + map.put(tenantId, tenantApiUsageState); + + Field fieldToSet = DefaultTbApiUsageStateService.class.getDeclaredField("myUsageStates"); + fieldToSet.setAccessible(true); + fieldToSet.set(service, map); + + service.checkStartOfNextCycle(); + + long firstOfNextMonth = LocalDate.now() + .with((temporal) -> temporal.with(DAY_OF_MONTH, 1) + .plus(1, MONTHS)) + .atStartOfDay(UTC).toInstant().toEpochMilli(); + assertThat(tenantApiUsageState.getNextCycleTs()).isEqualTo(firstOfNextMonth); + } + private TenantProfile createTenantProfile() { TenantProfile tenantProfile = new TenantProfile(); tenantProfile.setName("Tenant Profile"); diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java index e0805c27aa..5b8a241a1a 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/tools/SchedulerUtils.java @@ -60,16 +60,4 @@ public class SchedulerUtils { return LocalDate.now(UTC).with(TemporalAdjusters.firstDayOfNextMonth()).atStartOfDay(zoneId).toInstant().toEpochMilli(); } - public static long getStartOfNextNextMonth() { - return getStartOfNextNextMonth(UTC); - } - - public static long getStartOfNextNextMonth(ZoneId zoneId) { - return LocalDate.now(UTC).with(firstDayOfNextNextMonth()).atStartOfDay(zoneId).toInstant().toEpochMilli(); - } - - public static TemporalAdjuster firstDayOfNextNextMonth() { - return (temporal) -> temporal.with(DAY_OF_MONTH, 1).plus(2, MONTHS); - } - } From 388de2538748e6d031336a9b13b7223b90159354 Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Mon, 22 Sep 2025 15:29:24 +0300 Subject: [PATCH 140/216] fixed empty link bug --- .../components/relation/relation-table.component.html | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html index 9afaa889b4..7e75a00067 100644 --- a/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html +++ b/ui-ngx/src/app/modules/home/components/relation/relation-table.component.html @@ -117,9 +117,13 @@ {{ 'relation.to-entity-name' | translate }} - - {{ relation.toName | customTranslate }} - + @if(relation.entityURL){ + + {{ relation.toName | customTranslate }} + + } @else { + {{ relation.toName | customTranslate }} + } From 3fb6a30c507ef199713ced30ca327abdd7fa9072 Mon Sep 17 00:00:00 2001 From: Ekaterina Chantsova Date: Mon, 22 Sep 2025 20:04:47 +0300 Subject: [PATCH 141/216] Timewindow: clear parameters depending on selected aggregation function --- .../time/datapoints-limit.component.ts | 29 ++++++++++++------- .../src/app/shared/models/time/time.models.ts | 7 +++-- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/ui-ngx/src/app/shared/components/time/datapoints-limit.component.ts b/ui-ngx/src/app/shared/components/time/datapoints-limit.component.ts index 02470cc949..e2799bd0fc 100644 --- a/ui-ngx/src/app/shared/components/time/datapoints-limit.component.ts +++ b/ui-ngx/src/app/shared/components/time/datapoints-limit.component.ts @@ -29,6 +29,7 @@ import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { TimeService } from '@core/services/time.service'; import { takeUntil } from 'rxjs/operators'; import { Subject } from 'rxjs'; +import { isDefined } from '@core/utils'; @Component({ selector: 'tb-datapoints-limit', @@ -69,7 +70,11 @@ export class DatapointsLimitComponent implements ControlValueAccessor, Validator @Input() disabled: boolean; - private propagateChange = (v: any) => { }; + private propagateChangeValue: any; + + private propagateChange = (v: any) => { + this.propagateChangeValue = v; + }; private destroy$ = new Subject(); @@ -79,6 +84,9 @@ export class DatapointsLimitComponent implements ControlValueAccessor, Validator registerOnChange(fn: any): void { this.propagateChange = fn; + if (isDefined(this.propagateChangeValue)) { + this.propagateChange(this.propagateChangeValue); + } } registerOnTouched(fn: any): void { @@ -115,19 +123,20 @@ export class DatapointsLimitComponent implements ControlValueAccessor, Validator } } - private checkLimit(limit?: number): number { - if (!limit || limit < this.minDatapointsLimit()) { - return this.minDatapointsLimit(); + writeValue(value: number | null): void { + this.modelValue = value; + let limit = this.modelValue; + if (!limit) { + limit = Math.ceil(this.maxDatapointsLimit() / 2); + } else if (limit < this.minDatapointsLimit()) { + limit = this.minDatapointsLimit(); } else if (limit > this.maxDatapointsLimit()) { - return this.maxDatapointsLimit(); + limit = this.maxDatapointsLimit(); } - return limit; - } - writeValue(value: number | null): void { - this.modelValue = this.checkLimit(value); + this.updateView(limit); this.datapointsLimitFormGroup.patchValue( - { limit: this.modelValue }, {emitEvent: false} + { limit: limit }, {emitEvent: false} ); } diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index 35ff3a0572..03ee9e626a 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -1125,6 +1125,7 @@ export const cloneSelectedTimewindow = (timewindow: Timewindow): Timewindow => { export const clearTimewindowConfig = (timewindow: Timewindow, quickIntervalOnly: boolean, historyOnly: boolean, hasAggregation: boolean, hasTimezone = true): Timewindow => { + const noneAggregation = hasAggregation && timewindow.aggregation?.type === AggregationType.NONE; if (timewindow.selectedTab === TimewindowType.REALTIME) { if (quickIntervalOnly || timewindow.realtime.realtimeType === RealtimeWindowType.INTERVAL) { delete timewindow.realtime.timewindowMs; @@ -1138,7 +1139,7 @@ export const clearTimewindowConfig = (timewindow: Timewindow, quickIntervalOnly: delete timewindow.history?.quickInterval; delete timewindow.history?.interval; - if (!hasAggregation) { + if (!hasAggregation || noneAggregation) { delete timewindow.realtime.interval; } } else { @@ -1162,13 +1163,15 @@ export const clearTimewindowConfig = (timewindow: Timewindow, quickIntervalOnly: delete timewindow.realtime?.quickInterval; delete timewindow.realtime?.interval; - if (!hasAggregation) { + if (!hasAggregation || noneAggregation) { delete timewindow.history.interval; } } if (!hasAggregation) { delete timewindow.aggregation; + } else if (!noneAggregation) { + delete timewindow.aggregation.limit; } if (historyOnly) { From e34a2fe268ad08c15df6fed859751176772967ec Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 23 Sep 2025 17:31:51 +0300 Subject: [PATCH 142/216] fixed /api/alarmsQuery/find api to retrieve entity latest values --- .../query/DefaultEntityQueryService.java | 2 +- .../controller/EntityQueryControllerTest.java | 61 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java index 45a73072f5..f39769526a 100644 --- a/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java +++ b/application/src/main/java/org/thingsboard/server/service/query/DefaultEntityQueryService.java @@ -238,7 +238,7 @@ public class DefaultEntityQueryService implements EntityQueryService { entitiesSortOrder = sortOrder; } EntityDataPageLink edpl = new EntityDataPageLink(maxEntitiesPerAlarmSubscription, 0, null, entitiesSortOrder); - return new EntityDataQuery(query.getEntityFilter(), edpl, null, null, query.getKeyFilters()); + return new EntityDataQuery(query.getEntityFilter(), edpl, query.getEntityFields(), query.getLatestValues(), query.getKeyFilters()); } @Override diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 1f80c1f379..93d67ccd4b 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -519,6 +519,67 @@ public class EntityQueryControllerTest extends AbstractControllerTest { Assert.assertEquals(1, filteredAssetAlamData.getTotalElements()); } + @Test + public void testFindAlarmsWithEntityFilterAndLatestValues() throws Exception { + loginTenantAdmin(); + List devices = new ArrayList<>(); + List temps = new ArrayList<>(); + List deviceNames = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + Device device = new Device(); + device.setCustomerId(customerId); + device.setName("Device" + i); + device.setType("default"); + device.setLabel("testLabel" + (int) (Math.random() * 1000)); + device = doPost("/api/device", device, Device.class); + devices.add(device); + deviceNames.add(device.getName()); + + int temp = i * 10; + temps.add(String.valueOf(temp)); + JsonNode content = JacksonUtil.toJsonNode("{\"temperature\": " + temp + "}"); + doPost("/api/plugins/telemetry/" + EntityType.DEVICE.name() + "/" + device.getUuidId() + "/timeseries/SERVER_SCOPE", content) + .andExpect(status().isOk()); + Thread.sleep(1); + } + + for (int i = 0; i < devices.size(); i++) { + Alarm alarm = new Alarm(); + alarm.setCustomerId(customerId); + alarm.setOriginator(devices.get(i).getId()); + String type = "device alarm" + i; + alarm.setType(type); + alarm.setSeverity(AlarmSeverity.WARNING); + doPost("/api/alarm", alarm, Alarm.class); + Thread.sleep(1); + } + + AlarmDataPageLink pageLink = new AlarmDataPageLink(); + pageLink.setPage(0); + pageLink.setPageSize(100); + pageLink.setSortOrder(new EntityDataSortOrder(new EntityKey(EntityKeyType.ALARM_FIELD, "created_time"))); + + List alarmFields = new ArrayList<>(); + alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "type")); + + List entityFields = new ArrayList<>(); + entityFields.add(new EntityKey(EntityKeyType.ENTITY_FIELD, "name")); + + List latestValues = new ArrayList<>(); + latestValues.add(new EntityKey(EntityKeyType.TIME_SERIES, "temperature")); + + EntityTypeFilter deviceTypeFilter = new EntityTypeFilter(); + deviceTypeFilter.setEntityType(EntityType.DEVICE); + AlarmDataQuery deviceAlarmQuery = new AlarmDataQuery(deviceTypeFilter, pageLink, entityFields, latestValues, null, alarmFields); + + PageData alarmPageData = findAlarmsByQueryAndCheck(deviceAlarmQuery, 10); + List retrievedAlarmTemps = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()).toList(); + assertThat(retrievedAlarmTemps).containsExactlyInAnyOrderElementsOf(temps); + + List retrievedDeviceNames = alarmPageData.getData().stream().map(alarmData -> alarmData.getLatest().get(EntityKeyType.ENTITY_FIELD).get("name").getValue()).toList(); + assertThat(retrievedDeviceNames).containsExactlyInAnyOrderElementsOf(deviceNames); + } + private void testCountAlarmsByQuery(List alarms) throws Exception { AlarmCountQuery countQuery = new AlarmCountQuery(); From 599ccdc43c584fee6d0a238133f1e0d1117b33d8 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 12 Aug 2025 18:30:42 +0300 Subject: [PATCH 143/216] added NoXss validation --- .../server/common/data/mobile/bundle/MobileAppBundle.java | 3 +++ .../server/common/data/notification/rule/NotificationRule.java | 1 + .../common/data/notification/rule/NotificationRuleConfig.java | 2 ++ .../template/DeliveryMethodNotificationTemplate.java | 2 ++ 4 files changed, 8 insertions(+) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java index 2190972d7e..b023ba89c3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/mobile/bundle/MobileAppBundle.java @@ -30,6 +30,7 @@ import org.thingsboard.server.common.data.id.MobileAppId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.mobile.layout.MobileLayoutConfig; import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; @EqualsAndHashCode(callSuper = true) @Data @@ -40,9 +41,11 @@ public class MobileAppBundle extends BaseData implements HasT private TenantId tenantId; @Schema(description = "Application bundle title. Cannot be empty", requiredMode = Schema.RequiredMode.REQUIRED) @NotBlank + @NoXss @Length(fieldName = "title") private String title; @Schema(description = "Application bundle description.") + @NoXss @Length(fieldName = "description") private String description; @Schema(description = "Android application id") diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java index 81b5e0ccfb..fe87e0f966 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRule.java @@ -62,6 +62,7 @@ public class NotificationRule extends BaseData implements Ha @Valid private NotificationRuleRecipientsConfig recipientsConfig; + @Valid private NotificationRuleConfig additionalConfig; private NotificationRuleId externalId; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleConfig.java index 9103086b7c..013c0ae662 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/NotificationRuleConfig.java @@ -16,12 +16,14 @@ package org.thingsboard.server.common.data.notification.rule; import lombok.Data; +import org.thingsboard.server.common.data.validation.NoXss; import java.io.Serializable; @Data public class NotificationRuleConfig implements Serializable { + @NoXss private String description; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java index e660d49bca..d9b9df0fdf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java @@ -24,6 +24,7 @@ import jakarta.validation.constraints.NotEmpty; import lombok.Data; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; +import org.thingsboard.server.common.data.validation.NoXss; import java.util.List; @@ -43,6 +44,7 @@ public abstract class DeliveryMethodNotificationTemplate { private boolean enabled; @NotEmpty + @NoXss protected String body; public DeliveryMethodNotificationTemplate(DeliveryMethodNotificationTemplate other) { From 33df79cd12d5fa6b45e8da3247554f656494c468 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 13 Aug 2025 11:19:16 +0300 Subject: [PATCH 144/216] added sanitize for widget action name on delete --- .../widget/action/manage-widget-actions.component.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts index a9b9d207d0..01404f7c6e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/action/manage-widget-actions.component.ts @@ -24,6 +24,7 @@ import { NgZone, OnDestroy, OnInit, + SecurityContext, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @@ -53,6 +54,7 @@ import { import { deepClone } from '@core/utils'; import { hidePageSizePixelValue } from '@shared/models/constants'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'tb-manage-widget-actions', @@ -106,7 +108,8 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni private dialogs: DialogService, private cd: ChangeDetectorRef, private elementRef: ElementRef, - private zone: NgZone) { + private zone: NgZone, + private sanitizer: DomSanitizer) { super(); const sortOrder: SortOrder = { property: 'actionSourceName', direction: Direction.ASC }; this.pageLink = new PageLink(10, 0, null, sortOrder); @@ -289,7 +292,8 @@ export class ManageWidgetActionsComponent extends PageComponent implements OnIni } const title = this.translate.instant('widget-config.delete-action-title'); const content = this.translate.instant('widget-config.delete-action-text', {actionName: action.name}); - this.dialogs.confirm(title, content, + const safeContent = this.sanitizer.sanitize(SecurityContext.HTML, content); + this.dialogs.confirm(title, safeContent, this.translate.instant('action.no'), this.translate.instant('action.yes'), true).subscribe( (res) => { From 1984615d5ac1a2fee2801b45275bd9e91704ebf9 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 24 Sep 2025 11:45:18 +0300 Subject: [PATCH 145/216] rolled back @Noxss for body that can contain html --- .../template/DeliveryMethodNotificationTemplate.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java index d9b9df0fdf..e660d49bca 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/notification/template/DeliveryMethodNotificationTemplate.java @@ -24,7 +24,6 @@ import jakarta.validation.constraints.NotEmpty; import lombok.Data; import lombok.NoArgsConstructor; import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod; -import org.thingsboard.server.common.data.validation.NoXss; import java.util.List; @@ -44,7 +43,6 @@ public abstract class DeliveryMethodNotificationTemplate { private boolean enabled; @NotEmpty - @NoXss protected String body; public DeliveryMethodNotificationTemplate(DeliveryMethodNotificationTemplate other) { From 95ed1408f3c8cf38e09ecb6e9ed47eac0c09629b Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 17 Sep 2025 12:25:23 +0300 Subject: [PATCH 146/216] fixed XSS vulnerabilities in the Rule node --- .../src/app/core/interceptors/global-http-interceptor.ts | 4 +++- ui-ngx/src/app/core/utils.ts | 7 ++++++- .../home/pages/rulechain/rule-node-details.component.ts | 1 + .../home/pages/rulechain/rulechain-page.component.ts | 9 ++++++++- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts index 12d3c78817..dccda985b4 100644 --- a/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts +++ b/ui-ngx/src/app/core/interceptors/global-http-interceptor.ts @@ -30,6 +30,7 @@ import { DialogService } from '@core/services/dialog.service'; import { TranslateService } from '@ngx-translate/core'; import { parseHttpErrorMessage } from '@core/utils'; import { getInterceptorConfig } from './interceptor.util'; +import { DomSanitizer } from '@angular/platform-browser'; const tmpHeaders = {}; @@ -46,6 +47,7 @@ export class GlobalHttpInterceptor implements HttpInterceptor { private dialogService: DialogService, private translate: TranslateService, private authService: AuthService, + private sanitizer: DomSanitizer ) {} intercept(req: HttpRequest, next: HttpHandler): Observable> { @@ -129,7 +131,7 @@ export class GlobalHttpInterceptor implements HttpInterceptor { } if (unhandled && !ignoreErrors) { - const errorMessageWithTimeout = parseHttpErrorMessage(errorResponse, this.translate, req.responseType); + const errorMessageWithTimeout = parseHttpErrorMessage(errorResponse, this.translate, req.responseType, this.sanitizer); this.showError(errorMessageWithTimeout.message, errorMessageWithTimeout.timeout); } return throwError(() => errorResponse); diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 0fc948ac96..8ef3dc074e 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -31,6 +31,8 @@ import { isNotEmptyTbFunction, TbFunction } from '@shared/models/js-function.models'; +import { DomSanitizer } from '@angular/platform-browser'; +import { SecurityContext } from '@angular/core'; const varsRegex = /\${([^}]*)}/g; @@ -809,7 +811,7 @@ export function getEntityDetailsPageURL(id: string, entityType: EntityType): str } export function parseHttpErrorMessage(errorResponse: HttpErrorResponse, - translate: TranslateService, responseType?: string): {message: string; timeout: number} { + translate: TranslateService, responseType?: string, sanitizer?:DomSanitizer): {message: string; timeout: number} { let error = null; let errorMessage: string; let timeout = 0; @@ -837,6 +839,9 @@ export function parseHttpErrorMessage(errorResponse: HttpErrorResponse, errorText += errorKey ? translate.instant(errorKey) : errorResponse.statusText; errorMessage = errorText; } + if(sanitizer) { + errorMessage = sanitizer.sanitize(SecurityContext.HTML,errorMessage); + } return {message: errorMessage, timeout}; } diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts index 05f7261c44..3f52cc0f61 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts @@ -22,6 +22,7 @@ import { OnDestroy, OnInit, Output, + SecurityContext, SimpleChanges, ViewChild } from '@angular/core'; diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts index 2ac9b806e6..f7ead66646 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rulechain-page.component.ts @@ -26,6 +26,7 @@ import { OnInit, QueryList, Renderer2, + SecurityContext, SkipSelf, ViewChild, ViewChildren, @@ -97,6 +98,7 @@ import { HttpStatusCode } from '@angular/common/http'; import { TbContextMenuEvent } from '@shared/models/jquery-event.models'; import { EntityDebugSettings } from '@shared/models/entity.models'; import Timeout = NodeJS.Timeout; +import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'tb-rulechain-page', @@ -273,6 +275,7 @@ export class RuleChainPageComponent extends PageComponent private renderer: Renderer2, private viewContainerRef: ViewContainerRef, private changeDetector: ChangeDetectorRef, + private sanitizer:DomSanitizer, public dialog: MatDialog, public dialogService: DialogService, public fb: FormBuilder) { @@ -1360,9 +1363,13 @@ export class RuleChainPageComponent extends PageComponent name = node.name; desc = this.translate.instant(ruleNodeTypeDescriptors.get(node.component.type).name) + ' - ' + node.component.name; if (node.additionalInfo) { - details = node.additionalInfo.description; + details = this.sanitizer.sanitize(SecurityContext.HTML, node.additionalInfo.description); } } + + name = this.sanitizer.sanitize(SecurityContext.HTML, name); + desc = this.sanitizer.sanitize(SecurityContext.HTML, desc); + let tooltipContent = '
' + '
' + '
' + name + '
' + From 986d6289b9e57d4c224d609c5776585380d3d94e Mon Sep 17 00:00:00 2001 From: Maksym Tsymbarov Date: Wed, 24 Sep 2025 13:16:04 +0300 Subject: [PATCH 147/216] remove unused dependency --- .../modules/home/pages/rulechain/rule-node-details.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts index 3f52cc0f61..05f7261c44 100644 --- a/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts +++ b/ui-ngx/src/app/modules/home/pages/rulechain/rule-node-details.component.ts @@ -22,7 +22,6 @@ import { OnDestroy, OnInit, Output, - SecurityContext, SimpleChanges, ViewChild } from '@angular/core'; From f1b51cf2adc6970da4bb9efd78777eebbee3d399 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Thu, 25 Sep 2025 10:10:40 +0300 Subject: [PATCH 148/216] Send notification dialog: fix issue for Microsoft Teams when users cannot navigate back if an error occurs. --- .../notification/sent/sent-notification-dialog.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html index cb157a0904..424d952c3e 100644 --- a/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html +++ b/ui-ngx/src/app/modules/home/pages/notification/sent/sent-notification-dialog.component.html @@ -233,7 +233,7 @@
{{ preview.processedTemplates.MICROSOFT_TEAMS.subject }}
{{ preview.processedTemplates.MICROSOFT_TEAMS.body }} - +
From 7565fe3fa9e22c5b541b765d65b83d9212f13d53 Mon Sep 17 00:00:00 2001 From: deaflynx Date: Thu, 25 Sep 2025 10:23:24 +0300 Subject: [PATCH 149/216] Add tooltip truncation to entity version restore header. --- .../home/components/vc/entity-version-restore.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html index 1183223850..53837fd7c5 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html @@ -17,7 +17,7 @@ -->
-

{{ 'version-control.restore-entity-from-version' | translate: {versionName} }}

+

{{ 'version-control.restore-entity-from-version' | translate: {versionName} }}

Date: Thu, 25 Sep 2025 17:02:58 +0300 Subject: [PATCH 150/216] deleted full option as it makes UI to hang --- application/src/main/resources/thingsboard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 68cd479411..944520c4e5 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1542,7 +1542,7 @@ swagger: version: "${SWAGGER_VERSION:}" # The group name (definition) on the API doc UI page. group_name: "${SWAGGER_GROUP_NAME:thingsboard}" - # Control the initial display state of API operations and tags (none, list or full) + # Control the initial display state of API operations and tags (none or list) doc_expansion: "${SWAGGER_DOC_EXPANSION:list}" # Queue configuration parameters From ba0949e59cf5ffbb6c4d28bd3ffbd56f88dd0df9 Mon Sep 17 00:00:00 2001 From: Vladyslav Prykhodko Date: Thu, 25 Sep 2025 17:31:56 +0300 Subject: [PATCH 151/216] Update entity-version-restore.component.html --- .../home/components/vc/entity-version-restore.component.html | 1 - 1 file changed, 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html index 2e3f37b87f..e4312e0c98 100644 --- a/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html +++ b/ui-ngx/src/app/modules/home/components/vc/entity-version-restore.component.html @@ -15,7 +15,6 @@ limitations under the License. --> - @if (!versionLoadResult$) { @if (entityDataInfo) { From 9384a4b5309ad3fb2f7d088a088ba14f8293659b Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Thu, 25 Sep 2025 22:00:35 +0300 Subject: [PATCH 152/216] Refactoring and simplified --- .../service/edge/rpc/EdgeGrpcService.java | 87 +++++++------------ 1 file changed, 31 insertions(+), 56 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index d0b997d7b9..f92e89823a 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -68,8 +68,17 @@ import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import java.io.IOException; import java.io.InputStream; -import java.util.*; -import java.util.concurrent.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; @@ -84,15 +93,13 @@ import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAS @TbCoreComponent public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService { - private static final int DESTROY_SESSION_MAX_ATTEMPTS = 10; - private final ConcurrentMap sessions = new ConcurrentHashMap<>(); private final ConcurrentMap sessionNewEventsLocks = new ConcurrentHashMap<>(); private final Map sessionNewEvents = new HashMap<>(); private final ConcurrentMap> sessionEdgeEventChecks = new ConcurrentHashMap<>(); private final ConcurrentMap> localSyncEdgeRequests = new ConcurrentHashMap<>(); private final ConcurrentMap edgeEventsMigrationProcessed = new ConcurrentHashMap<>(); - private final Queue zombieSessions = new ConcurrentLinkedQueue<>(); + private final List zombieSessions = new ArrayList<>(); @Value("${edges.rpc.port}") private int rpcPort; @@ -154,8 +161,6 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private ScheduledExecutorService executorService; - private ScheduledExecutorService zombieSessionsExecutorService; - @AfterStartUp(order = AfterStartUp.REGULAR_SERVICE) public void onStartUp() { log.info("Initializing Edge RPC service!"); @@ -187,9 +192,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i this.edgeEventProcessingExecutorService = ThingsBoardExecutors.newScheduledThreadPool(schedulerPoolSize, "edge-event-check-scheduler"); this.sendDownlinkExecutorService = ThingsBoardExecutors.newScheduledThreadPool(sendSchedulerPoolSize, "edge-send-scheduler"); this.executorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edge-service"); - this.zombieSessionsExecutorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("zombie-sessions"); - this.executorService.scheduleAtFixedRate(this::destroyKafkaSessionIfDisconnectedAndConsumerActive, 60, 60, TimeUnit.SECONDS); - this.zombieSessionsExecutorService.scheduleAtFixedRate(this::cleanupZombieSessions, 30, 60, TimeUnit.SECONDS); + this.executorService.scheduleAtFixedRate(this::cleanupZombieSessions, 60, 60, TimeUnit.SECONDS); log.info("Edge RPC service initialized!"); } @@ -215,9 +218,6 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i if (executorService != null) { executorService.shutdownNow(); } - if(zombieSessionsExecutorService != null){ - zombieSessionsExecutorService.shutdownNow(); - } } @Override @@ -501,13 +501,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i sessionNewEvents.remove(edgeId); } finally { newEventLock.unlock(); - } - boolean destroySessionResult = destroySession(toRemove); - if(!destroySessionResult){ - log.error("[{}][{}] Session destroy failed for edge [{}] with session id [{}]. Adding to zombie queue for later cleanup.", - edge.getTenantId(), edgeId, edge.getName(), sessionId); - zombieSessions.add(toRemove); - } + }destroySession(toRemove); TenantId tenantId = toRemove.getEdge().getTenantId(); save(tenantId, edgeId, ACTIVITY_STATE, false); long lastDisconnectTs = System.currentTimeMillis(); @@ -520,19 +514,14 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i edgeIdServiceIdCache.evict(edgeId); } - private boolean destroySession(EdgeGrpcSession session) { + private void destroySession(EdgeGrpcSession session) { try (session) { - for (int i = 0; i < DESTROY_SESSION_MAX_ATTEMPTS; i++) { - if (session.destroy()) { - return true; - } else { - try { - Thread.sleep(100); - } catch (InterruptedException ignored) {} - } + if (!session.destroy()) { + log.warn("[{}][{}] Session destroy failed for edge [{}] with session id [{}]. Adding to zombie queue for later cleanup.", + session.getTenantId(), session.getEdge().getId(), session.getEdge().getName(), session.getSessionId()); + zombieSessions.add(session); } } - return false; } private void save(TenantId tenantId, EdgeId edgeId, String key, long value) { @@ -639,7 +628,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i } } - private void destroyKafkaSessionIfDisconnectedAndConsumerActive() { + private void cleanupZombieSessions() { try { List toRemove = new ArrayList<>(); for (EdgeGrpcSession session : sessions.values()) { @@ -660,33 +649,19 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i } } } + zombieSessions.removeIf(zombie -> { + if (zombie.destroy()) { + log.info("[{}][{}] Successfully cleaned up zombie session [{}] for edge [{}].", + zombie.getTenantId(), zombie.getEdge().getId(), zombie.getSessionId(), zombie.getEdge().getName()); + return true; + } else { + log.warn("[{}][{}] Failed to remove zombie session [{}] for edge [{}].", + zombie.getTenantId(), zombie.getEdge().getId(), zombie.getSessionId(), zombie.getEdge().getName()); + return false; + } + }); } catch (Exception e) { log.warn("Failed to cleanup kafka sessions", e); } } - - - private void cleanupZombieSessions() { - int zombiesToProcess = zombieSessions.size(); - if (zombiesToProcess == 0) { - return; - } - log.info("Found {} zombie sessions in the queue. Starting cleanup cycle.", zombiesToProcess); - for (int i = 0; i < zombiesToProcess; i++) { - EdgeGrpcSession zombie = zombieSessions.poll(); - if (zombie == null) { - break; - } - log.warn("[{}] Attempting to clean up zombie session [{}] for edge [{}].", - zombie.getTenantId(), zombie.getSessionId(), zombie.getEdge().getId()); - if (!destroySession(zombie)) { - log.warn("[{}] Zombie session [{}] cleanup failed again. Re-queuing for next attempt.", - zombie.getTenantId(), zombie.getSessionId()); - zombieSessions.add(zombie); - } else { - log.info("[{}] Successfully cleaned up zombie session [{}].", - zombie.getTenantId(), zombie.getSessionId()); - } - } - } } From 5edc8cb277672b6093c614f701cf52d3e4a4dd61 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Thu, 25 Sep 2025 22:02:10 +0300 Subject: [PATCH 153/216] Line formatted --- .../thingsboard/server/service/edge/rpc/EdgeGrpcService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index f92e89823a..9fcb7425b2 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -501,7 +501,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i sessionNewEvents.remove(edgeId); } finally { newEventLock.unlock(); - }destroySession(toRemove); + } + destroySession(toRemove); TenantId tenantId = toRemove.getEdge().getTenantId(); save(tenantId, edgeId, ACTIVITY_STATE, false); long lastDisconnectTs = System.currentTimeMillis(); From fc9c692e0b3ca39fc439513c1a5d9232ab1b867c Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 26 Sep 2025 11:37:15 +0300 Subject: [PATCH 154/216] UI: Add support UTF-8 symbols in export file name --- .../import-export/import-export.service.ts | 95 +++++++------------ 1 file changed, 36 insertions(+), 59 deletions(-) diff --git a/ui-ngx/src/app/shared/import-export/import-export.service.ts b/ui-ngx/src/app/shared/import-export/import-export.service.ts index 94960cb838..d7c6d5880a 100644 --- a/ui-ngx/src/app/shared/import-export/import-export.service.ts +++ b/ui-ngx/src/app/shared/import-export/import-export.service.ts @@ -22,7 +22,6 @@ import { AppState } from '@core/core.state'; import { ActionNotificationShow } from '@core/notification/notification.actions'; import { BreakpointId, Dashboard, DashboardLayoutId } from '@shared/models/dashboard.models'; import { deepClone, guid, isDefined, isNotEmptyStr, isObject, isString, isUndefined } from '@core/utils'; -import { WINDOW } from '@core/services/window.service'; import { DOCUMENT } from '@angular/common'; import { AliasesInfo, @@ -100,8 +99,7 @@ type SupportEntityResources = 'includeResourcesInExportWidgetTypes' | 'includeRe @Injectable() export class ImportExportService { - constructor(@Inject(WINDOW) private window: Window, - @Inject(DOCUMENT) private document: Document, + constructor(@Inject(DOCUMENT) private document: Document, private store: Store, private translate: TranslateService, private dashboardService: DashboardService, @@ -177,9 +175,7 @@ export class ImportExportService { public exportCalculatedField(calculatedFieldId: string): void { this.calculatedFieldsService.getCalculatedFieldById(calculatedFieldId).subscribe({ next: (calculatedField) => { - let name = calculatedField.name; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(this.prepareCalculatedFieldExport(calculatedField), name); + this.exportToPc(this.prepareCalculatedFieldExport(calculatedField), calculatedField.name, true); }, error: (e) => { this.handleExportError(e, 'calculated-fields.export-failed-error'); @@ -200,9 +196,7 @@ export class ImportExportService { this.updateUserSettingsIncludeResourcesIfNeeded(includeResources, result.include, 'includeResourcesInExportDashboard'); this.dashboardService.exportDashboard(dashboardId, result.include).subscribe({ next: (dashboard) => { - let name = dashboard.title; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(this.prepareDashboardExport(dashboard), name); + this.exportToPc(this.prepareDashboardExport(dashboard), dashboard.title, true); }, error: (e) => { this.handleExportError(e, 'dashboard.export-failed-error'); @@ -261,9 +255,8 @@ export class ImportExportService { widgetTitle: string, breakpoint: BreakpointId) { const widgetItem = this.itembuffer.prepareWidgetItem(dashboard, sourceState, sourceLayout, widget, breakpoint); const widgetDefaultName = this.widgetService.getWidgetInfoFromCache(widget.typeFullFqn).widgetName; - let fileName = widgetDefaultName + (isNotEmptyStr(widgetTitle) ? `_${widgetTitle}` : ''); - fileName = fileName.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(this.prepareExport(widgetItem), fileName); + const fileName = widgetDefaultName + (isNotEmptyStr(widgetTitle) ? `_${widgetTitle}` : ''); + this.exportToPc(this.prepareExport(widgetItem), fileName, true); } public importWidget(dashboard: Dashboard, targetState: string, @@ -360,9 +353,7 @@ export class ImportExportService { this.updateUserSettingsIncludeResourcesIfNeeded(includeResources, result.include, 'includeResourcesInExportWidgetTypes'); this.widgetService.exportWidgetType(widgetTypeId, result.include).subscribe({ next: (widgetTypeDetails) => { - let name = widgetTypeDetails.name; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(this.prepareExport(widgetTypeDetails), name); + this.exportToPc(this.prepareExport(widgetTypeDetails), widgetTypeDetails.name, true); }, error: (e) => { this.handleExportError(e, 'widget-type.export-failed-error'); @@ -440,7 +431,7 @@ export class ImportExportService { public exportEntity(entityData: VersionedEntity): void { const id = (entityData as EntityInfoData).id ?? (entityData as RuleChainMetaData).ruleChainId; let fileName = (entityData as EntityInfoData).name; - let preparedData; + let preparedData: any; switch (id.entityType) { case EntityType.DEVICE_PROFILE: case EntityType.ASSET_PROFILE: @@ -511,9 +502,7 @@ export class ImportExportService { for (const widgetTypeDetails of widgetTypesDetails) { widgetsBundleItem.widgetTypes.push(this.prepareExport(widgetTypeDetails)); } - let name = widgetsBundle.title; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(widgetsBundleItem, name); + this.exportToPc(widgetsBundleItem, widgetsBundle.title, true); }, error: (e) => { this.handleExportError(e, 'widgets-bundle.export-failed-error'); @@ -528,9 +517,7 @@ export class ImportExportService { widgetsBundle: this.prepareExport(widgetsBundle), widgetTypeFqns }; - let name = widgetsBundle.title; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(widgetsBundleItem, name); + this.exportToPc(widgetsBundleItem, widgetsBundle.title, true); }, error: (e) => { this.handleExportError(e, 'widgets-bundle.export-failed-error'); @@ -662,11 +649,9 @@ export class ImportExportService { private onRuleChainExported() { return { next: (ruleChainExport: RuleChainImport) => { - let name = ruleChainExport.ruleChain.name; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(ruleChainExport, name); + this.exportToPc(ruleChainExport, ruleChainExport.ruleChain.name, true); }, - error: (e) => { + error: (e: any) => { this.handleExportError(e, 'rulechain.export-failed-error'); } }; @@ -747,9 +732,7 @@ export class ImportExportService { public exportDeviceProfile(deviceProfileId: string) { this.deviceProfileService.exportDeviceProfile(deviceProfileId).subscribe({ next: (deviceProfile) => { - let name = deviceProfile.name; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(this.prepareProfileExport(deviceProfile), name); + this.exportToPc(this.prepareProfileExport(deviceProfile), deviceProfile.name, true); }, error: (e) => { this.handleExportError(e, 'device-profile.export-failed-error'); @@ -776,9 +759,7 @@ export class ImportExportService { public exportAssetProfile(assetProfileId: string) { this.assetProfileService.exportAssetProfile(assetProfileId).subscribe({ next: (assetProfile) => { - let name = assetProfile.name; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(this.prepareProfileExport(assetProfile), name); + this.exportToPc(this.prepareProfileExport(assetProfile), assetProfile.name, true); }, error: (e) => { this.handleExportError(e, 'asset-profile.export-failed-error'); @@ -805,9 +786,7 @@ export class ImportExportService { public exportTenantProfile(tenantProfileId: string) { this.tenantProfileService.getTenantProfile(tenantProfileId).subscribe({ next: (tenantProfile) => { - let name = tenantProfile.name; - name = name.toLowerCase().replace(/\W/g, '_'); - this.exportToPc(this.prepareProfileExport(tenantProfile), name); + this.exportToPc(this.prepareProfileExport(tenantProfile), tenantProfile.name, true); }, error: (e) => { this.handleExportError(e, 'tenant-profile.export-failed-error'); @@ -882,7 +861,7 @@ export class ImportExportService { jsZip.generateAsync({type: 'blob'}).then(content => { this.downloadFile(content, filename, ZIP_TYPE); exportJsSubjectSubject.next(null); - }).catch(e => { + }).catch((e: any) => { exportJsSubjectSubject.error(e); }); } catch (e) { @@ -1180,42 +1159,40 @@ export class ImportExportService { )); } - private exportToPc(data: any, filename: string) { + private exportToPc(data: any, filename: string, normalizeFileName = false) { if (!data) { console.error('No data'); return; } - this.exportJson(data, filename); + this.exportJson(data, filename, normalizeFileName); } - public exportJson(data: any, filename: string) { + public exportJson(data: any, filename: string, normalizeFileName = false) { if (isObject(data)) { data = JSON.stringify(data, null, 2); } - this.downloadFile(data, filename, JSON_TYPE); + this.downloadFile(data, filename, JSON_TYPE, normalizeFileName); } - private downloadFile(data: any, filename: string, fileType: FileType) { - if (!filename) { - filename = 'download'; + private prepareFilename(filename: string, extension: string, normalizeFileName: boolean): string { + if (normalizeFileName) { + filename = filename.toLowerCase().replace(/\s/g, '_'); } - filename += '.' + fileType.extension; + filename = filename.replace(/[\\/<>:"|?*\s]/g, '_'); + return `${filename}.${extension}`; + } + + private downloadFile(data: any, filename = 'download', fileType: FileType, normalizeFileName = false) { + filename = this.prepareFilename(filename, fileType.extension, normalizeFileName); const blob = new Blob([data], {type: fileType.mimeType}); - // @ts-ignore - if (this.window.navigator && this.window.navigator.msSaveOrOpenBlob) { - // @ts-ignore - this.window.navigator.msSaveOrOpenBlob(blob, filename); - } else { - const e = this.document.createEvent('MouseEvents'); - const a = this.document.createElement('a'); - a.download = filename; - a.href = URL.createObjectURL(blob); - a.dataset.downloadurl = [fileType.mimeType, a.download, a.href].join(':'); - // @ts-ignore - e.initEvent('click', true, false, this.window, - 0, 0, 0, 0, 0, false, false, false, false, 0, null); - a.dispatchEvent(e); - } + const url = URL.createObjectURL(blob); + + const a = this.document.createElement('a'); + a.href = url; + a.download = filename; + a.dataset.downloadurl = [fileType.mimeType, a.download, a.href].join(':'); + a.click(); + setTimeout(() => URL.revokeObjectURL(url), 0); } private prepareDashboardExport(dashboard: Dashboard): Dashboard { From cdcb2c4fd68b395ea899277c73e70afa347a9e09 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Fri, 26 Sep 2025 11:59:10 +0300 Subject: [PATCH 155/216] UI: updated public api in import-export service --- .../import-export/import-export.service.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ui-ngx/src/app/shared/import-export/import-export.service.ts b/ui-ngx/src/app/shared/import-export/import-export.service.ts index d7c6d5880a..34e584a527 100644 --- a/ui-ngx/src/app/shared/import-export/import-export.service.ts +++ b/ui-ngx/src/app/shared/import-export/import-export.service.ts @@ -821,7 +821,7 @@ export class ImportExportService { return cellData; } - public exportCsv(data: {[key: string]: any}[], filename: string) { + public exportCsv(data: {[key: string]: any}[], filename: string, normalizeFileName = false) { let colsHead: string; let colsData: string; if (data && data.length) { @@ -836,18 +836,18 @@ export class ImportExportService { colsData = ''; } const csvData = `${colsHead}\n${colsData}`; - this.downloadFile(csvData, filename, CSV_TYPE); + this.downloadFile(csvData, filename, CSV_TYPE, normalizeFileName); } - public exportText(data: string | Array, filename: string) { + public exportText(data: string | Array, filename: string, normalizeFileName = false) { let content = data; if (Array.isArray(data)) { content = data.join('\n'); } - this.downloadFile(content, filename, TEXT_TYPE); + this.downloadFile(content, filename, TEXT_TYPE, normalizeFileName); } - public exportJSZip(data: object, filename: string): Observable { + public exportJSZip(data: object, filename: string, normalizeFileName = false): Observable { const exportJsSubjectSubject = new Subject(); import('jszip').then((JSZip) => { try { @@ -859,7 +859,7 @@ export class ImportExportService { } } jsZip.generateAsync({type: 'blob'}).then(content => { - this.downloadFile(content, filename, ZIP_TYPE); + this.downloadFile(content, filename, ZIP_TYPE, normalizeFileName); exportJsSubjectSubject.next(null); }).catch((e: any) => { exportJsSubjectSubject.error(e); @@ -1174,7 +1174,7 @@ export class ImportExportService { this.downloadFile(data, filename, JSON_TYPE, normalizeFileName); } - private prepareFilename(filename: string, extension: string, normalizeFileName: boolean): string { + private prepareFilename(filename: string, extension: string, normalizeFileName = false): string { if (normalizeFileName) { filename = filename.toLowerCase().replace(/\s/g, '_'); } @@ -1182,7 +1182,7 @@ export class ImportExportService { return `${filename}.${extension}`; } - private downloadFile(data: any, filename = 'download', fileType: FileType, normalizeFileName = false) { + private downloadFile(data: any, filename = 'download', fileType: FileType, normalizeFileName: boolean) { filename = this.prepareFilename(filename, fileType.extension, normalizeFileName); const blob = new Blob([data], {type: fileType.mimeType}); const url = URL.createObjectURL(blob); From ec30bb0578c8186b3d83a848d72add37a22b6845 Mon Sep 17 00:00:00 2001 From: VIacheslavKlimov Date: Fri, 26 Sep 2025 13:16:31 +0300 Subject: [PATCH 156/216] AI Request Node: added ability to attach files (#13910) --- .../server/actors/ActorSystemContext.java | 5 + .../actors/ruleChain/DefaultTbContext.java | 21 +- .../controller/TbResourceController.java | 17 ++ ...faultTbCalculatedFieldConsumerService.java | 4 +- .../queue/DefaultTbClusterService.java | 9 +- .../queue/DefaultTbCoreConsumerService.java | 4 +- .../queue/DefaultTbEdgeConsumerService.java | 2 +- .../DefaultTbRuleEngineConsumerService.java | 4 +- .../processing/AbstractConsumerService.java | 5 + ...AbstractPartitionBasedConsumerService.java | 4 +- .../resource/DefaultTbResourceService.java | 1 - .../src/main/resources/thingsboard.yml | 3 + .../controller/TbResourceControllerTest.java | 23 +- .../DefaultResourceDataCacheTest.java | 83 +++++++ .../sql/BaseTbResourceServiceTest.java | 121 +++++++++- .../server/dao/resource/ResourceService.java | 5 + .../dao/resource/TbResourceDataCache.java | 28 +++ .../common/data/GeneralFileDescriptor.java | 29 +++ .../server/common/data/ResourceType.java | 3 +- .../server/common/data/TbResource.java | 6 + .../common/data/TbResourceDataInfo.java | 31 +++ .../common/data/TbResourceDeleteResult.java | 3 +- .../thingsboard/common/util/DonAsynchron.java | 15 ++ .../server/dao/ResourceContainerDao.java | 5 +- .../server/dao/resource/BaseImageService.java | 5 +- .../dao/resource/BaseResourceService.java | 72 ++++-- .../resource/DefaultTbResourceDataCache.java | 72 ++++++ .../server/dao/resource/TbResourceDao.java | 2 + .../dao/resource/TbResourceInfoDao.java | 2 + .../server/dao/rule/RuleChainDao.java | 4 +- .../dashboard/DashboardInfoRepository.java | 16 +- .../sql/dashboard/JpaDashboardInfoDao.java | 10 +- .../dao/sql/resource/JpaTbResourceDao.java | 6 + .../sql/resource/JpaTbResourceInfoDao.java | 8 + .../resource/TbResourceInfoRepository.java | 6 + .../sql/resource/TbResourceRepository.java | 3 + .../server/dao/sql/rule/JpaRuleChainDao.java | 12 + .../dao/sql/rule/RuleChainRepository.java | 15 ++ .../dao/sql/widget/JpaWidgetTypeDao.java | 10 +- .../sql/widget/WidgetTypeInfoRepository.java | 15 +- .../dao/sql/rule/JpaRuleNodeDaoTest.java | 1 + .../rule/engine/api/TbContext.java | 7 + .../thingsboard/rule/engine/ai/TbAiNode.java | 121 +++++++++- .../rule/engine/ai/TbAiNodeConfiguration.java | 6 +- .../rule/engine/ai/TbAiNodeTest.java | 223 ++++++++++++++++++ 45 files changed, 965 insertions(+), 82 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java create mode 100644 common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceDataCache.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/GeneralFileDescriptor.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDataInfo.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/resource/DefaultTbResourceDataCache.java diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index ea46ce86eb..b23015a1fe 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -97,6 +97,7 @@ import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.resource.TbResourceDataCache; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleNodeStateService; @@ -511,6 +512,10 @@ public class ActorSystemContext { @Getter private ResourceService resourceService; + @Autowired + @Getter + private TbResourceDataCache resourceDataCache; + @Lazy @Autowired(required = false) @Getter diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 6374e4016d..88b04c7613 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -51,6 +51,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasRuleEngineProfile; +import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.alarm.Alarm; @@ -60,6 +61,7 @@ import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -110,6 +112,7 @@ import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.resource.TbResourceDataCache; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; @@ -770,6 +773,11 @@ public class DefaultTbContext implements TbContext { return mainCtx.getResourceService(); } + @Override + public TbResourceDataCache getTbResourceDataCache() { + return mainCtx.getResourceDataCache(); + } + @Override public OtaPackageService getOtaPackageService() { return mainCtx.getOtaPackageService(); @@ -1054,7 +1062,18 @@ public class DefaultTbContext implements TbContext { @Override public void checkTenantEntity(EntityId entityId) throws TbNodeException { - if (!this.getTenantId().equals(TenantIdLoader.findTenantId(this, entityId))) { + TenantId actualTenantId = TenantIdLoader.findTenantId(this, entityId); + assertSameTenantId(actualTenantId, entityId); + } + + @Override + public & HasTenantId, I extends EntityId> void checkTenantEntity(E entity) throws TbNodeException { + TenantId actualTenantId = entity.getTenantId(); + assertSameTenantId(actualTenantId, entity.getId()); + } + + private void assertSameTenantId(TenantId tenantId, EntityId entityId) throws TbNodeException { + if (!getTenantId().equals(tenantId)) { throw new TbNodeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant.", true); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java index b23603f6a2..54d2494679 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java @@ -55,14 +55,17 @@ import org.thingsboard.server.common.data.util.ThrowingSupplier; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.resource.TbResourceService; +import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.UUID; import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; import static org.thingsboard.server.controller.ControllerConstants.LWM2M_OBJECT_DESCRIPTION; @@ -263,6 +266,20 @@ public class TbResourceController extends BaseController { } } + @ApiOperation(value = "Get Resource Infos by ids (getSystemOrTenantResourcesByIds)") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/resource", params = {"resourceIds"}) + public List getSystemOrTenantResourcesByIds( + @Parameter(description = "A list of resource ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) + @RequestParam("resourceIds") Set resourceUuids) throws ThingsboardException { + SecurityUser user = getCurrentUser(); + List resourceIds = new ArrayList<>(); + for (UUID resourceId : resourceUuids) { + resourceIds.add(new TbResourceId(resourceId)); + } + return resourceService.findSystemOrTenantResourcesByIds(user.getTenantId(), resourceIds); + } + @ApiOperation(value = "Get All Resource Infos (getAllResources)", notes = "Returns a page of Resource Info objects owned by tenant. " + PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java index 8d4ab25578..acb36449e8 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.dao.resource.TbResourceDataCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; @@ -83,6 +84,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa ActorSystemContext actorContext, TbDeviceProfileCache deviceProfileCache, TbAssetProfileCache assetProfileCache, + TbResourceDataCache tbResourceDataCache, TbTenantProfileCache tenantProfileCache, TbApiUsageStateService apiUsageStateService, PartitionService partitionService, @@ -90,7 +92,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa JwtSettingsService jwtSettingsService, CalculatedFieldCache calculatedFieldCache, CalculatedFieldStateService stateService) { - super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService, + super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService); this.queueFactory = tbQueueFactory; this.stateService = stateService; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 265f14c4e2..86d00c77ca 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -48,6 +48,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.page.PageData; @@ -435,8 +436,9 @@ public class DefaultTbClusterService implements TbClusterService { @Override public void onResourceChange(TbResourceInfo resource, TbQueueCallback callback) { + TenantId tenantId = resource.getTenantId(); + TbResourceId resourceId = resource.getId(); if (resource.getResourceType() == ResourceType.LWM2M_MODEL) { - TenantId tenantId = resource.getTenantId(); log.trace("[{}][{}][{}] Processing change resource", tenantId, resource.getResourceType(), resource.getResourceKey()); ResourceUpdateMsg resourceUpdateMsg = ResourceUpdateMsg.newBuilder() .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) @@ -447,6 +449,7 @@ public class DefaultTbClusterService implements TbClusterService { ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceUpdateMsg(resourceUpdateMsg).build(); broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback); } + broadcastEntityStateChangeEvent(tenantId, resourceId, ComponentLifecycleEvent.UPDATED); } @Override @@ -462,6 +465,7 @@ public class DefaultTbClusterService implements TbClusterService { ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceDeleteMsg(resourceDeleteMsg).build(); broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback); } + broadcastEntityStateChangeEvent(resource.getTenantId(), resource.getId(), ComponentLifecycleEvent.DELETED); } private void broadcastEntityChangeToTransport(TenantId tenantId, EntityId entityid, T entity, TbQueueCallback callback) { @@ -592,7 +596,8 @@ public class DefaultTbClusterService implements TbClusterService { EntityType.TENANT_PROFILE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE, - EntityType.JOB) + EntityType.JOB, + EntityType.TB_RESOURCE) || (entityType == EntityType.ASSET && msg.getEvent() == ComponentLifecycleEvent.UPDATED) || (entityType == EntityType.DEVICE && msg.getEvent() == ComponentLifecycleEvent.UPDATED) ) { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index f0b1a4d7d2..9ab5a062eb 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -55,6 +55,7 @@ import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.util.KvProtoUtil; import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.dao.resource.ImageCacheKey; +import org.thingsboard.server.dao.resource.TbResourceDataCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto; @@ -176,10 +177,11 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService>>() { + var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { }); Assert.assertNotNull(widgetTypeInfos); Assert.assertFalse(widgetTypeInfos.isEmpty()); Assert.assertEquals(1, widgetTypeInfos.size()); - var dashboardInfo = widgetTypeInfos.get(EntityType.WIDGET_TYPE.name()).get(0); - Assert.assertNotNull(dashboardInfo); - - WidgetTypeInfo foundedWidgetType = doGet("/api/widgetTypeInfo/" + savedWidgetType.getId().getId().toString(), WidgetTypeInfo.class); - Assert.assertNotNull(foundedWidgetType); - Assert.assertEquals(foundedWidgetType, dashboardInfo); + var widgetTypeInfo = widgetTypeInfos.get(EntityType.WIDGET_TYPE.name()).get(0); + Assert.assertNotNull(widgetTypeInfo); + Assert.assertEquals(new EntityInfo(savedWidgetType.getId(), savedWidgetType.getName()), widgetTypeInfo); } @Test @@ -372,7 +370,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { Assert.assertTrue(isSuccess); var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); - var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { + var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { }); Assert.assertNull(widgetTypeInfos); } @@ -417,7 +415,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); Assert.assertNotNull(referenceValues); - var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { + var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { }); Assert.assertNotNull(dashboardInfos); Assert.assertFalse(dashboardInfos.isEmpty()); @@ -425,10 +423,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { var dashboardInfo = dashboardInfos.get(EntityType.DASHBOARD.name()).get(0); Assert.assertNotNull(dashboardInfo); - - DashboardInfo foundDashboard = doGet("/api/dashboard/info/" + savedDashboard.getId().getId().toString(), DashboardInfo.class); - Assert.assertNotNull(foundDashboard); - Assert.assertEquals(foundDashboard, dashboardInfo); + Assert.assertEquals(new EntityInfo(savedDashboard.getId(), savedDashboard.getName()), dashboardInfo); } @Test @@ -469,7 +464,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { Assert.assertTrue(isSuccess); var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); - var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { + var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { }); Assert.assertNull(dashboardInfos); } diff --git a/application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java b/application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java new file mode 100644 index 0000000000..f12a8d3c5d --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java @@ -0,0 +1,83 @@ +/** + * 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.service.resource; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.GeneralFileDescriptor; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDataInfo; +import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.resource.ResourceService; +import org.thingsboard.server.dao.resource.TbResourceDataCache; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +@DaoSqlTest +public class DefaultResourceDataCacheTest extends AbstractControllerTest { + + @MockitoSpyBean + private ResourceService resourceService; + @Autowired + private TbResourceService tbResourceService; + @MockitoSpyBean + private TbResourceDataCache resourceDataCache; + + @Test + public void testGetCachedResourceData() throws Exception { + loginTenantAdmin(); + + TbResource resource = new TbResource(); + resource.setTenantId(tenantId); + resource.setTitle("File for AI request"); + resource.setResourceType(ResourceType.GENERAL); + resource.setFileName("myTestJson.json"); + GeneralFileDescriptor descriptor = new GeneralFileDescriptor("application/json"); + resource.setDescriptorValue(descriptor); + byte[] data = "This is a test prompt for AI request.".getBytes(); + resource.setData(data); + TbResourceInfo savedResource = tbResourceService.save(resource); + verify(resourceDataCache, timeout(2000).times(1)).evictResourceData(tenantId, savedResource.getId()); + + TbResourceDataInfo cachedData = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get(); + assertThat(cachedData.getData()).isEqualTo(data); + assertThat(JacksonUtil.treeToValue(cachedData.getDescriptor(), GeneralFileDescriptor.class)).isEqualTo(descriptor); + verify(resourceService).getResourceDataInfo(tenantId, savedResource.getId()); + + // retrieve resource data second time + clearInvocations(resourceService); + TbResourceDataInfo cachedData2 = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get(); + assertThat(cachedData2.getData()).isEqualTo(data); + verifyNoMoreInteractions(resourceService); + + // delete resource, check cache + TbResource resourceById = resourceService.findResourceById(tenantId, savedResource.getId()); + tbResourceService.delete(resourceById, true, null); + verify(resourceDataCache, timeout(2000).times(2)).evictResourceData(tenantId, savedResource.getId()); + TbResourceDataInfo cachedDataAfterDeletion = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get(); + assertThat(cachedDataAfterDeletion).isEqualTo(null); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java index fe416eacd5..da20b9489c 100644 --- a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.resource.sql; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -24,8 +25,10 @@ import org.junit.Test; import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.ai.TbAiNode; +import org.thingsboard.rule.engine.ai.TbAiNodeConfiguration; +import org.thingsboard.rule.engine.ai.TbResponseFormat; import org.thingsboard.server.common.data.Dashboard; -import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; @@ -37,26 +40,40 @@ import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig; +import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; +import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; -import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.ai.AiModelService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.resource.ResourceService; +import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.service.resource.TbResourceService; import java.util.ArrayList; +import java.util.Arrays; import java.util.Base64; import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.function.Function; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -135,6 +152,10 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { private WidgetTypeService widgetTypeService; @Autowired private DashboardService dashboardService; + @Autowired + private RuleChainService ruleChainService; + @Autowired + private AiModelService aiModelService; private Tenant savedTenant; private User tenantAdmin; @@ -453,11 +474,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { Assert.assertFalse(result.getReferences().isEmpty()); Assert.assertEquals(1, result.getReferences().size()); - WidgetTypeInfo widgetTypeInfo = (WidgetTypeInfo) result.getReferences().get(EntityType.WIDGET_TYPE.name()).get(0); - WidgetTypeInfo foundWidgetTypeInfo = new WidgetTypeInfo(foundWidgetType); + EntityInfo widgetTypeInfo = (EntityInfo) result.getReferences().get(EntityType.WIDGET_TYPE.name()).get(0); Assert.assertNotNull(widgetTypeInfo); - Assert.assertNotNull(foundWidgetTypeInfo); - Assert.assertEquals(widgetTypeInfo, foundWidgetTypeInfo); + Assert.assertEquals(widgetTypeInfo, new EntityInfo(foundWidgetType.getId(), foundWidgetType.getName())); TbResourceInfo foundResourceInfo = resourceService.findResourceInfoById(savedTenant.getId(), savedResource.getId()); Assert.assertNotNull(foundResource); @@ -546,11 +565,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { Assert.assertNotNull(result.getReferences()); Assert.assertEquals(1, result.getReferences().size()); - DashboardInfo dashboardInfo = (DashboardInfo) result.getReferences().get(EntityType.DASHBOARD.name()).get(0); - DashboardInfo foundDashboardInfo = dashboardService.findDashboardInfoById(savedTenant.getId(), savedDashboard.getId()); + EntityInfo dashboardInfo = (EntityInfo) result.getReferences().get(EntityType.DASHBOARD.name()).get(0); Assert.assertNotNull(dashboardInfo); - Assert.assertNotNull(foundDashboardInfo); - Assert.assertEquals(foundDashboardInfo, dashboardInfo); + Assert.assertEquals(new EntityInfo(savedDashboard.getId(), savedDashboard.getName()), dashboardInfo); foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); Assert.assertNotNull(foundResource); @@ -598,6 +615,90 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { Assert.assertNull(foundResource); } + @Test + public void testShouldNotDeleteResourceIfUsedInAiNode() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.GENERAL); + resource.setTitle("My resource"); + resource.setFileName("test.json"); + resource.setTenantId(savedTenant.getId()); + resource.setData("".getBytes()); + TbResourceInfo savedResource = tbResourceService.save(resource); + RuleChainMetaData ruleChain = createRuleChainReferringResource(savedResource.getId()); + + TbResourceDeleteResult result = tbResourceService.delete(savedResource, false, null); + assertThat(result).isNotNull(); + assertThat(result.isSuccess()).isFalse(); + assertThat(result.getReferences()).isNotEmpty().hasSize(1); + EntityInfo entityInfo = (EntityInfo) result.getReferences().get(EntityType.RULE_CHAIN.name()).get(0); + assertThat(entityInfo).isEqualTo(new EntityInfo(ruleChain.getRuleChainId(), "Test")); + + TbResource foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); + assertThat(foundResource).isNotNull(); + + // force delete + TbResourceDeleteResult deleteResult = tbResourceService.delete(savedResource, true, null); + assertThat(deleteResult).isNotNull(); + assertThat(deleteResult.isSuccess()).isTrue(); + + TbResource resourceAfterDeletion = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); + assertThat(resourceAfterDeletion).isNull(); + } + + private RuleChainMetaData createRuleChainReferringResource(TbResourceId resourceId) { + AiModel model = constructValidOpenAiModel("Test model"); + AiModel saved = aiModelService.save(model); + + RuleChain ruleChain = new RuleChain(); + ruleChain.setTenantId(tenantId); + ruleChain.setName("Test"); + ruleChain.setType(RuleChainType.CORE); + ruleChain.setDebugMode(true); + ruleChain.setConfiguration(JacksonUtil.newObjectNode().set("a", new TextNode("b"))); + ruleChain = ruleChainService.saveRuleChain(ruleChain); + RuleChainId ruleChainId = ruleChain.getId(); + + RuleChainMetaData metaData = new RuleChainMetaData(); + metaData.setRuleChainId(ruleChainId); + + RuleNode aiNode = new RuleNode(); + aiNode.setName("Ai request"); + aiNode.setType(org.thingsboard.rule.engine.ai.TbAiNode.class.getName()); + aiNode.setConfigurationVersion(TbAiNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); + aiNode.setDebugSettings(DebugSettings.all()); + TbAiNodeConfiguration configuration = new TbAiNodeConfiguration(); + configuration.setResourceIds(Set.of(resourceId.getId())); + configuration.setModelId(saved.getId()); + configuration.setResponseFormat(new TbResponseFormat.TbJsonResponseFormat()); + configuration.setTimeoutSeconds(1); + configuration.setUserPrompt("What is temp"); + aiNode.setConfiguration(JacksonUtil.valueToTree(configuration)); + + metaData.setNodes(Arrays.asList(aiNode)); + metaData.setFirstNodeIndex(0); + ruleChainService.saveRuleChainMetaData(tenantId, metaData, Function.identity()); + return ruleChainService.loadRuleChainMetaData(tenantId, ruleChainId); + } + + private AiModel constructValidOpenAiModel(String name) { + var modelConfig = OpenAiChatModelConfig.builder() + .providerConfig(new OpenAiProviderConfig("test-api-key")) + .modelId("gpt-4o") + .temperature(0.5) + .topP(0.3) + .frequencyPenalty(0.1) + .presencePenalty(0.2) + .maxOutputTokens(1000) + .timeoutSeconds(60) + .maxRetries(2) + .build(); + + return AiModel.builder() + .tenantId(tenantId) + .name(name) + .configuration(modelConfig) + .build(); + } @Test public void testFindTenantResourcesByTenantId() throws Exception { loginSysAdmin(); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java index ee187db46b..65211ec17a 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.ResourceExportData; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDataInfo; import org.thingsboard.server.common.data.TbResourceDeleteResult; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfoFilter; @@ -46,6 +47,8 @@ public interface ResourceService extends EntityDaoService { byte[] getResourceData(TenantId tenantId, TbResourceId resourceId); + TbResourceDataInfo getResourceDataInfo(TenantId tenantId, TbResourceId resourceId); + ResourceExportData exportResource(TbResourceInfo resourceInfo); List exportResources(TenantId tenantId, Collection resources); @@ -90,4 +93,6 @@ public interface ResourceService extends EntityDaoService { TbResource createOrUpdateSystemResource(ResourceType resourceType, ResourceSubType resourceSubType, String resourceKey, byte[] data); + List findSystemOrTenantResourcesByIds(TenantId tenantId, List resourceIds); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceDataCache.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceDataCache.java new file mode 100644 index 0000000000..23485684af --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceDataCache.java @@ -0,0 +1,28 @@ +/** + * 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.resource; + +import com.google.common.util.concurrent.FluentFuture; +import org.thingsboard.server.common.data.TbResourceDataInfo; +import org.thingsboard.server.common.data.id.TbResourceId; +import org.thingsboard.server.common.data.id.TenantId; + +public interface TbResourceDataCache { + + FluentFuture getResourceDataInfoAsync(TenantId tenantId, TbResourceId resourceId); + + void evictResourceData(TenantId tenantId, TbResourceId resourceId); +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/GeneralFileDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/GeneralFileDescriptor.java new file mode 100644 index 0000000000..94edd4fa01 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/GeneralFileDescriptor.java @@ -0,0 +1,29 @@ +/** + * 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.common.data; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Data +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor +public class GeneralFileDescriptor { + private String mediaType; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java index 77b17198e9..f7579b6878 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java @@ -25,7 +25,8 @@ public enum ResourceType { PKCS_12("application/x-pkcs12", false, false), JS_MODULE("application/javascript", true, true), IMAGE(null, true, true), - DASHBOARD("application/json", true, true); + DASHBOARD("application/json", true, true), + GENERAL(null, false, true); @Getter private final String mediaType; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java index ba37067106..457d30e263 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSetter; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -86,6 +87,11 @@ public class TbResource extends TbResourceInfo { .orElse(null); } + @JsonIgnore + public TbResourceDataInfo toResourceDataInfo() { + return new TbResourceDataInfo(data, getDescriptor()); + } + @Override public String toString() { return super.toString(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDataInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDataInfo.java new file mode 100644 index 0000000000..039478470d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDataInfo.java @@ -0,0 +1,31 @@ +/** + * 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.common.data; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TbResourceDataInfo { + + private byte[] data; + private JsonNode descriptor; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java index edc5a2f539..76945a97ed 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java @@ -17,7 +17,6 @@ package org.thingsboard.server.common.data; import lombok.Builder; import lombok.Data; -import org.thingsboard.server.common.data.id.HasId; import java.util.List; import java.util.Map; @@ -27,6 +26,6 @@ import java.util.Map; public class TbResourceDeleteResult { private boolean success; - private Map>> references; + private Map> references; } diff --git a/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java b/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java index 0f1a56cb17..79b8181548 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java +++ b/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java @@ -15,12 +15,15 @@ */ package org.thingsboard.common.util; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -71,4 +74,16 @@ public class DonAsynchron { return future; } + public static FluentFuture toFluentFuture(CompletableFuture completable) { + SettableFuture future = SettableFuture.create(); + completable.whenComplete((result, exception) -> { + if (exception != null) { + future.setException(exception); + } else { + future.set(result); + } + }); + return FluentFuture.from(future); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java b/dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java index 6a952fd501..93cc64b2db 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; @@ -22,8 +23,8 @@ import java.util.List; public interface ResourceContainerDao> { - List findByTenantIdAndResourceLink(TenantId tenantId, String link, int limit); + List findByTenantIdAndResource(TenantId tenantId, String reference, int limit); - List findByResourceLink(String link, int limit); + List findByResource(String reference, int limit); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java index 3be7f5b91c..c16e37d30f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java @@ -50,6 +50,7 @@ import org.thingsboard.server.dao.ImageContainerDao; import org.thingsboard.server.dao.asset.AssetProfileDao; import org.thingsboard.server.dao.dashboard.DashboardInfoDao; import org.thingsboard.server.dao.device.DeviceProfileDao; +import org.thingsboard.server.dao.rule.RuleChainDao; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.validator.ResourceDataValidator; import org.thingsboard.server.dao.util.ImageUtils; @@ -109,8 +110,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic public BaseImageService(TbResourceDao resourceDao, TbResourceInfoDao resourceInfoDao, ResourceDataValidator resourceValidator, AssetProfileDao assetProfileDao, DeviceProfileDao deviceProfileDao, WidgetsBundleDao widgetsBundleDao, - WidgetTypeDao widgetTypeDao, DashboardInfoDao dashboardInfoDao) { - super(resourceDao, resourceInfoDao, resourceValidator, widgetTypeDao, dashboardInfoDao); + WidgetTypeDao widgetTypeDao, DashboardInfoDao dashboardInfoDao, RuleChainDao ruleChainDao) { + super(resourceDao, resourceInfoDao, resourceValidator, widgetTypeDao, dashboardInfoDao, ruleChainDao); this.assetProfileDao = assetProfileDao; this.deviceProfileDao = deviceProfileDao; this.widgetsBundleDao = widgetsBundleDao; diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index bf941256f5..7f0dd4a6bf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -35,11 +35,13 @@ import org.thingsboard.server.cache.resourceInfo.ResourceInfoCacheKey; import org.thingsboard.server.cache.resourceInfo.ResourceInfoEvictEvent; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ResourceExportData; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDataInfo; import org.thingsboard.server.common.data.TbResourceDeleteResult; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfoFilter; @@ -56,6 +58,7 @@ import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.rule.RuleChainDao; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.validator.ResourceDataValidator; @@ -92,13 +95,16 @@ public class BaseResourceService extends AbstractCachedEntityService> resourceContainerDaoMap = new HashMap<>(); + protected final RuleChainDao ruleChainDao; + private final Map> resourceLinkContainerDaoMap = new HashMap<>(); + private final Map> generalResourceContainerDaoMap = new HashMap<>(); protected static final int MAX_ENTITIES_TO_FIND = 10; @PostConstruct public void init() { - resourceContainerDaoMap.put(EntityType.WIDGET_TYPE, widgetTypeDao); - resourceContainerDaoMap.put(EntityType.DASHBOARD, dashboardInfoDao); + resourceLinkContainerDaoMap.put(EntityType.WIDGET_TYPE, widgetTypeDao); + resourceLinkContainerDaoMap.put(EntityType.DASHBOARD, dashboardInfoDao); + generalResourceContainerDaoMap.put(EntityType.RULE_CHAIN, ruleChainDao); } @Autowired @Lazy @@ -206,6 +212,12 @@ public class BaseResourceService extends AbstractCachedEntityService>> affectedEntities = new HashMap<>(); - - resourceContainerDaoMap.forEach((entityType, resourceContainerDao) -> { - var entities = tenantId.isSysTenantId() ? resourceContainerDao.findByResourceLink(link, MAX_ENTITIES_TO_FIND) : - resourceContainerDao.findByTenantIdAndResourceLink(tenantId, link, MAX_ENTITIES_TO_FIND); - if (!entities.isEmpty()) { - affectedEntities.put(entityType.name(), entities); - } - }); - - if (!affectedEntities.isEmpty()) { - success = false; - result.references(affectedEntities); - } + Map> references = findResourceReferences(tenantId, resource); + if (!references.isEmpty()) { + success = false; + result.references(references); } } if (success) { resourceDao.removeById(tenantId, resourceId.getId()); + publishEvictEvent(new ResourceInfoEvictEvent(tenantId, resourceId)); eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entity(resource).entityId(resourceId).build()); } return result.success(success).build(); } + private Map> findResourceReferences(TenantId tenantId, TbResourceInfo resource) { + Map> references = new HashMap<>(); + + if (resource.getResourceType() == ResourceType.JS_MODULE) { + var ref = resource.getLink(); + findReferences(tenantId, references, ref, resourceLinkContainerDaoMap); + } + + if (resource.getResourceType() == ResourceType.GENERAL) { + var ref = resource.getId().getId().toString(); + findReferences(tenantId, references, ref, generalResourceContainerDaoMap); + } + + return references; + } + + private void findReferences(TenantId tenantId, Map> references, String ref, Map> resourceLinkContainerDaoMap) { + resourceLinkContainerDaoMap.forEach((entityType, dao) -> { + List entities = tenantId.isSysTenantId() + ? dao.findByResource(ref, MAX_ENTITIES_TO_FIND) + : dao.findByTenantIdAndResource(tenantId, ref, MAX_ENTITIES_TO_FIND); + if (!entities.isEmpty()) { + references.put(entityType.name(), entities); + } + }); + } + @Override public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { deleteResource(tenantId, (TbResourceId) id, force); @@ -663,6 +691,12 @@ public class BaseResourceService extends AbstractCachedEntityService findSystemOrTenantResourcesByIds(TenantId tenantId, List resourceIds) { + log.trace("Executing findSystemOrTenantResourcesByIds, tenantId [{}], resourceIds [{}]", tenantId, resourceIds); + return resourceInfoDao.findSystemOrTenantResourcesByIds(tenantId, resourceIds); + } + @Override public String calculateEtag(byte[] data) { return Hashing.sha256().hashBytes(data).toString(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/DefaultTbResourceDataCache.java b/dao/src/main/java/org/thingsboard/server/dao/resource/DefaultTbResourceDataCache.java new file mode 100644 index 0000000000..452f86b1a6 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/DefaultTbResourceDataCache.java @@ -0,0 +1,72 @@ +/** + * 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.resource; + +import com.github.benmanes.caffeine.cache.AsyncLoadingCache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.google.common.util.concurrent.FluentFuture; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.DonAsynchron; +import org.thingsboard.server.common.data.TbResourceDataInfo; +import org.thingsboard.server.common.data.id.TbResourceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.sql.JpaExecutorService; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +@Service +@RequiredArgsConstructor +@Slf4j +public class DefaultTbResourceDataCache implements TbResourceDataCache { + + private final ResourceService resourceService; + private final JpaExecutorService executorService; + + @Value("${cache.tbResourceData.maxSize:100000}") + private int cacheMaxSize; + @Value("${cache.tbResourceData.timeToLiveInMinutes:44640}") + private int cacheValueTtl; + private AsyncLoadingCache cache; + + @PostConstruct + private void init() { + cache = Caffeine.newBuilder() + .maximumSize(cacheMaxSize) + .expireAfterAccess(cacheValueTtl, TimeUnit.MINUTES) + .executor(executorService) + .buildAsync((key, executor) -> CompletableFuture.supplyAsync(() -> resourceService.getResourceDataInfo(key.tenantId(), key.resourceId()), executor)); + } + + @Override + public FluentFuture getResourceDataInfoAsync(TenantId tenantId, TbResourceId resourceId) { + log.trace("Retrieving resource data info by id [{}], tenant id [{}] from cache", resourceId, tenantId); + return DonAsynchron.toFluentFuture(cache.get(new ResourceDataKey(tenantId, resourceId))); + } + + @Override + public void evictResourceData(TenantId tenantId, TbResourceId resourceId) { + cache.asMap().remove(new ResourceDataKey(tenantId, resourceId)); + log.trace("Evicted resource data info with id [{}], tenant id [{}]", resourceId, tenantId); + } + + record ResourceDataKey (TenantId tenantId, TbResourceId resourceId) {} + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java index 23b59b5658..1b9f250521 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.resource; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDataInfo; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -51,4 +52,5 @@ public interface TbResourceDao extends Dao, TenantEntityWithDataDao, long getResourceSize(TenantId tenantId, TbResourceId resourceId); + TbResourceDataInfo getResourceDataInfo(TenantId tenantId, TbResourceId resourceId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java index 8e97738501..f4fe02843d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.resource; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfoFilter; +import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -46,4 +47,5 @@ public interface TbResourceInfoDao extends Dao { TbResourceInfo findPublicResourceByKey(ResourceType resourceType, String publicResourceKey); + List findSystemOrTenantResourcesByIds(TenantId tenantId, List resourceIds); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java index 5b09eec42a..ac716bb4fc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java @@ -21,8 +21,10 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; +import org.thingsboard.server.dao.ResourceContainerDao; import org.thingsboard.server.dao.TenantEntityDao; import java.util.Collection; @@ -31,7 +33,7 @@ import java.util.UUID; /** * Created by igor on 3/12/18. */ -public interface RuleChainDao extends Dao, TenantEntityDao, ExportableEntityDao { +public interface RuleChainDao extends Dao, TenantEntityDao, ExportableEntityDao, ResourceContainerDao { /** * Find rule chains by tenantId and page link. diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java index 7624ddc738..32ac596562 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.dao.model.sql.DashboardInfoEntity; import java.util.List; @@ -87,12 +88,15 @@ public interface DashboardInfoRepository extends JpaRepository findByImageLink(@Param("imageLink") String imageLink, @Param("limit") int limit); - @Query(value = "SELECT * FROM dashboard d WHERE d.tenant_id = :tenantId and d.configuration ILIKE CONCAT('%', :link, '%') limit :limit", - nativeQuery = true) - List findDashboardInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId, @Param("link") String link, @Param("limit") int limit); + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(d.id, 'DASHBOARD', d.title) " + + "FROM DashboardEntity d WHERE d.tenantId = :tenantId AND ilike(cast(d.configuration as string), CONCAT('%', :link, '%')) = true") + List findDashboardInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId, + @Param("link") String link, + Pageable pageable); - @Query(value = "SELECT * FROM dashboard d WHERE d.configuration ILIKE CONCAT('%', :link, '%') limit :limit", - nativeQuery = true) - List findDashboardInfosByResourceLink(@Param("link") String link, @Param("limit") int limit); + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(d.id, 'DASHBOARD', d.title) " + + "FROM DashboardEntity d WHERE ilike(cast(d.configuration as string), CONCAT('%', :link, '%')) = true") + List findDashboardInfosByResourceLink(@Param("link") String link, + Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java index bc07139725..0e04c94a46 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java @@ -17,9 +17,11 @@ package org.thingsboard.server.dao.sql.dashboard; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.DashboardInfo; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -135,13 +137,13 @@ public class JpaDashboardInfoDao extends JpaAbstractDao findByTenantIdAndResourceLink(TenantId tenantId, String url, int limit) { - return DaoUtil.convertDataList(dashboardInfoRepository.findDashboardInfosByTenantIdAndResourceLink(tenantId.getId(), url, limit)); + public List findByTenantIdAndResource(TenantId tenantId, String reference, int limit) { + return dashboardInfoRepository.findDashboardInfosByTenantIdAndResourceLink(tenantId.getId(), reference, PageRequest.of(0, limit)); } @Override - public List findByResourceLink(String link, int limit) { - return DaoUtil.convertDataList(dashboardInfoRepository.findDashboardInfosByResourceLink(link, limit)); + public List findByResource(String reference, int limit) { + return dashboardInfoRepository.findDashboardInfosByResourceLink(reference, PageRequest.of(0, limit)); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java index 6cce9d76c2..48e41c8553 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDataInfo; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -115,6 +116,11 @@ public class JpaTbResourceDao extends JpaAbstractDao findSystemOrTenantResourcesByIds(TenantId tenantId, List resourceIds) { + return DaoUtil.convertDataList(resourceInfoRepository.findSystemOrTenantResourcesByIdIn(tenantId.getId(), TenantId.NULL_UUID, toUUIDs(resourceIds))); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java index 6eea20a287..97b1e56527 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java @@ -79,4 +79,10 @@ public interface TbResourceInfoRepository extends JpaRepository findSystemOrTenantResourcesByIdIn(@Param("tenantId") UUID tenantId, + @Param("systemTenantId") UUID systemTenantId, + @Param("resourceIds") List resourceIds); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java index 1c642d2069..4aa699174f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.TbResourceDataInfo; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.TbResourceEntity; @@ -101,4 +102,6 @@ public interface TbResourceRepository extends JpaRepository findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); + @Query("SELECT new org.thingsboard.server.common.data.TbResourceDataInfo(r.data, r.descriptor) FROM TbResourceEntity r WHERE r.id = :id") + TbResourceDataInfo getDataInfoById(UUID id); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java index 77044d41dc..4a6427a7e5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java @@ -18,8 +18,10 @@ package org.thingsboard.server.dao.sql.rule; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; +import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.edqs.fields.RuleChainFields; import org.thingsboard.server.common.data.id.RuleChainId; @@ -141,6 +143,16 @@ public class JpaRuleChainDao extends JpaAbstractDao return findRuleChainsByTenantId(tenantId.getId(), pageLink); } + @Override + public List findByTenantIdAndResource(TenantId tenantId, String reference, int limit) { + return ruleChainRepository.findRuleChainsByTenantIdAndResource(tenantId.getId(), reference, PageRequest.of(0, limit)); + } + + @Override + public List findByResource(String reference, int limit) { + return ruleChainRepository.findRuleChainsByResource(reference, PageRequest.of(0, limit)); + } + @Override public List findNextBatch(UUID id, int batchSize) { return ruleChainRepository.findNextBatch(id, Limit.of(batchSize)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java index cfa06caf14..4bf648cbbd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java @@ -17,10 +17,12 @@ package org.thingsboard.server.dao.sql.rule; import org.springframework.data.domain.Limit; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.edqs.fields.RuleChainFields; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.ExportableEntityRepository; @@ -72,6 +74,19 @@ public interface RuleChainRepository extends JpaRepository findRuleChainsByTenantIdAndResource(@Param("tenantId") UUID tenantId, + @Param("resourceId") String resourceId, + PageRequest of); + + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(rc.id, 'RULE_CHAIN', rc.name) " + + "FROM RuleChainEntity rc WHERE EXISTS " + + "(SELECT 1 FROM RuleNodeEntity rn WHERE rn.ruleChainId = rc.id AND cast(rn.configuration as string) LIKE CONCAT('%', :resourceId, '%'))") + List findRuleChainsByResource(@Param("resourceId") String resourceId, + Pageable pageable); + @Query("SELECT new org.thingsboard.server.common.data.edqs.fields.RuleChainFields(r.id, r.createdTime, r.tenantId," + "r.name, r.version, r.additionalInfo) FROM RuleChainEntity r WHERE r.id > :id ORDER BY r.id") List findNextBatch(@Param("id") UUID id, Limit limit); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java index c728f5d006..18fb544dcb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java @@ -17,8 +17,10 @@ package org.thingsboard.server.dao.sql.widget; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; +import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.edqs.fields.WidgetTypeFields; import org.thingsboard.server.common.data.id.TenantId; @@ -269,13 +271,13 @@ public class JpaWidgetTypeDao extends JpaAbstractDao findByTenantIdAndResourceLink(TenantId tenantId, String link, int limit) { - return DaoUtil.convertDataList(widgetTypeInfoRepository.findWidgetTypeInfosByTenantIdAndResourceLink(tenantId.getId(), link, limit)); + public List findByTenantIdAndResource(TenantId tenantId, String reference, int limit) { + return widgetTypeInfoRepository.findWidgetTypeInfosByTenantIdAndResourceLink(tenantId.getId(), reference, PageRequest.of(0, limit)); } @Override - public List findByResourceLink(String link, int limit) { - return DaoUtil.convertDataList(widgetTypeInfoRepository.findWidgetTypeInfosByResourceLink(link, limit)); + public List findByResource(String reference, int limit) { + return widgetTypeInfoRepository.findWidgetTypeInfosByResourceLink(reference, PageRequest.of(0, limit)); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java index dc79280bcf..b97b42a6b9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.dao.model.sql.WidgetTypeInfoEntity; import java.util.List; @@ -214,10 +215,14 @@ public interface WidgetTypeInfoRepository extends JpaRepository findByImageUrl(@Param("imageLink") String imageLink, @Param("limit") int limit); - @Query(value = "SELECT * FROM widget_type_info_view w WHERE w.tenant_id = :tenantId AND w.descriptor ILIKE CONCAT('%', :link, '%') LIMIT :limit ", nativeQuery = true) - List findWidgetTypeInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId, @Param("link") String link, @Param("limit") int limit); - - @Query(value = "SELECT * FROM widget_type_info_view w WHERE w.descriptor ILIKE CONCAT('%', :link, '%') LIMIT :limit ", nativeQuery = true) - List findWidgetTypeInfosByResourceLink(@Param("link") String link, @Param("limit") int limit); + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(w.id, 'WIDGET_TYPE', w.name) " + + "FROM WidgetTypeEntity w WHERE w.tenantId = :tenantId AND ilike(cast(w.descriptor as string), CONCAT('%', :link, '%')) = true") + List findWidgetTypeInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId, + @Param("link") String link, + Pageable pageable); + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(w.id, 'WIDGET_TYPE', w.name) " + + "FROM WidgetTypeEntity w WHERE ilike(cast(w.descriptor as string), CONCAT('%', :link, '%')) = true") + List findWidgetTypeInfosByResourceLink(@Param("link") String link, + Pageable pageable); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java index f5aa8a3af5..59c0db9eee 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java @@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index d2687a1b10..920f00ed27 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -24,6 +24,7 @@ import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; @@ -32,6 +33,7 @@ import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -78,6 +80,7 @@ import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.resource.ResourceService; +import org.thingsboard.server.dao.resource.TbResourceDataCache; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -252,6 +255,8 @@ public interface TbContext { void checkTenantEntity(EntityId entityId) throws TbNodeException; + & HasTenantId, I extends EntityId> void checkTenantEntity(E entity) throws TbNodeException; + boolean isLocalEntity(EntityId entityId); RuleNodeId getSelfId(); @@ -308,6 +313,8 @@ public interface TbContext { ResourceService getResourceService(); + TbResourceDataCache getTbResourceDataCache(); + OtaPackageService getOtaPackageService(); RuleEngineDeviceProfileCache getDeviceProfileCache(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index 3497795771..bd02089204 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -18,12 +18,20 @@ package org.thingsboard.rule.engine.ai; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.data.message.Content; +import dev.langchain4j.data.message.ImageContent; +import dev.langchain4j.data.message.PdfFileContent; import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.TextContent; import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.model.chat.request.ChatRequest; import dev.langchain4j.model.chat.request.ResponseFormat; import dev.langchain4j.model.chat.response.ChatResponse; +import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.NonNull; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; @@ -33,24 +41,38 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.external.TbAbstractExternalNode; +import org.thingsboard.server.common.data.GeneralFileDescriptor; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResourceDataInfo; +import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.ai.AiModel; import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig; import org.thingsboard.server.common.data.id.AiModelId; +import org.thingsboard.server.common.data.id.TbResourceId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.resource.TbResourceDataCache; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Base64; +import java.util.HashSet; import java.util.List; import java.util.NoSuchElementException; +import java.util.Objects; import java.util.Optional; +import java.util.Set; +import java.util.UUID; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbResponseFormatType; import static org.thingsboard.server.dao.service.ConstraintValidator.validateFields; +@Slf4j @RuleNode( type = ComponentType.EXTERNAL, name = "AI request", @@ -77,6 +99,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { private String systemPrompt; private String userPrompt; + private Set resourceIds; private ResponseFormat responseFormat; private int timeoutSeconds; private AiModelId modelId; @@ -111,6 +134,14 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { // LangChain4j AnthropicChatModel rejects requests with non-null ResponseFormat even if ResponseFormatType is TEXT responseFormat = config.getResponseFormat().toLangChainResponseFormat(); } + if (config.getResourceIds() != null && !config.getResourceIds().isEmpty()) { + resourceIds = new HashSet<>(config.getResourceIds().size()); + for (UUID resourceId : config.getResourceIds()) { + TbResourceId tbResourceId = new TbResourceId(resourceId); + validateResource(ctx, tbResourceId); + resourceIds.add(tbResourceId); + } + } systemPrompt = config.getSystemPrompt(); userPrompt = config.getUserPrompt(); @@ -126,12 +157,42 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { var ackedMsg = ackIfNeeded(ctx, msg); + final String processedUserPrompt = TbNodeUtils.processPattern(this.userPrompt, ackedMsg); + + final ListenableFuture userMessageFuture = + resourceIds == null + ? Futures.immediateFuture(UserMessage.from(processedUserPrompt)) + : Futures.transform( + loadResources(ctx), + resources -> UserMessage.from(buildContents(processedUserPrompt, resources)), + ctx.getDbCallbackExecutor() + ); + + Futures.addCallback( + userMessageFuture, + new FutureCallback<>() { + @Override + public void onSuccess(UserMessage userMessage) { + buildAndSendRequest(ctx, ackedMsg, userMessage); + } + @Override + public void onFailure(Throwable t) { + tellFailure(ctx, ackedMsg, t); + } + }, + MoreExecutors.directExecutor() + ); + } + + private void buildAndSendRequest(TbContext ctx, TbMsg ackedMsg, UserMessage userMessage) { List chatMessages = new ArrayList<>(2); - if (systemPrompt != null) { + + if (systemPrompt != null && !systemPrompt.isBlank()) { chatMessages.add(SystemMessage.from(TbNodeUtils.processPattern(systemPrompt, ackedMsg))); } - chatMessages.add(UserMessage.from(TbNodeUtils.processPattern(userPrompt, ackedMsg))); + + chatMessages.add(userMessage); var chatRequest = ChatRequest.builder() .messages(chatMessages) @@ -192,11 +253,67 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { return JacksonUtil.newObjectNode().put("response", response).toString(); } + private void validateResource(TbContext ctx, TbResourceId tbResourceId) throws TbNodeException { + TbResourceInfo resource = ctx.getResourceService().findResourceInfoById(ctx.getTenantId(), tbResourceId); + if (resource == null) { + throw new TbNodeException("[" + ctx.getTenantId() + "] Resource with ID: [" + tbResourceId + "] was not found", true); + } + if (!ResourceType.GENERAL.equals(resource.getResourceType())) { + throw new TbNodeException("[" + ctx.getTenantId() + "] Resource with ID: [" + tbResourceId + "] has unsupported resource type: " + resource.getResourceType(), true); + } + ctx.checkTenantEntity(resource); + } + + private ListenableFuture> loadResources(TbContext ctx) { + final TenantId tenantId = ctx.getTenantId(); + final TbResourceDataCache cache = ctx.getTbResourceDataCache(); + List> futures = resourceIds.stream() + .map(id -> cache.getResourceDataInfoAsync(tenantId, id)) + .toList(); + return Futures.allAsList(futures); + } + + private List buildContents(String userPrompt, List resources) { + List contents = new ArrayList<>(1 + resources.size()); + contents.add(new TextContent(userPrompt)); // user prompt first + + resources.stream() + .filter(Objects::nonNull) + .map(this::toContent) + .forEach(contents::add); + + return contents; + } + + private Content toContent(TbResourceDataInfo resource) { + if (resource.getDescriptor() == null) { + throw new RuntimeException("Missing descriptor for resource"); + } + GeneralFileDescriptor descriptor = JacksonUtil.treeToValue(resource.getDescriptor(), GeneralFileDescriptor.class); + String mediaType = descriptor.getMediaType(); + if (mediaType == null) { + throw new RuntimeException("Missing mediaType in resource descriptor " + resource.getDescriptor()); + } + byte[] data = resource.getData(); + if (mediaType.startsWith("text/")) { + return new TextContent(new String(data, StandardCharsets.UTF_8)); + } + if (mediaType.equals("application/pdf")) { + return new PdfFileContent(Base64.getEncoder().encodeToString(data), mediaType); + } + if (mediaType.startsWith("image/")) { + return new ImageContent(Base64.getEncoder().encodeToString(data), mediaType); + } + log.debug("Trying to create text content for {}", resource.getDescriptor()); + return new TextContent(new String(data, StandardCharsets.UTF_8)); + } + @Override public void destroy() { super.destroy(); systemPrompt = null; userPrompt = null; + resourceIds = null; responseFormat = null; modelId = null; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java index 48392aa76e..f51983ecb1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java @@ -20,12 +20,14 @@ import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; import lombok.Data; import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.validation.Length; +import java.util.Set; +import java.util.UUID; + import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonResponseFormat; @Data @@ -41,6 +43,8 @@ public class TbAiNodeConfiguration implements NodeConfiguration resourceIds; + @NotNull @Valid private TbResponseFormat responseFormat; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java index f21aa559d4..c5b7f2c44b 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java @@ -17,8 +17,11 @@ package org.thingsboard.rule.engine.ai; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.Futures; import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.data.message.ImageContent; import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.TextContent; import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.model.chat.request.ResponseFormat; import dev.langchain4j.model.chat.request.ResponseFormatType; @@ -32,6 +35,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; @@ -43,6 +47,10 @@ import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.GeneralFileDescriptor; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDataInfo; import org.thingsboard.server.common.data.ai.AiModel; import org.thingsboard.server.common.data.ai.model.AiModelConfig; import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig; @@ -52,6 +60,7 @@ import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.rule.RuleNode; @@ -59,9 +68,14 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.ai.AiModelService; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.resource.ResourceService; +import org.thingsboard.server.dao.resource.TbResourceDataCache; +import java.util.Base64; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.stream.Stream; @@ -76,16 +90,23 @@ import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.never; +import static org.thingsboard.server.common.data.ResourceType.GENERAL; @ExtendWith(MockitoExtension.class) class TbAiNodeTest { + private static final byte[] PNG_IMAGE = Base64.getDecoder().decode("iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAC9FBMVEUAAAABAQEBAgICAgICAwMCBAQDAwMDBQUDBgYEBAQEBwcECAgFCQkFCgoGBgYGCwsGDAwHBwcHDQ0HDg4ICAgIDw8IEBAJCQkJEREKEhIKExMLFBQLFRUMFhYMFxcNDQ0NGBgNGRkODg4OGhoOGxsPDw8PHBwPHR0QEBAQHh4QHx8RERERICARISESEhISIiITExMTIyMTJCQUJSUUJiYVKCgWFhYWKSkXFxcXGhwYGBgYLC0ZGRkaMDEaMTIbGxsbMjMcMzQdNTYfOTogICAgOzwiP0AiQEEjIyMjQkMkQ0QnJycnSEkoS0wpKSkrUFErUVIsLCwvV1gvWFkwWlszMzMzYGE1NTU2NjY3Zmc4aWo5OTk5ams5a2w6Ojo6bG07bm88cXI9cnM9c3Q/dndAQEBAeHlBeXpCQkJCe3xCfH1DQ0NEREREf4FFRUVFgIJGg4VHhYdISEhIhohJh4lLi41LjI5MTExMjpBNj5FNkJJOkpRQUFBQlZdRUVFSUlJTU1NTmpxUVFRUnZ9VVVVVnqBWVlZYpadZWVlZp6laqKpbW1tbqatbqqxcXFxcrK5dra9drrBeXl5er7FfsbNfsrRgs7VhYWFiYmJiuLpjubtku71lvL5lvb9mvsBnwcNowsRpxMZpxcdra2tryctsysxubm5uzc9vb29vz9Fw0dNx0tVy1Ndy1dhz1tlz19p0dHR02Nt02dx12t1229523N93d3d33eB33uF5eXl54eR6enp64+Z65Od75eh75ul8fHx85+p86Ot96ex96u2AgICA7vGA7/KB8fSC8/aD9PeD9fiEhISE9/qF+PuF+fyGhoaG+v2G+/6Hh4eH/P+IiIiMjIyNjY2Ojo6QkJCRkZGSkpKTk5Obm5ucnJyfn5+lpaWnp6eoqKipqamqqqqwsLCzs7O1tbW4uLi5ubm6urq7u7u8vLy/v7/BwcHCwsLFxcXGxsbPz8/Y2Nji4uLj4+Pv7+/4+Pj5+fn+/v7/75T///+GLm1tAAAAAWJLR0T7omo23AAABJtJREFUeNrt3Wd8E3UYB/CH0oqm1dJaS5N0IKu0qQSVinXG4gKlKFi3uMC9FVwoVQnQqCBgBVxFnKCoFFFExFGhliWt/zoYLuIMKEpB7b3xuf9dQu+MvAjXcsTf7/PJk/ul1/S+TS53r3KkNFfk0V6evDHbFGruQ3EQTzNVUFxkHOXFB6QbIQiCIAiC/GeSs/QkR6vkCPeUaNUeSUjkkdR1npCp6a7VV7U6P1dbKfNFrS89rJNas/T6rlZtkUS/i2evhw99Q92y9/r7nVzzw7VfeDX3y2qv893plTVb1uW+uw6xiyNpspAQ8bjLy8l5REiImOlUq3Pniunyxw8Ib+vqF7aB5AgdItLVmit0iOgc9W0owhDt1RSAABL3EGeDDqmXhwRXgw6pj3qESFhtgHC1DYSGrJCQjweFq4SEqzkD67zGah8Inay+p1yl4XqKWt2lF69UDxQrzzevXZprrDn2gfTIUs85Iv/oHpny8HKHdugeVZhpXNudu6u6J1P8lmpIX1ys10X6myVfPeLl919UZFi74JXjWtfCecfa5sj+odx908XSg9Taqdaw+3I1QuYLA6RG2AbiEDpE9JJnvcYP1BRhgiw3QuoAASTuIQnP6JCF8hQlcbYBwrWIKgPDIg9UGSGP2QdCnZ+QkDneKQs4swqe1CDJ09RaXfBUETWKm3a+gFMMEMc0+0AoJVX9nM1+VDsCznLurz64b5VWq7nWLLi81QfygYZfNlU7nAUP0nOwrLnGiiAIgiAIgiAIgiDI/zstLS3tMEtKSiycgAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBYAkEQBEEQBEEQBEEQBGmrdLwuyLmhg703km8Z63k7N2Tw0jnqFt/f0bROn69WBYOfbuxiyR+8MXC9vB8QCBTQkEAgMOG2gVyvDmTzdAWuifFp077m8f503vwZr/PSd28Hg+uaTjVDlOFEIxVrINVijfwi4glCHE1XioXPz6kX9xHNFIUkvyM/xqeduIPHup95bGni8edYotOUqJCrrII0iMv4LnNFg4Sczd/9/Zw4abchD0Ygv0pIBVFZG0Nq587lu/PE02EIXSQuaSfI92l88bfNFkHqLxUnEM1+bXQEMloMY8hgn893esyQIzbzWHtveXn51GW89AtfTeyATWZIWm919s6wBtLYdfXdVCyuuEdCHhoxwr/mAzdDtMQKoaP4duQmRVG+kUtyu83X3OuylX09f+9r0c6eOvkjx82fdPdLiHrdjsrD1Z39LP5W06ExQ475g8eqSR6PZ+oXvLSVNWk/nmmGKNcSXaBYBXEPFkMXV1GlhFyYlSof3t19ZOxfPJp+4/HTeh47JhGdqLQxJDtpyRJxBgUi+0g7QkYSlVsHoVtFrcNiyO0SsoXHDxIykej4v/8F+XxDKLRxmXWQfo2jyGJIh894PDs9FArNeIGXvlwbCn37Upl5rXObOMPtf1K4z5u8ne/sx0tl6hbfgtNkBEGQPZs4uUBwTxoTH5DxtM0TD46+20lpHrfXX7e52/jtyj9kFKbIT2L3FQAAAABJRU5ErkJggg=="); + @Mock TbContext ctxMock; @Mock AiModelService aiModelServiceMock; @Mock RuleEngineAiChatModelService aiChatModelServiceMock; + @Mock + TbResourceDataCache tbResourceDataCacheMock; + @Mock + ResourceService resourceServiceMock; TbAiNode aiNode; TbAiNodeConfiguration config; @@ -141,6 +162,8 @@ class TbAiNodeTest { lenient().when(ctxMock.getAiModelService()).thenReturn(aiModelServiceMock); lenient().when(ctxMock.getAiChatModelService()).thenReturn(aiChatModelServiceMock); lenient().when(ctxMock.getDbCallbackExecutor()).thenReturn(new TestDbCallbackExecutor()); + lenient().when(ctxMock.getTbResourceDataCache()).thenReturn(tbResourceDataCacheMock); + lenient().when(ctxMock.getResourceService()).thenReturn(resourceServiceMock); } @Test @@ -158,6 +181,7 @@ class TbAiNodeTest { assertThat(config.getResponseFormat()).isEqualTo(new TbJsonResponseFormat()); assertThat(config.getTimeoutSeconds()).isEqualTo(60); assertThat(config.isForceAck()).isTrue(); + assertThat(config.getResourceIds()).isNull(); } /* -- Node initialization tests -- */ @@ -373,6 +397,36 @@ class TbAiNodeTest { .matches(e -> ((TbNodeException) e).isUnrecoverable()); } + @Test + void givenNotExistingResources_whenInit_thenThrowsException() { + // GIVEN + config = constructValidConfig(); + UUID resourceId = UUID.randomUUID(); + config.setResourceIds(Set.of(resourceId)); + + // WHEN-THEN + assertThatThrownBy(() -> aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(TbNodeException.class) + .hasMessageContaining("[" + tenantId + "] Resource with ID: [" + resourceId + "] was not found"); + } + + @Test + void givenResourceOfWrongType_whenInit_thenThrowsException() { + // GIVEN + config = constructValidConfig(); + UUID resourceId = UUID.randomUUID(); + config.setResourceIds(Set.of(resourceId)); + + // WHEN-THEN + TbResource tbResource = new TbResource(); + tbResource.setResourceType(ResourceType.DASHBOARD); + given(resourceServiceMock.findResourceInfoById(any(), any())).willReturn(tbResource); + + assertThatThrownBy(() -> aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(TbNodeException.class) + .hasMessageContaining("[" + tenantId + "] Resource with ID: [" + resourceId + "] has unsupported resource type: " + ResourceType.DASHBOARD); + } + /* -- Message processing tests -- */ @Test @@ -560,6 +614,166 @@ class TbAiNodeTest { ); } + @Test + void givenSystemPromptAndUserPromptAndResourcesConfigured_whenOnMsg_thenRequestContainsSystemAndUserAndResourceContent() throws TbNodeException { + String systemPrompt = "Respond with valid JSON"; + String userPrompt = "Tell me a joke"; + String textData = "Text resource content for AI request."; + String xmlData = ""; + + // GIVEN + config = constructValidConfig(); + config.setSystemPrompt(systemPrompt); + config.setUserPrompt(userPrompt); + UUID resourceId = UUID.randomUUID(); + UUID resourceId2 = UUID.randomUUID(); + UUID resourceId3 = UUID.randomUUID(); + + config.setResourceIds(Set.of(resourceId, resourceId2, resourceId3)); + + // WHEN-THEN + TbResource textResource = buildGeneralResource(textData.getBytes(), "text/plain"); + TbResource xmlResource = buildGeneralResource(xmlData.getBytes(), "application/xml"); + TbResource imageResource = buildGeneralResource(PNG_IMAGE, "image/png"); + + given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId)))).willReturn(textResource); + given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId2)))).willReturn(xmlResource); + given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId3)))).willReturn(imageResource); + + given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId)))).willReturn(FluentFuture.from(Futures.immediateFuture(textResource.toResourceDataInfo()))); + given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId2)))).willReturn(FluentFuture.from(Futures.immediateFuture(xmlResource.toResourceDataInfo()))); + given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId3)))).willReturn(FluentFuture.from(Futures.immediateFuture(imageResource.toResourceDataInfo()))); + + aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + var msg = TbMsg.newMsg() + .originator(deviceId) + .data(TbMsg.EMPTY_JSON_OBJECT) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + var chatResponse = ChatResponse.builder() + .aiMessage(AiMessage.from("{\"type\":\"joke\",\"setup\":\"Why did the scarecrow win an award?\",\"punchline\":\"Because he was outstanding in his field.\"}")) + .build(); + + given(aiChatModelServiceMock.sendChatRequestAsync(any(), any())).willReturn(FluentFuture.from(immediateFuture(chatResponse))); + + // WHEN + aiNode.onMsg(ctxMock, msg); + + // THEN + then(aiChatModelServiceMock).should().sendChatRequestAsync(any(), + argThat(actualChatRequest -> { + assertThat(actualChatRequest.messages()).hasSize(2); + assertThat(actualChatRequest.messages().get(0)).isEqualTo(SystemMessage.from(systemPrompt)); + assertThat(((UserMessage)actualChatRequest.messages().get(1)).contents()) + .containsAll(List.of(new TextContent(userPrompt), new TextContent(textData), + new TextContent(xmlData), new ImageContent(Base64.getEncoder().encodeToString(PNG_IMAGE), "image/png"))); + return true; + }) + ); + } + + @Test + void givenNullResource_whenOnMsg_thenRequestContainsSystemAndUserPrompt() throws TbNodeException { + // GIVEN + config = constructValidConfig(); + UUID resourceId = UUID.randomUUID(); + config.setResourceIds(Set.of(resourceId)); + + // WHEN-THEN + TbResource tbResource = buildGeneralResource("Text resource content for AI request.".getBytes(), "text/plain"); + + given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId)))).willReturn(tbResource); + given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId)))).willReturn(FluentFuture.from(Futures.immediateFuture(null))); + + aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + var msg = TbMsg.newMsg() + .originator(deviceId) + .data(TbMsg.EMPTY_JSON_OBJECT) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + aiNode.onMsg(ctxMock, msg); + + // THEN + then(aiChatModelServiceMock).should().sendChatRequestAsync(any(), + argThat(actualChatRequest -> { + assertThat(actualChatRequest.messages()).hasSize(2); + assertThat(actualChatRequest.messages().get(0)).isEqualTo(SystemMessage.from(config.getSystemPrompt())); + assertThat(((UserMessage)actualChatRequest.messages().get(1)).contents()) + .containsAll(List.of(new TextContent(config.getUserPrompt()))); + return true; + }) + ); + } + + @Test + void givenResourceWithNoDescriptor_whenOnMsg_thenEnqueueForTellFailure() throws TbNodeException { + // GIVEN + config = constructValidConfig(); + UUID resourceId = UUID.randomUUID(); + config.setResourceIds(Set.of(resourceId)); + + // WHEN-THEN + TbResource tbResource = buildGeneralResource("Text resource content for AI request.".getBytes(), "text/plain"); + TbResourceDataInfo resourceDataInfo = new TbResourceDataInfo(tbResource.getData(), null); + + given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId)))).willReturn(tbResource); + given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId)))).willReturn(FluentFuture.from(Futures.immediateFuture(resourceDataInfo))); + + aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + var msg = TbMsg.newMsg() + .originator(deviceId) + .data(TbMsg.EMPTY_JSON_OBJECT) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + aiNode.onMsg(ctxMock, msg); + + // THEN + var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class); + then(ctxMock).should().enqueueForTellFailure(any(), exceptionCaptor.capture()); + Throwable actualException = exceptionCaptor.getValue(); + assertThat(actualException.getMessage()).isEqualTo("Missing descriptor for resource"); + } + + @Test + void givenResourceWithNoMediaType_whenOnMsg_thenEnqueueForTellFailure() throws TbNodeException { + // GIVEN + config = constructValidConfig(); + UUID resourceId = UUID.randomUUID(); + config.setResourceIds(Set.of(resourceId)); + + // WHEN-THEN + TbResource tbResource = buildGeneralResource("Text resource content for AI request.".getBytes(), "text/plain"); + TbResourceDataInfo resourceDataInfo = new TbResourceDataInfo(tbResource.getData(), JacksonUtil.newObjectNode()); + + given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId)))).willReturn(tbResource); + given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId)))).willReturn(FluentFuture.from(Futures.immediateFuture(resourceDataInfo))); + + aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + var msg = TbMsg.newMsg() + .originator(deviceId) + .data(TbMsg.EMPTY_JSON_OBJECT) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + aiNode.onMsg(ctxMock, msg); + + // THEN + var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class); + then(ctxMock).should().enqueueForTellFailure(any(), exceptionCaptor.capture()); + Throwable actualException = exceptionCaptor.getValue(); + assertThat(actualException.getMessage()).isEqualTo("Missing mediaType in resource descriptor {}"); + } + @Test void givenTemplatedPrompts_whenOnMsg_thenRequestContainsSubstitutedMessages() throws TbNodeException { // GIVEN @@ -950,4 +1164,13 @@ class TbAiNodeTest { then(ctxMock).should(never()).tellFailure(any(), any()); } + private TbResource buildGeneralResource(byte[] data, String mediaType) { + TbResource tbResource = new TbResource(); + tbResource.setResourceType(GENERAL); + GeneralFileDescriptor descriptor = new GeneralFileDescriptor(mediaType); + tbResource.setDescriptorValue(descriptor); + tbResource.setData(data); + return tbResource; + } + } From e477dd17b641488546d0303c06b495fa45067c7c Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Fri, 26 Sep 2025 13:34:58 +0300 Subject: [PATCH 157/216] Improved Edge session cleanup --- .../service/edge/rpc/EdgeGrpcService.java | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java index 7340696788..ec3f839cb3 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java @@ -93,14 +93,13 @@ import static org.thingsboard.server.service.state.DefaultDeviceStateService.LAS @TbCoreComponent public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase implements EdgeRpcService { - private static final int DESTROY_SESSION_MAX_ATTEMPTS = 10; - private final ConcurrentMap sessions = new ConcurrentHashMap<>(); private final ConcurrentMap sessionNewEventsLocks = new ConcurrentHashMap<>(); private final Map sessionNewEvents = new HashMap<>(); private final ConcurrentMap> sessionEdgeEventChecks = new ConcurrentHashMap<>(); private final ConcurrentMap> localSyncEdgeRequests = new ConcurrentHashMap<>(); private final ConcurrentMap edgeEventsMigrationProcessed = new ConcurrentHashMap<>(); + private final List zombieSessions = new ArrayList<>(); @Value("${edges.rpc.port}") private int rpcPort; @@ -193,7 +192,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i this.edgeEventProcessingExecutorService = ThingsBoardExecutors.newScheduledThreadPool(schedulerPoolSize, "edge-event-check-scheduler"); this.sendDownlinkExecutorService = ThingsBoardExecutors.newScheduledThreadPool(sendSchedulerPoolSize, "edge-send-scheduler"); this.executorService = ThingsBoardExecutors.newSingleThreadScheduledExecutor("edge-service"); - this.executorService.scheduleAtFixedRate(this::destroyKafkaSessionIfDisconnectedAndConsumerActive, 60, 60, TimeUnit.SECONDS); + this.executorService.scheduleAtFixedRate(this::cleanupZombieSessions, 60, 60, TimeUnit.SECONDS); log.info("Edge RPC service initialized!"); } @@ -518,14 +517,10 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i private void destroySession(EdgeGrpcSession session) { try (session) { - for (int i = 0; i < DESTROY_SESSION_MAX_ATTEMPTS; i++) { - if (session.destroy()) { - break; - } else { - try { - Thread.sleep(100); - } catch (InterruptedException ignored) {} - } + if (!session.destroy()) { + log.warn("[{}][{}] Session destroy failed for edge [{}] with session id [{}]. Adding to zombie queue for later cleanup.", + session.getTenantId(), session.getEdge().getId(), session.getEdge().getName(), session.getSessionId()); + zombieSessions.add(session); } } } @@ -634,7 +629,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i } } - private void destroyKafkaSessionIfDisconnectedAndConsumerActive() { + private void cleanupZombieSessions() { try { List toRemove = new ArrayList<>(); for (EdgeGrpcSession session : sessions.values()) { @@ -655,6 +650,17 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i } } } + zombieSessions.removeIf(zombie -> { + if (zombie.destroy()) { + log.info("[{}][{}] Successfully cleaned up zombie session [{}] for edge [{}].", + zombie.getTenantId(), zombie.getEdge().getId(), zombie.getSessionId(), zombie.getEdge().getName()); + return true; + } else { + log.warn("[{}][{}] Failed to remove zombie session [{}] for edge [{}].", + zombie.getTenantId(), zombie.getEdge().getId(), zombie.getSessionId(), zombie.getEdge().getName()); + return false; + } + }); } catch (Exception e) { log.warn("Failed to cleanup kafka sessions", e); } From 41f0a9702e57f8e1ba7ecc641c6f6b5fafe7b088 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 15 Sep 2025 17:32:57 +0300 Subject: [PATCH 158/216] AI models: add support for Ollama --- application/pom.xml | 4 ++ .../Langchain4jChatModelConfigurerImpl.java | 16 ++++++ .../common/data/ai/dto/TbChatResponse.java | 6 +- .../common/data/ai/model/AiModelConfig.java | 8 ++- .../data/ai/model/chat/AiChatModelConfig.java | 2 +- .../chat/Langchain4jChatModelConfigurer.java | 2 + .../ai/model/chat/OllamaChatModelConfig.java | 57 +++++++++++++++++++ .../common/data/ai/provider/AiProvider.java | 3 +- .../data/ai/provider/AiProviderConfig.java | 2 +- .../ai/provider/OllamaProviderConfig.java | 22 +++++++ .../rule/engine/ai/TbResponseFormat.java | 8 +-- .../ai-model/ai-model-dialog.component.html | 9 +++ .../ai-model/ai-model-dialog.component.ts | 1 + .../src/app/shared/models/ai-model.models.ts | 21 +++++-- .../assets/locale/locale.constant-en_US.json | 5 +- 15 files changed, 146 insertions(+), 20 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java diff --git a/application/pom.xml b/application/pom.xml index 33bc0972d4..0413f7732c 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -419,6 +419,10 @@ + + dev.langchain4j + langchain4j-ollama + diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java index 69dd98f47f..7008e866f4 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -32,6 +32,7 @@ import dev.langchain4j.model.chat.request.ChatRequestParameters; import dev.langchain4j.model.github.GitHubModelsChatModel; import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel; import dev.langchain4j.model.mistralai.MistralAiChatModel; +import dev.langchain4j.model.ollama.OllamaChatModel; import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel; import org.springframework.stereotype.Component; @@ -43,6 +44,7 @@ import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelC import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer; import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.OllamaChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig; import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig; import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig; @@ -262,6 +264,20 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .build(); } + @Override + public ChatModel configureChatModel(OllamaChatModelConfig chatModelConfig) { + return OllamaChatModel.builder() + .baseUrl(chatModelConfig.providerConfig().baseUrl()) + .modelName(chatModelConfig.modelId()) + .temperature(chatModelConfig.temperature()) + .topP(chatModelConfig.topP()) + .topK(chatModelConfig.topK()) + .numPredict(chatModelConfig.maxOutputTokens()) + .timeout(toDuration(chatModelConfig.timeoutSeconds())) + .maxRetries(chatModelConfig.maxRetries()) + .build(); + } + private static Duration toDuration(Integer timeoutSeconds) { return timeoutSeconds != null ? Duration.ofSeconds(timeoutSeconds) : null; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java index 2cc17e4553..73e6557fb5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java @@ -22,7 +22,7 @@ import io.swagger.v3.oas.annotations.media.Schema; @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, property = "status", - include = JsonTypeInfo.As.PROPERTY, + include = JsonTypeInfo.As.EXISTING_PROPERTY, visible = true ) @JsonSubTypes({ @@ -51,9 +51,7 @@ public sealed interface TbChatResponse permits TbChatResponse.Success, TbChatRes } record Failure( - @Schema( - description = "A string containing details about the failure" - ) + @Schema(description = "A string containing details about the failure") String errorDetails ) implements TbChatResponse { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java index bfaa29a6e3..f18429e7cf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.ai.model.chat.GitHubModelsChatModelCon import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.OllamaChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; @@ -34,6 +35,7 @@ import org.thingsboard.server.common.data.ai.provider.GitHubModelsProviderConfig import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig; import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig; +import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig; import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; @JsonTypeInfo( @@ -50,7 +52,8 @@ import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; @JsonSubTypes.Type(value = MistralAiChatModelConfig.class, name = "MISTRAL_AI"), @JsonSubTypes.Type(value = AnthropicChatModelConfig.class, name = "ANTHROPIC"), @JsonSubTypes.Type(value = AmazonBedrockChatModelConfig.class, name = "AMAZON_BEDROCK"), - @JsonSubTypes.Type(value = GitHubModelsChatModelConfig.class, name = "GITHUB_MODELS") + @JsonSubTypes.Type(value = GitHubModelsChatModelConfig.class, name = "GITHUB_MODELS"), + @JsonSubTypes.Type(value = OllamaChatModelConfig.class, name = "OLLAMA") }) public interface AiModelConfig { @@ -69,7 +72,8 @@ public interface AiModelConfig { @JsonSubTypes.Type(value = MistralAiProviderConfig.class, name = "MISTRAL_AI"), @JsonSubTypes.Type(value = AnthropicProviderConfig.class, name = "ANTHROPIC"), @JsonSubTypes.Type(value = AmazonBedrockProviderConfig.class, name = "AMAZON_BEDROCK"), - @JsonSubTypes.Type(value = GitHubModelsProviderConfig.class, name = "GITHUB_MODELS") + @JsonSubTypes.Type(value = GitHubModelsProviderConfig.class, name = "GITHUB_MODELS"), + @JsonSubTypes.Type(value = OllamaProviderConfig.class, name = "OLLAMA") }) AiProviderConfig providerConfig(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java index 2bc28cfce0..49126c1861 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java @@ -24,7 +24,7 @@ public sealed interface AiChatModelConfig> extend permits OpenAiChatModelConfig, AzureOpenAiChatModelConfig, GoogleAiGeminiChatModelConfig, GoogleVertexAiGeminiChatModelConfig, MistralAiChatModelConfig, AnthropicChatModelConfig, - AmazonBedrockChatModelConfig, GitHubModelsChatModelConfig { + AmazonBedrockChatModelConfig, GitHubModelsChatModelConfig, OllamaChatModelConfig { ChatModel configure(Langchain4jChatModelConfigurer configurer); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java index c9c1bc3173..828256dcdc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java @@ -35,4 +35,6 @@ public interface Langchain4jChatModelConfigurer { ChatModel configureChatModel(GitHubModelsChatModelConfig chatModelConfig); + ChatModel configureChatModel(OllamaChatModelConfig chatModelConfig); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java new file mode 100644 index 0000000000..360b514d6d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java @@ -0,0 +1,57 @@ +/** + * 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.common.data.ai.model.chat; + +import dev.langchain4j.model.chat.ChatModel; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.Builder; +import lombok.With; +import org.thingsboard.server.common.data.ai.provider.AiProvider; +import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig; + +@Builder +public record OllamaChatModelConfig( + @NotNull @Valid OllamaProviderConfig providerConfig, + @NotBlank String modelId, + @PositiveOrZero Double temperature, + @Positive @Max(1) Double topP, + @PositiveOrZero Integer topK, + @Positive Integer maxOutputTokens, + @With @Positive Integer timeoutSeconds, + @With @PositiveOrZero Integer maxRetries +) implements AiChatModelConfig { + + @Override + public AiProvider provider() { + return AiProvider.OLLAMA; + } + + @Override + public ChatModel configure(Langchain4jChatModelConfigurer configurer) { + return configurer.configureChatModel(this); + } + + @Override + public boolean supportsJsonMode() { + return true; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java index d0a5bd0510..a9a6af4de8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java @@ -24,6 +24,7 @@ public enum AiProvider { MISTRAL_AI, ANTHROPIC, AMAZON_BEDROCK, - GITHUB_MODELS + GITHUB_MODELS, + OLLAMA } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java index bd32c88efb..5423b24410 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java @@ -19,4 +19,4 @@ public sealed interface AiProviderConfig permits OpenAiProviderConfig, AzureOpenAiProviderConfig, GoogleAiGeminiProviderConfig, GoogleVertexAiGeminiProviderConfig, MistralAiProviderConfig, AnthropicProviderConfig, - AmazonBedrockProviderConfig, GitHubModelsProviderConfig {} + AmazonBedrockProviderConfig, GitHubModelsProviderConfig, OllamaProviderConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java new file mode 100644 index 0000000000..fc0a2d6fd8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java @@ -0,0 +1,22 @@ +/** + * 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.common.data.ai.provider; + +import jakarta.validation.constraints.NotBlank; + +public record OllamaProviderConfig( + @NotBlank String baseUrl +) implements AiProviderConfig {} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbResponseFormat.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbResponseFormat.java index 5c891a9c74..5107e613a4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbResponseFormat.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbResponseFormat.java @@ -60,9 +60,7 @@ public sealed interface TbResponseFormat permits TbTextResponseFormat, TbJsonRes @Override public ResponseFormat toLangChainResponseFormat() { - return ResponseFormat.builder() - .type(ResponseFormatType.TEXT) - .build(); + return ResponseFormat.TEXT; } } @@ -76,9 +74,7 @@ public sealed interface TbResponseFormat permits TbTextResponseFormat, TbJsonRes @Override public ResponseFormat toLangChainResponseFormat() { - return ResponseFormat.builder() - .type(ResponseFormatType.JSON) - .build(); + return ResponseFormat.JSON; } } diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html index 5f9c189399..1a3c2bf181 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html @@ -150,6 +150,15 @@ } + @if (providerFieldsList.includes('baseUrl')) { + + ai-models.baseurl + + + {{ 'ai-models.baseurl-required' | translate }} + + + }
diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts index c459d66f12..e6490cd84d 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts @@ -100,6 +100,7 @@ export class AIModelDialogComponent extends DialogComponent, 'label'>, HasTenantId region?: string; accessKeyId?: string; secretAccessKey?: string; + baseUrl?: string; }; modelId: string; temperature?: number; @@ -57,7 +58,8 @@ export enum AiProvider { MISTRAL_AI = 'MISTRAL_AI', ANTHROPIC = 'ANTHROPIC', AMAZON_BEDROCK = 'AMAZON_BEDROCK', - GITHUB_MODELS = 'GITHUB_MODELS' + GITHUB_MODELS = 'GITHUB_MODELS', + OLLAMA = 'OLLAMA' } export const AiProviderTranslations = new Map( @@ -69,7 +71,8 @@ export const AiProviderTranslations = new Map( [AiProvider.MISTRAL_AI , 'ai-models.ai-providers.mistral-ai'], [AiProvider.ANTHROPIC , 'ai-models.ai-providers.anthropic'], [AiProvider.AMAZON_BEDROCK , 'ai-models.ai-providers.amazon-bedrock'], - [AiProvider.GITHUB_MODELS , 'ai-models.ai-providers.github-models'] + [AiProvider.GITHUB_MODELS , 'ai-models.ai-providers.github-models'], + [AiProvider.OLLAMA , 'ai-models.ai-providers.ollama'] ] ); @@ -84,7 +87,8 @@ export const ProviderFieldsAllList = [ 'serviceVersion', 'region', 'accessKeyId', - 'secretAccessKey' + 'secretAccessKey', + 'baseUrl' ]; export const ModelFieldsAllList = ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens']; @@ -191,6 +195,14 @@ export const AiModelMap = new Map Date: Tue, 16 Sep 2025 14:41:38 +0300 Subject: [PATCH 159/216] AI models: add context length support for Ollama --- .../Langchain4jChatModelConfigurerImpl.java | 1 + .../chat/AmazonBedrockChatModelConfig.java | 2 +- .../model/chat/AnthropicChatModelConfig.java | 2 +- .../chat/AzureOpenAiChatModelConfig.java | 2 +- .../chat/GitHubModelsChatModelConfig.java | 2 +- .../chat/GoogleAiGeminiChatModelConfig.java | 2 +- .../GoogleVertexAiGeminiChatModelConfig.java | 2 +- .../model/chat/MistralAiChatModelConfig.java | 2 +- .../ai/model/chat/OllamaChatModelConfig.java | 3 ++- .../ai/model/chat/OpenAiChatModelConfig.java | 2 +- .../ai-model/ai-model-dialog.component.html | 23 +++++++++++-------- .../ai-model/ai-model-dialog.component.ts | 3 ++- .../src/app/shared/models/ai-model.models.ts | 5 ++-- .../assets/locale/locale.constant-en_US.json | 3 ++- 14 files changed, 31 insertions(+), 23 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java index 7008e866f4..84b09b9188 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -272,6 +272,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .temperature(chatModelConfig.temperature()) .topP(chatModelConfig.topP()) .topK(chatModelConfig.topK()) + .numCtx(chatModelConfig.contextLength()) .numPredict(chatModelConfig.maxOutputTokens()) .timeout(toDuration(chatModelConfig.timeoutSeconds())) .maxRetries(chatModelConfig.maxRetries()) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java index 2bb4de5aa8..d2ab72086a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java @@ -33,7 +33,7 @@ public record AmazonBedrockChatModelConfig( @NotBlank String modelId, @PositiveOrZero Double temperature, @Positive @Max(1) Double topP, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java index 69b5578fb3..6d505f75a6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java @@ -34,7 +34,7 @@ public record AnthropicChatModelConfig( @PositiveOrZero Double temperature, @Positive @Max(1) Double topP, @PositiveOrZero Integer topK, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java index 47e7e96c37..f70f2af539 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java @@ -35,7 +35,7 @@ public record AzureOpenAiChatModelConfig( @Positive @Max(1) Double topP, Double frequencyPenalty, Double presencePenalty, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java index b509254f77..0aafd72197 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java @@ -35,7 +35,7 @@ public record GitHubModelsChatModelConfig( @Positive @Max(1) Double topP, Double frequencyPenalty, Double presencePenalty, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java index fe11a11460..b5c3d4263d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java @@ -36,7 +36,7 @@ public record GoogleAiGeminiChatModelConfig( @PositiveOrZero Integer topK, Double frequencyPenalty, Double presencePenalty, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java index 609e14f86e..944963ee27 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java @@ -36,7 +36,7 @@ public record GoogleVertexAiGeminiChatModelConfig( @PositiveOrZero Integer topK, Double frequencyPenalty, Double presencePenalty, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java index f603e99c53..8f67d93398 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java @@ -35,7 +35,7 @@ public record MistralAiChatModelConfig( @Positive @Max(1) Double topP, Double frequencyPenalty, Double presencePenalty, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java index 360b514d6d..ea48670b63 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java @@ -34,7 +34,8 @@ public record OllamaChatModelConfig( @PositiveOrZero Double temperature, @Positive @Max(1) Double topP, @PositiveOrZero Integer topK, - @Positive Integer maxOutputTokens, + Integer contextLength, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java index 00b5115d7d..23db9accc2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java @@ -35,7 +35,7 @@ public record OpenAiChatModelConfig( @Positive @Max(1) Double topP, Double frequencyPenalty, Double presencePenalty, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html index 1a3c2bf181..c730850474 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html @@ -151,7 +151,7 @@ } @if (providerFieldsList.includes('baseUrl')) { - + ai-models.baseurl @@ -264,15 +264,18 @@ - - warning - + type="number" step="1" placeholder="{{ 'ai-models.set' | translate }}"> + + + } + @if (modelFieldsList.includes('contextLength')) { +
+
+ {{ 'ai-models.context-length' | translate }} +
+ +
} diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts index e6490cd84d..3294c6ac76 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts @@ -108,7 +108,8 @@ export class AIModelDialogComponent extends DialogComponent, 'label'>, HasTenantId frequencyPenalty?: number; presencePenalty?: number; maxOutputTokens?: number; + contextLength?: number; } } @@ -91,7 +92,7 @@ export const ProviderFieldsAllList = [ 'baseUrl' ]; -export const ModelFieldsAllList = ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens']; +export const ModelFieldsAllList = ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens', 'contextLength']; export const AiModelMap = new Map([ [ @@ -200,7 +201,7 @@ export const AiModelMap = new Map Date: Mon, 22 Sep 2025 12:18:06 +0300 Subject: [PATCH 160/216] AI models: add auth support for Ollama --- .../Langchain4jChatModelConfigurerImpl.java | 28 +++++++++++++--- .../ai/provider/OllamaProviderConfig.java | 32 +++++++++++++++++-- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java index 84b09b9188..2cb6c2097f 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -35,6 +35,7 @@ import dev.langchain4j.model.mistralai.MistralAiChatModel; import dev.langchain4j.model.ollama.OllamaChatModel; import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel; +import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig; @@ -49,6 +50,7 @@ import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig; import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig; import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig; import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; +import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.regions.Region; @@ -56,7 +58,11 @@ import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.Base64; + +import static java.util.Collections.singletonMap; @Component class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer { @@ -136,7 +142,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur // set request timeout from model config if (chatModelConfig.timeoutSeconds() != null) { - retrySettings.setTotalTimeout(org.threeten.bp.Duration.ofSeconds(chatModelConfig.timeoutSeconds())); + retrySettings.setTotalTimeoutDuration(Duration.ofSeconds(chatModelConfig.timeoutSeconds())); } // set updated retry settings @@ -266,7 +272,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur @Override public ChatModel configureChatModel(OllamaChatModelConfig chatModelConfig) { - return OllamaChatModel.builder() + var builder = OllamaChatModel.builder() .baseUrl(chatModelConfig.providerConfig().baseUrl()) .modelName(chatModelConfig.modelId()) .temperature(chatModelConfig.temperature()) @@ -275,8 +281,22 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .numCtx(chatModelConfig.contextLength()) .numPredict(chatModelConfig.maxOutputTokens()) .timeout(toDuration(chatModelConfig.timeoutSeconds())) - .maxRetries(chatModelConfig.maxRetries()) - .build(); + .maxRetries(chatModelConfig.maxRetries()); + + var auth = chatModelConfig.providerConfig().auth(); + if (auth instanceof OllamaProviderConfig.OllamaAuth.Basic basicAuth) { + String credentials = basicAuth.username() + ":" + basicAuth.password(); + String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + builder.customHeaders(singletonMap(HttpHeaders.AUTHORIZATION, "Basic " + encodedCredentials)); + } else if (auth instanceof OllamaProviderConfig.OllamaAuth.Token tokenAuth) { + builder.customHeaders(singletonMap(HttpHeaders.AUTHORIZATION, "Bearer " + tokenAuth.token())); + } else if (auth instanceof OllamaProviderConfig.OllamaAuth.None) { + // do nothing + } else { + throw new UnsupportedOperationException("Unknown authentication type: " + auth.getClass().getSimpleName()); + } + + return builder.build(); } private static Duration toDuration(Integer timeoutSeconds) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java index fc0a2d6fd8..39bb57834c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java @@ -15,8 +15,34 @@ */ package org.thingsboard.server.common.data.ai.provider; -import jakarta.validation.constraints.NotBlank; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; public record OllamaProviderConfig( - @NotBlank String baseUrl -) implements AiProviderConfig {} + @NotNull String baseUrl, + @NotNull @Valid OllamaAuth auth +) implements AiProviderConfig { + + @JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" + ) + @JsonSubTypes({ + @JsonSubTypes.Type(value = OllamaAuth.None.class, name = "NONE"), + @JsonSubTypes.Type(value = OllamaAuth.Basic.class, name = "BASIC"), + @JsonSubTypes.Type(value = OllamaAuth.Token.class, name = "TOKEN") + }) + public sealed interface OllamaAuth { + + record None() implements OllamaAuth {} + + record Basic(@NotNull String username, @NotNull String password) implements OllamaAuth {} + + record Token(@NotNull String token) implements OllamaAuth {} + + } + +} From 329534df61f80eee17e6536cec449dd08028a8ea Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Thu, 25 Sep 2025 15:44:50 +0300 Subject: [PATCH 161/216] UI: Add authentication for Ollama model --- .../ai-model/ai-model-dialog.component.html | 87 +++++++++++++++---- .../ai-model/ai-model-dialog.component.ts | 67 +++++++++++--- .../src/app/shared/models/ai-model.models.ts | 11 +++ .../assets/locale/locale.constant-en_US.json | 16 +++- 4 files changed, 153 insertions(+), 28 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html index c730850474..abfe8500b4 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html @@ -55,31 +55,34 @@
-
+
@if (providerFieldsList.includes('personalAccessToken')) { ai-models.personal-access-token - + {{ 'ai-models.personal-access-token-required' | translate }} } @if (providerFieldsList.includes('projectId')) { - + ai-models.project-id - + {{ 'ai-models.project-id-required' | translate }} } @if (providerFieldsList.includes('location')) { - + ai-models.location - + {{ 'ai-models.location-required' | translate }} @@ -98,16 +101,17 @@ } @if (providerFieldsList.includes('endpoint')) { - + ai-models.endpoint - + {{ 'ai-models.endpoint-required' | translate }} } @if (providerFieldsList.includes('serviceVersion')) { - + ai-models.service-version @@ -117,25 +121,28 @@ ai-models.api-key - + {{ 'ai-models.api-key-required' | translate }} } @if (providerFieldsList.includes('region')) { - + ai-models.region - + {{ 'ai-models.region-required' | translate }} } @if (providerFieldsList.includes('accessKeyId')) { - + ai-models.access-key-id - + {{ 'ai-models.access-key-id-required' | translate }} @@ -145,7 +152,8 @@ ai-models.secret-access-key - + {{ 'ai-models.secret-access-key-required' | translate }} @@ -154,11 +162,58 @@ ai-models.baseurl - + {{ 'ai-models.baseurl-required' | translate }} } + @if (provider === aiProvider.OLLAMA) { +
+
+
+ {{ 'ai-models.authentication' | translate }} +
+ + {{ 'ai-models.authentication-type.none' | translate }} + {{ 'ai-models.authentication-type.basic' | translate }} + {{ 'ai-models.authentication-type.token' | translate }} + +
+
+ @if (aiModelForms.get('configuration.providerConfig.auth.type').value === AuthenticationType.BASIC) { + + ai-models.username + + + {{ 'ai-models.username-required' | translate }} + + + + ai-models.password + + + + {{ 'ai-models.password-required' | translate }} + + + } + @if (aiModelForms.get('configuration.providerConfig.auth.type').value === AuthenticationType.TOKEN) { + + ai-models.token + + + + {{ 'ai-models.token-required' | translate }} + + + } +
+
+ }
diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts index 3294c6ac76..9d0d28e627 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts @@ -30,6 +30,7 @@ import { AiModelMap, AiProvider, AiProviderTranslations, + AuthenticationType, ModelType, ProviderFieldsAllList } from '@shared/models/ai-model.models'; @@ -37,6 +38,7 @@ import { AiModelService } from '@core/http/ai-model.service'; import { CheckConnectivityDialogComponent } from '@home/components/ai-model/check-connectivity-dialog.component'; import { map } from 'rxjs/operators'; import { deepTrim } from '@core/utils'; +import { TranslateService } from '@ngx-translate/core'; export interface AIModelDialogData { AIModel?: AiModel; @@ -62,18 +64,23 @@ export class AIModelDialogComponent extends DialogComponent, protected router: Router, protected dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: AIModelDialogData, private fb: FormBuilder, private aiModelService: AiModelService, + private translate: TranslateService, private dialog: MatDialog) { super(store, router, dialogRef); @@ -89,18 +96,24 @@ export class AIModelDialogComponent extends DialogComponent { + this.getAuthenticationHint(type); + this.aiModelForms.get('configuration.providerConfig.auth.username').disable(); + this.aiModelForms.get('configuration.providerConfig.auth.password').disable(); + this.aiModelForms.get('configuration.providerConfig.auth.token').disable(); + if (type === AuthenticationType.BASIC) { + this.aiModelForms.get('configuration.providerConfig.auth.username').enable(); + this.aiModelForms.get('configuration.providerConfig.auth.password').enable(); + } + if (type === AuthenticationType.TOKEN) { + this.aiModelForms.get('configuration.providerConfig.auth.token').enable(); + } + }); this.updateValidation(this.provider); } @@ -132,6 +161,16 @@ export class AIModelDialogComponent extends DialogComponent { if (AiModelMap.get(provider).providerFieldsList.includes(key)) { @@ -139,7 +178,13 @@ export class AIModelDialogComponent extends DialogComponent, 'label'>, HasTenantId accessKeyId?: string; secretAccessKey?: string; baseUrl?: string; + auth?: { + type: AuthenticationType; + username?: string; + password?: string; + token?: string + } }; modelId: string; temperature?: number; @@ -242,3 +248,8 @@ export interface CheckConnectivityResult { status: string; errorDetails: string; } +export enum AuthenticationType { + NONE = 'NONE', + BASIC = 'BASIC', + TOKEN = 'TOKEN' +} diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 5e527ee1db..67c5d99f24 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1169,7 +1169,21 @@ "check-connectivity-failed": "Test request failed", "no-model-matching": "No models matching '{{entity}}' were found.", "model-required": "Model is required.", - "no-model-text": "No models found." + "no-model-text": "No models found.", + "authentication": "Authentication", + "authentication-basic-hint": "Uses standard HTTP Basic authentication. The username and password will be combined, Base64-encoded, and sent in an \"Authorization\" header with each request to the Ollama server.", + "authentication-token-hint": "Uses Bearer token authentication. The provided token will be sent directly in an \"Authorization\" eader with each request to the Ollama server.", + "authentication-type": { + "none": "None", + "basic": "Basic", + "token": "Token" + }, + "username": "Username", + "username-required": "Username is required.", + "password": "Password", + "password-required": "Password is required.", + "token": "Token", + "token-required": "Token is required." }, "confirm-on-exit": { "message": "You have unsaved changes. Are you sure you want to leave this page?", From c3d40a61e879200291ea30a5b13b943b294c484b Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Tue, 2 Sep 2025 14:30:09 +0300 Subject: [PATCH 162/216] UI: Add new resource type text --- ui-ngx/src/app/core/http/entity.service.ts | 8 +- ui-ngx/src/app/core/http/resource.service.ts | 8 +- .../home/components/home-components.module.ts | 6 + .../resources/resources-dialog.component.html | 54 +++++++++ .../resources/resources-dialog.component.scss | 24 ++++ .../resources/resources-dialog.component.ts | 113 ++++++++++++++++++ .../resources-library.component.html | 2 +- .../resources}/resources-library.component.ts | 18 ++- .../external/ai-config.component.html | 10 ++ .../rule-node/external/ai-config.component.ts | 24 ++++ .../modules/home/pages/admin/admin.module.ts | 2 - .../resources-library-table-config.resolve.ts | 2 +- .../resources-table-header.component.ts | 2 +- .../entity/entity-list.component.html | 11 ++ .../entity/entity-list.component.ts | 29 ++++- .../src/app/shared/models/resource.models.ts | 12 +- .../assets/locale/locale.constant-en_US.json | 6 +- 17 files changed, 310 insertions(+), 21 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts rename ui-ngx/src/app/modules/home/{pages/admin/resource => components/resources}/resources-library.component.html (98%) rename ui-ngx/src/app/modules/home/{pages/admin/resource => components/resources}/resources-library.component.ts (89%) diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 572d6cc473..53a55bfe7f 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -100,6 +100,7 @@ import { OAuth2Service } from '@core/http/oauth2.service'; import { MobileAppService } from '@core/http/mobile-app.service'; import { PlatformType } from '@shared/models/oauth2.models'; import { AiModelService } from '@core/http/ai-model.service'; +import { ResourceType } from "@shared/models/resource.models"; @Injectable({ providedIn: 'root' @@ -297,6 +298,11 @@ export class EntityService { (id) => this.ruleChainService.getRuleChain(id, config), entityIds); break; + case EntityType.TB_RESOURCE: + observable = this.getEntitiesByIdsObservable( + (id) => this.resourceService.getResource(id, config), + entityIds); + break; } return observable; } @@ -472,7 +478,7 @@ export class EntityService { break; case EntityType.TB_RESOURCE: pageLink.sortOrder.property = 'title'; - entitiesObservable = this.resourceService.getTenantResources(pageLink, config); + entitiesObservable = this.resourceService.getTenantResources(pageLink, subType as ResourceType, config); break; case EntityType.QUEUE_STATS: pageLink.sortOrder.property = 'createdTime'; diff --git a/ui-ngx/src/app/core/http/resource.service.ts b/ui-ngx/src/app/core/http/resource.service.ts index 615b721b97..52c92d96e4 100644 --- a/ui-ngx/src/app/core/http/resource.service.ts +++ b/ui-ngx/src/app/core/http/resource.service.ts @@ -47,8 +47,12 @@ export class ResourceService { return this.http.get>(url, defaultHttpOptionsFromConfig(config)); } - public getTenantResources(pageLink: PageLink, config?: RequestConfig): Observable> { - return this.http.get>(`/api/resource/tenant${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + public getTenantResources(pageLink: PageLink, resourceType?: ResourceType, config?: RequestConfig): Observable> { + let url = `/api/resource${pageLink.toQuery()}`; + if (isNotEmptyStr(resourceType)) { + url += `&resourceType=${resourceType}`; + } + return this.http.get>(url, defaultHttpOptionsFromConfig(config)); } public getResource(resourceId: string, config?: RequestConfig): Observable { diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 31a3066edf..060f804cdf 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -205,6 +205,8 @@ import { } from '@home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component'; import { CheckConnectivityDialogComponent } from '@home/components/ai-model/check-connectivity-dialog.component'; import { AIModelDialogComponent } from '@home/components/ai-model/ai-model-dialog.component'; +import { ResourcesDialogComponent } from "@home/components/resources/resources-dialog.component"; +import { ResourcesLibraryComponent } from "@home/components/resources/resources-library.component"; @NgModule({ declarations: @@ -358,6 +360,8 @@ import { AIModelDialogComponent } from '@home/components/ai-model/ai-model-dialo CalculatedFieldTestArgumentsComponent, CheckConnectivityDialogComponent, AIModelDialogComponent, + ResourcesDialogComponent, + ResourcesLibraryComponent, ], imports: [ CommonModule, @@ -505,6 +509,8 @@ import { AIModelDialogComponent } from '@home/components/ai-model/ai-model-dialo CalculatedFieldTestArgumentsComponent, CheckConnectivityDialogComponent, AIModelDialogComponent, + ResourcesDialogComponent, + ResourcesLibraryComponent, ], providers: [ WidgetComponentService, diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html new file mode 100644 index 0000000000..c6813ff0f2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html @@ -0,0 +1,54 @@ + + + +

{{ 'resource.add' | translate }}

+ + +
+ + +
+
+ + +
+
+ + +
+ diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.scss b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.scss new file mode 100644 index 0000000000..b32c3933c5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.scss @@ -0,0 +1,24 @@ +/** + * 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. + */ + +:host ::ng-deep { + .mat-mdc-dialog-content { + display: flex; + flex-direction: column; + height: 100%; + padding: 0 !important; + } +} diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts new file mode 100644 index 0000000000..a06c72827a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts @@ -0,0 +1,113 @@ +/// +/// 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. +/// + +import { AfterViewInit, Component, Inject, SkipSelf, ViewChild } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { FormGroupDirective, NgForm, UntypedFormControl } from '@angular/forms'; +import { EntityType } from '@shared/models/entity-type.models'; +import { map } from 'rxjs/operators'; +import { ResourcesLibraryComponent } from "@home/components/resources/resources-library.component"; +import { ErrorStateMatcher } from "@angular/material/core"; +import { Resource, ResourceType } from "@shared/models/resource.models"; +import { ResourceService } from "@core/http/resource.service"; + +export interface ResourcesDialogData { + resources?: Resource; + isAdd?: boolean; +} + +@Component({ + selector: 'tb-resources-dialog', + templateUrl: './resources-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: ResourcesDialogComponent}], + styleUrls: ['./resources-dialog.component.scss'] +}) +export class ResourcesDialogComponent extends DialogComponent implements ErrorStateMatcher, AfterViewInit { + + readonly entityType = EntityType; + + ResourceType = ResourceType; + + isAdd = false; + + submitted = false; + + resources: Resource; + + @ViewChild('resourcesComponent', {static: true}) resourcesComponent: ResourcesLibraryComponent; + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: ResourcesDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + private resourceService: ResourceService) { + super(store, router, dialogRef); + + if (this.data.isAdd) { + this.isAdd = true; + } + + if (this.data.resources) { + this.resources = this.data.resources; + } + } + + ngAfterViewInit(): void { + if (this.isAdd) { + setTimeout(() => { + this.resourcesComponent.entityForm.markAsDirty(); + }, 0); + } + } + + isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + if (this.resourcesComponent.entityForm.valid) { + const resource = {...this.resourcesComponent.entityFormValue()}; + if (Array.isArray(resource.data)) { + const resources = []; + resource.data.forEach((data, index) => { + resources.push({ + resourceType: resource.resourceType, + data, + fileName: resource.fileName[index], + title: resource.title + }); + }); + this.resourceService.saveResources(resources, {resendRequest: true}).pipe( + map((response) => response[0]) + ).subscribe(result => this.dialogRef.close(result)); + } else { + this.resourceService.saveResource(resource).subscribe(result => this.dialogRef.close(result)); + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html similarity index 98% rename from ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html rename to ui-ngx/src/app/modules/home/components/resources/resources-library.component.html index ebb946ccbc..c602906be1 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html +++ b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts index bfba25afa1..a4d973e998 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts @@ -24,6 +24,8 @@ import { AiModel, AiRuleNodeResponseFormatTypeOnlyText, ResponseFormat } from '@ import { deepTrim } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; import { jsonRequired } from '@shared/components/json-object-edit.component'; +import { Resource, ResourceType } from "@shared/models/resource.models"; +import { ResourcesDialogComponent, ResourcesDialogData } from "@home/components/resources/resources-dialog.component"; @Component({ selector: 'tb-external-node-ai-config', @@ -38,6 +40,9 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { responseFormat = ResponseFormat; + EntityType = EntityType; + ResourceType = ResourceType; + constructor(private fb: UntypedFormBuilder, private translate: TranslateService, private dialog: MatDialog) { @@ -53,6 +58,7 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { modelId: [configuration?.modelId ?? null, [Validators.required]], systemPrompt: [configuration?.systemPrompt ?? '', [Validators.maxLength(500_000), Validators.pattern(/.*\S.*/)]], userPrompt: [configuration?.userPrompt ?? '', [Validators.required, Validators.maxLength(500_000), Validators.pattern(/.*\S.*/)]], + resourceIds: [configuration?.resourceIds ?? []], responseFormat: this.fb.group({ type: [configuration?.responseFormat?.type ?? ResponseFormat.JSON, []], schema: [configuration?.responseFormat?.schema ?? null, [jsonRequired]], @@ -116,5 +122,23 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { this.aiConfigForm.get(formControl).markAsDirty(); } }); + }; + + createAiResources(name: string, formControl: string) { + this.dialog.open(ResourcesDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + resources: {title: name, resourceType: ResourceType.TEXT}, + isAdd: true + } + }).afterClosed() + .subscribe((resource) => { + if (resource) { + const resourceIds = [...(this.aiConfigForm.get(formControl).value || []), resource.id.id]; + this.aiConfigForm.get(formControl).patchValue(resourceIds); + this.aiConfigForm.get(formControl).markAsDirty(); + } + }); } } diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts index 10721ade5a..60790edd74 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts @@ -26,7 +26,6 @@ import { HomeComponentsModule } from '@modules/home/components/home-components.m import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component'; import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component'; import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component'; -import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-library.component'; import { ResourceTabsComponent } from '@home/pages/admin/resource/resource-tabs.component'; import { ResourcesTableHeaderComponent } from '@home/pages/admin/resource/resources-table-header.component'; import { QueueComponent } from '@home/pages/admin/queue/queue.component'; @@ -49,7 +48,6 @@ import { ResourceLibraryTabsComponent } from '@home/pages/admin/resource/resourc SendTestSmsDialogComponent, SecuritySettingsComponent, HomeSettingsComponent, - ResourcesLibraryComponent, ResourceTabsComponent, ResourceLibraryTabsComponent, ResourcesTableHeaderComponent, diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts index f39ed9ff7b..dc85ca4914 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts @@ -32,7 +32,7 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { Authority } from '@shared/models/authority.enum'; -import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-library.component'; +import { ResourcesLibraryComponent } from '@home/components/resources/resources-library.component'; import { PageLink } from '@shared/models/page/page-link'; import { EntityAction } from '@home/models/entity/entity-component.models'; import { map } from 'rxjs/operators'; diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts index 80c760c6ca..d4b5d18493 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts @@ -28,7 +28,7 @@ import { PageLink } from '@shared/models/page/page-link'; }) export class ResourcesTableHeaderComponent extends EntityTableHeaderComponent { - readonly resourceTypes = [ResourceType.LWM2M_MODEL, ResourceType.PKCS_12, ResourceType.JKS]; + readonly resourceTypes = [ResourceType.LWM2M_MODEL, ResourceType.PKCS_12, ResourceType.JKS, ResourceType.TEXT]; readonly resourceTypesTranslationMap = ResourceTypeTranslationMap; constructor(protected store: Store) { diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.html b/ui-ngx/src/app/shared/components/entity/entity-list.component.html index 6bf7cdb78d..e0e7dfe3f7 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.html @@ -40,6 +40,12 @@ [matAutocompleteConnectedTo]="origin" [matAutocomplete]="entityAutocomplete" [matChipInputFor]="chipList"> + {{ 'entity.no-entities-matching' | translate: {entity: searchText} }} + @if (allowCreateNew) { + + entity.create-new-key + + }
diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts index 552c4f1f71..9d1de180e9 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts @@ -14,7 +14,18 @@ /// limitations under the License. /// -import { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; +import { + Component, + ElementRef, + EventEmitter, + forwardRef, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, + ViewChild +} from '@angular/core'; import { ControlValueAccessor, NG_VALIDATORS, @@ -93,6 +104,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, OnChan } @Input() + @coerceBoolean() disabled: boolean; @Input() @@ -109,6 +121,13 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, OnChan @coerceBoolean() inlineField: boolean; + @Input() + @coerceBoolean() + allowCreateNew: boolean; + + @Output() + createNew = new EventEmitter(); + @ViewChild('entityInput') entityInput: ElementRef; @ViewChild('entityAutocomplete') matAutocomplete: MatAutocomplete; @ViewChild('chipList', {static: true}) chipList: MatChipGrid; @@ -136,6 +155,11 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, OnChan this.entityListFormGroup.get('entities').updateValueAndValidity(); } + createNewEntity($event: Event, searchText?: string) { + $event.stopPropagation(); + this.createNew.emit(searchText); + } + registerOnChange(fn: any): void { this.propagateChange = fn; } @@ -201,6 +225,9 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, OnChan this.modelValue = null; } this.dirty = true; + if (this.entityInput) { + this.entityInput.nativeElement.value = ''; + } } validate(): ValidationErrors | null { diff --git a/ui-ngx/src/app/shared/models/resource.models.ts b/ui-ngx/src/app/shared/models/resource.models.ts index 0419e40a7c..3495bf9eac 100644 --- a/ui-ngx/src/app/shared/models/resource.models.ts +++ b/ui-ngx/src/app/shared/models/resource.models.ts @@ -24,7 +24,8 @@ export enum ResourceType { LWM2M_MODEL = 'LWM2M_MODEL', PKCS_12 = 'PKCS_12', JKS = 'JKS', - JS_MODULE = 'JS_MODULE' + JS_MODULE = 'JS_MODULE', + TEXT = 'TEXT', } export enum ResourceSubType { @@ -57,7 +58,8 @@ export const ResourceTypeTranslationMap = new Map( [ResourceType.LWM2M_MODEL, 'resource.type.lwm2m-model'], [ResourceType.PKCS_12, 'resource.type.pkcs-12'], [ResourceType.JKS, 'resource.type.jks'], - [ResourceType.JS_MODULE, 'resource.type.js-module'] + [ResourceType.JS_MODULE, 'resource.type.js-module'], + [ResourceType.TEXT, 'resource.type.text'], ] ); @@ -76,8 +78,8 @@ export interface TbResourceInfo extends Omit, 'name' | title?: string; resourceType: ResourceType; resourceSubType?: ResourceSubType; - fileName: string; - public: boolean; + fileName?: string; + public?: boolean; publicResourceKey?: string; readonly link?: string; readonly publicLink?: string; @@ -87,7 +89,7 @@ export interface TbResourceInfo extends Omit, 'name' | export type ResourceInfo = TbResourceInfo; export interface Resource extends ResourceInfo { - data: string; + data?: string; name?: string; } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 80328181f6..d4bfd4ea06 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -4488,7 +4488,8 @@ "jks": "JKS", "js-module": "JS module", "lwm2m-model": "LWM2M model", - "pkcs-12": "PKCS #12" + "pkcs-12": "PKCS #12", + "text": "Text" }, "resource-sub-type": "Sub-type", "sub-type": { @@ -5467,7 +5468,8 @@ "timeout-required": "Timeout is required", "timeout-validation": "Must be from 1 second to 10 minutes.", "force-acknowledgement": "Force acknowledgement", - "force-acknowledgement-hint": "If enabled, the incoming message is acknowledged immediately. The model's response is then enqueued as a separate, new message." + "force-acknowledgement-hint": "If enabled, the incoming message is acknowledged immediately. The model's response is then enqueued as a separate, new message.", + "ai-resources": "AI resources" } }, "timezone": { From cd6063fd097e8c2db83b87e392011ad34866065c Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 10 Sep 2025 09:41:47 +0300 Subject: [PATCH 163/216] UI: General resources --- ui-ngx/src/app/core/http/entity.service.ts | 6 +- ui-ngx/src/app/core/http/resource.service.ts | 14 +++-- .../resources/resources-dialog.component.html | 4 +- .../resources/resources-dialog.component.ts | 3 + .../resources-library.component.html | 61 ++++++++++--------- .../resources/resources-library.component.ts | 13 +++- .../external/ai-config.component.html | 2 +- .../rule-node/external/ai-config.component.ts | 2 +- .../resources-table-header.component.ts | 2 +- .../shared/components/file-input.component.ts | 14 ++++- .../src/app/shared/models/resource.models.ts | 4 +- .../assets/locale/locale.constant-en_US.json | 2 +- 12 files changed, 77 insertions(+), 50 deletions(-) diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 53a55bfe7f..c652ba39ba 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -299,9 +299,7 @@ export class EntityService { entityIds); break; case EntityType.TB_RESOURCE: - observable = this.getEntitiesByIdsObservable( - (id) => this.resourceService.getResource(id, config), - entityIds); + observable = this.resourceService.getResourcesByIds(entityIds, config); break; } return observable; @@ -478,7 +476,7 @@ export class EntityService { break; case EntityType.TB_RESOURCE: pageLink.sortOrder.property = 'title'; - entitiesObservable = this.resourceService.getTenantResources(pageLink, subType as ResourceType, config); + entitiesObservable = this.resourceService.getResources(pageLink, subType as ResourceType, null, config); break; case EntityType.QUEUE_STATS: pageLink.sortOrder.property = 'createdTime'; diff --git a/ui-ngx/src/app/core/http/resource.service.ts b/ui-ngx/src/app/core/http/resource.service.ts index 52c92d96e4..90335758dd 100644 --- a/ui-ngx/src/app/core/http/resource.service.ts +++ b/ui-ngx/src/app/core/http/resource.service.ts @@ -24,6 +24,7 @@ import { Resource, ResourceInfo, ResourceSubType, ResourceType, TBResourceScope import { catchError, mergeMap } from 'rxjs/operators'; import { isNotEmptyStr } from '@core/utils'; import { ResourcesService } from '@core/services/resources.service'; +import { NotificationTarget } from "@shared/models/notification.models"; @Injectable({ providedIn: 'root' @@ -47,12 +48,8 @@ export class ResourceService { return this.http.get>(url, defaultHttpOptionsFromConfig(config)); } - public getTenantResources(pageLink: PageLink, resourceType?: ResourceType, config?: RequestConfig): Observable> { - let url = `/api/resource${pageLink.toQuery()}`; - if (isNotEmptyStr(resourceType)) { - url += `&resourceType=${resourceType}`; - } - return this.http.get>(url, defaultHttpOptionsFromConfig(config)); + public getTenantResources(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/resource/tenant${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)) } public getResource(resourceId: string, config?: RequestConfig): Observable { @@ -98,4 +95,9 @@ export class ResourceService { return this.http.delete(`/api/resource/${resourceId}?force=${force}`, defaultHttpOptionsFromConfig(config)); } + public getResourcesByIds(ids: string[], config?: RequestConfig): Observable> { + return this.http.get>(`/api/resource?resourceIds=${ids.join(',')}`, + defaultHttpOptionsFromConfig(config)); + } + } diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html index c6813ff0f2..0062cfa9ac 100644 --- a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html @@ -32,8 +32,8 @@ diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts index a06c72827a..6216f087b1 100644 --- a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts @@ -106,6 +106,9 @@ export class ResourcesDialogComponent extends DialogComponent response[0]) ).subscribe(result => this.dialogRef.close(result)); } else { + if (resource.resourceType !== ResourceType.GENERAL) { + delete resource.descriptor; + } this.resourceService.saveResource(resource).subscribe(result => this.dialogRef.close(result)); } } diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html index c602906be1..4737b75ef2 100644 --- a/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html +++ b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html @@ -48,14 +48,16 @@
- - resource.resource-type - - - {{ resourceTypesTranslationMap.get(resourceType) | translate }} - - - + @if (resourceTypes.length > 1) { + + resource.resource-type + + + {{ resourceTypesTranslationMap.get(resourceType) | translate }} + + + + } resource.title @@ -66,26 +68,29 @@ {{ 'resource.title-max-length' | translate }} - - -
- - resource.file-name - - -
+ @if (isAdd || ((isAdd || isEdit) && entityForm.get('resourceType').value === resourceType.GENERAL)) { + + + } @else { +
+ + resource.file-name + + +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts index 0b50e45a7a..e3ad0e15f3 100644 --- a/ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts +++ b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts @@ -44,7 +44,7 @@ export class ResourcesLibraryComponent extends EntityComponent impleme standalone = false; @Input() - resourceTypes = [ResourceType.LWM2M_MODEL, ResourceType.PKCS_12, ResourceType.JKS, ResourceType.TEXT]; + resourceTypes = [ResourceType.LWM2M_MODEL, ResourceType.PKCS_12, ResourceType.JKS, ResourceType.GENERAL]; @Input() defaultResourceType = ResourceType.LWM2M_MODEL; @@ -90,10 +90,19 @@ export class ResourcesLibraryComponent extends EntityComponent impleme title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]], resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL, Validators.required], fileName: [entity ? entity.fileName : null, Validators.required], - data: [entity ? entity.data : null, this.isAdd ? [Validators.required] : []] + data: [entity ? entity.data : null, this.isAdd ? [Validators.required] : []], + descriptor: this.fb.group({ + mediaType: [''] + }) }); } + mediaTypeChange(mediaType: string): void { + if (this.entityForm.get('resourceType').value === ResourceType.GENERAL) { + this.entityForm.get('descriptor').get('mediaType').patchValue(mediaType); + } + } + updateForm(entity: Resource): void { this.entityForm.patchValue(entity); } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html index d259d57ef3..5381fc770b 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html @@ -76,7 +76,7 @@ placeholderText="{{ 'rule-node-config.ai.ai-resources' | translate }}" [inlineField]="true" [entityType]="EntityType.TB_RESOURCE" - [subType]="ResourceType.TEXT" + [subType]="ResourceType.GENERAL" (createNew)="createAiResources($event, 'resourceIds')" formControlName="resourceIds"> diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts index a4d973e998..07bcb86fdf 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts @@ -129,7 +129,7 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { - resources: {title: name, resourceType: ResourceType.TEXT}, + resources: {title: name, resourceType: ResourceType.GENERAL}, isAdd: true } }).afterClosed() diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts index d4b5d18493..136c143e3c 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts @@ -28,7 +28,7 @@ import { PageLink } from '@shared/models/page/page-link'; }) export class ResourcesTableHeaderComponent extends EntityTableHeaderComponent { - readonly resourceTypes = [ResourceType.LWM2M_MODEL, ResourceType.PKCS_12, ResourceType.JKS, ResourceType.TEXT]; + readonly resourceTypes = [ResourceType.LWM2M_MODEL, ResourceType.PKCS_12, ResourceType.JKS, ResourceType.GENERAL]; readonly resourceTypesTranslationMap = ResourceTypeTranslationMap; constructor(protected store: Store) { diff --git a/ui-ngx/src/app/shared/components/file-input.component.ts b/ui-ngx/src/app/shared/components/file-input.component.ts index bbe68bb9c6..6960db73dd 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.ts +++ b/ui-ngx/src/app/shared/components/file-input.component.ts @@ -129,10 +129,15 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, @Output() fileNameChanged = new EventEmitter(); + @Output() + mediaTypeChanged = new EventEmitter(); + fileName: string | string[]; fileContent: any; files: File[]; + mediaType: string; + @ViewChild('flow', {static: true}) flow: FlowDirective; @@ -180,6 +185,7 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, this.fileContent = files[0].fileContent; this.fileName = files[0].fileName; this.files = files[0].files; + this.mediaType = files[0].mediaType; this.updateModel(); } else if (files.length > 1) { this.fileContent = files.map(content => content.fileContent); @@ -203,6 +209,7 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, let fileName = null; let fileContent = null; let files = null; + let mediaType = null; if (reader.readyState === reader.DONE) { if (!this.workFromFileObj) { fileContent = reader.result; @@ -211,16 +218,18 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, fileContent = this.contentConvertFunction(fileContent); } fileName = fileContent ? file.name : null; + mediaType = file?.file?.type || null; } } else if (file.name || file.file){ files = file.file; fileName = file.name; + mediaType = file.file.type || null; } } - resolve({fileContent, fileName, files}); + resolve({fileContent, fileName, files, mediaType}); }; reader.onerror = () => { - resolve({fileContent: null, fileName: null, files: null}); + resolve({fileContent: null, fileName: null, files: null, mediaType: null}); }; if (this.readAsBinary) { reader.readAsBinaryString(file.file); @@ -283,6 +292,7 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, this.propagateChange(this.files); } else { this.propagateChange(this.fileContent); + this.mediaTypeChanged.emit(this.mediaType); this.fileNameChanged.emit(this.fileName); } } diff --git a/ui-ngx/src/app/shared/models/resource.models.ts b/ui-ngx/src/app/shared/models/resource.models.ts index 3495bf9eac..12590e7608 100644 --- a/ui-ngx/src/app/shared/models/resource.models.ts +++ b/ui-ngx/src/app/shared/models/resource.models.ts @@ -25,7 +25,7 @@ export enum ResourceType { PKCS_12 = 'PKCS_12', JKS = 'JKS', JS_MODULE = 'JS_MODULE', - TEXT = 'TEXT', + GENERAL = 'GENERAL', } export enum ResourceSubType { @@ -59,7 +59,7 @@ export const ResourceTypeTranslationMap = new Map( [ResourceType.PKCS_12, 'resource.type.pkcs-12'], [ResourceType.JKS, 'resource.type.jks'], [ResourceType.JS_MODULE, 'resource.type.js-module'], - [ResourceType.TEXT, 'resource.type.text'], + [ResourceType.GENERAL, 'resource.type.general'], ] ); diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index d4bfd4ea06..d79bb81ae5 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -4489,7 +4489,7 @@ "js-module": "JS module", "lwm2m-model": "LWM2M model", "pkcs-12": "PKCS #12", - "text": "Text" + "general": "General" }, "resource-sub-type": "Sub-type", "sub-type": { From caf90636382db5418dd59703c52ceed0a0aaf0e5 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Fri, 12 Sep 2025 16:23:53 +0300 Subject: [PATCH 164/216] UI: Add resources in use dialog with force to delete --- .../resources-library-table-config.resolve.ts | 180 +++++++++++++++++- .../assets/locale/locale.constant-en_US.json | 7 +- 2 files changed, 177 insertions(+), 10 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts index dc85ca4914..d92355fbac 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts @@ -22,7 +22,13 @@ import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { Router } from '@angular/router'; -import { Resource, ResourceInfo, ResourceType, ResourceTypeTranslationMap } from '@shared/models/resource.models'; +import { + Resource, + ResourceInfo, ResourceInfoWithReferences, + ResourceType, + ResourceTypeTranslationMap, + toResourceDeleteResult +} from '@shared/models/resource.models'; import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { DatePipe } from '@angular/common'; @@ -35,9 +41,19 @@ import { Authority } from '@shared/models/authority.enum'; import { ResourcesLibraryComponent } from '@home/components/resources/resources-library.component'; import { PageLink } from '@shared/models/page/page-link'; import { EntityAction } from '@home/models/entity/entity-component.models'; -import { map } from 'rxjs/operators'; +import { catchError, map } from 'rxjs/operators'; import { ResourcesTableHeaderComponent } from '@home/pages/admin/resource/resources-table-header.component'; import { ResourceLibraryTabsComponent } from '@home/pages/admin/resource/resource-library-tabs.component'; +import { forkJoin, of } from "rxjs"; +import { + ResourcesInUseDialogComponent, + ResourcesInUseDialogData +} from "@shared/components/resource/resources-in-use-dialog.component"; +import { parseHttpErrorMessage } from "@core/utils"; +import { ActionNotificationShow } from "@core/notification/notification.actions"; +import { ResourcesDatasource } from "@home/pages/admin/resource/resources-datasource"; +import { MatDialog } from "@angular/material/dialog"; +import { DialogService } from "@core/services/dialog.service"; @Injectable() export class ResourcesLibraryTableConfigResolver { @@ -49,6 +65,8 @@ export class ResourcesLibraryTableConfigResolver { private resourceService: ResourceService, private translate: TranslateService, private router: Router, + private dialog: MatDialog, + private dialogService: DialogService, private datePipe: DatePipe) { this.config.entityType = EntityType.TB_RESOURCE; @@ -76,19 +94,27 @@ export class ResourcesLibraryTableConfigResolver { icon: 'file_download', isEnabled: () => true, onAction: ($event, entity) => this.downloadResource($event, entity) - } + }, + { + name: this.translate.instant('resource.delete'), + icon: 'delete', + isEnabled: (resource) => this.config.deleteEnabled(resource), + onAction: ($event, entity) => this.deleteResource($event, entity) + }, ); - this.config.deleteEntityTitle = resource => this.translate.instant('resource.delete-resource-title', - { resourceTitle: resource.title }); - this.config.deleteEntityContent = () => this.translate.instant('resource.delete-resource-text'); - this.config.deleteEntitiesTitle = count => this.translate.instant('resource.delete-resources-title', {count}); - this.config.deleteEntitiesContent = () => this.translate.instant('resource.delete-resources-text'); + this.config.groupActionDescriptors = [{ + name: this.translate.instant('action.delete'), + icon: 'delete', + isEnabled: true, + onAction: ($event, entities) => this.deleteResources($event, entities) + }]; + + this.config.entitiesDeleteEnabled = false; this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink, this.config.componentsData.resourceType); this.config.loadEntity = id => this.resourceService.getResourceInfoById(id.id); this.config.saveEntity = resource => this.saveResource(resource); - this.config.deleteEntity = id => this.resourceService.deleteResource(id.id); this.config.onEntityAction = action => this.onResourceAction(action); } @@ -147,6 +173,8 @@ export class ResourcesLibraryTableConfigResolver { case 'downloadResource': this.downloadResource(action.event, action.entity); return true; + case 'deleteLibrary': + this.deleteResource(action.event, action.entity); } return false; } @@ -165,4 +193,138 @@ export class ResourcesLibraryTableConfigResolver { return authority === Authority.SYS_ADMIN; } } + + private deleteResource($event: Event, resource: ResourceInfo) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('resource.delete-resource-title', { resourceTitle: resource.title }), + this.translate.instant('resource.delete-resource-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((result) => { + if (result) { + this.resourceService.deleteResource(resource.id.id, false, {ignoreErrors: true}).pipe( + map(() => toResourceDeleteResult(resource)), + catchError((err) => of(toResourceDeleteResult(resource, err))) + ).subscribe( + (deleteResult) => { + if (deleteResult.success) { + if (this.config.getEntityDetailsPage()) { + this.config.getEntityDetailsPage().goBack(); + } else { + this.config.updateData(true); + } + } else if (deleteResult.resourceIsReferencedError) { + const resources: ResourceInfoWithReferences[] = [{...resource, ...{references: deleteResult.references}}]; + const data = { + multiple: false, + resources, + configuration: { + title: 'resource.resource-is-in-use', + message: this.translate.instant('resource.resource-is-in-use-text', {title: resources[0].title}), + deleteText: 'resource.delete-resource-in-use-text', + selectedText: 'resource.selected-resources', + columns: ['select', 'title', 'references'] + } + }; + this.dialog.open(ResourcesInUseDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data + }).afterClosed().subscribe((resources) => { + if (resources) { + this.resourceService.deleteResource(resource.id.id, true).subscribe(() => { + if (this.config.getEntityDetailsPage()) { + this.config.getEntityDetailsPage().goBack(); + } else { + this.config.updateData(true); + } + }); + } + }); + } else { + const errorMessageWithTimeout = parseHttpErrorMessage(deleteResult.error, this.translate); + setTimeout(() => { + this.store.dispatch(new ActionNotificationShow({message: errorMessageWithTimeout.message, type: 'error'})); + }, errorMessageWithTimeout.timeout); + } + } + ); + } + }); + } + + private deleteResources($event: Event, resources: ResourceInfo[]) { + if ($event) { + $event.stopPropagation(); + } + if (resources && resources.length) { + const title = this.translate.instant('resource.delete-resources-title', {count: resources.length}); + const content = this.translate.instant('resource.delete-resources-text'); + this.dialogService.confirm(title, content, + this.translate.instant('action.no'), + this.translate.instant('action.yes')).subscribe((result) => { + if (result) { + const tasks = resources.map((resource) => + this.resourceService.deleteResource(resource.id.id, false, {ignoreErrors: true}).pipe( + map(() => toResourceDeleteResult(resource)), + catchError((err) => of(toResourceDeleteResult(resource, err))) + ) + ); + forkJoin(tasks).subscribe( + (deleteResults) => { + const anySuccess = deleteResults.some(res => res.success); + const referenceErrors = deleteResults.filter(res => res.resourceIsReferencedError); + const otherError = deleteResults.find(res => !res.success); + if (anySuccess) { + this.config.updateData(); + } + if (referenceErrors?.length) { + const resourcesWithReferences: ResourceInfoWithReferences[] = + referenceErrors.map(ref => ({...ref.resource, ...{references: ref.references}})); + const data = { + multiple: true, + resources: resourcesWithReferences, + configuration: { + title: 'resource.resources-are-in-use', + message: this.translate.instant('resource.resources-are-in-use-text'), + deleteText: 'resource.delete-resource-in-use-text', + selectedText: 'resource.selected-resources', + datasource: new ResourcesDatasource(this.resourceService, resourcesWithReferences, () => true), + columns: ['select', 'title', 'references'] + } + }; + this.dialog.open(ResourcesInUseDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data + }).afterClosed().subscribe((forceDeleteResources) => { + if (forceDeleteResources && forceDeleteResources.length) { + const forceDeleteTasks = forceDeleteResources.map((resource) => + this.resourceService.deleteResource(resource.id.id, true) + ); + forkJoin(forceDeleteTasks).subscribe( + () => { + this.config.updateData(); + } + ); + } + }); + } else if (otherError) { + const errorMessageWithTimeout = parseHttpErrorMessage(otherError.error, this.translate); + setTimeout(() => { + this.store.dispatch(new ActionNotificationShow({message: errorMessageWithTimeout.message, type: 'error'})); + }, errorMessageWithTimeout.timeout); + } + } + ); + } + }); + } + } } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index d79bb81ae5..4639552658 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -4497,7 +4497,12 @@ "scada-symbol": "Scada symbol", "extension": "Extension", "module": "Module" - } + }, + "resource-is-in-use": "Resource is used by other entities", + "resources-are-in-use": "Resources are used by other entities", + "resource-is-in-use-text": "The Resource '{{title}}' was not deleted because it is used by the following entities:", + "resources-are-in-use-text": "Not all Resources have been deleted because they are used by other entities.
You can view referenced entities by clicking the References button in the corresponding resource row.
If you still want to delete these resources, select them in the table below and click the Delete selected button.", + "delete-resource-in-use-text": "If you still want to delete the resource, click the Delete anyway button." }, "javascript": { "add": "Add JavaScript resource", From af600ff450925466baa86461f33e98065d82af9b Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Fri, 12 Sep 2025 16:46:38 +0300 Subject: [PATCH 165/216] UI: oprimize import --- ui-ngx/src/app/core/http/resource.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ui-ngx/src/app/core/http/resource.service.ts b/ui-ngx/src/app/core/http/resource.service.ts index 90335758dd..168c63b3b1 100644 --- a/ui-ngx/src/app/core/http/resource.service.ts +++ b/ui-ngx/src/app/core/http/resource.service.ts @@ -24,7 +24,6 @@ import { Resource, ResourceInfo, ResourceSubType, ResourceType, TBResourceScope import { catchError, mergeMap } from 'rxjs/operators'; import { isNotEmptyStr } from '@core/utils'; import { ResourcesService } from '@core/services/resources.service'; -import { NotificationTarget } from "@shared/models/notification.models"; @Injectable({ providedIn: 'root' From f9b52c3a43e33caf494972fb19f4749f36c8082e Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 29 Sep 2025 11:44:27 +0300 Subject: [PATCH 166/216] fixed missing ACK when message publishing fails --- .../AbstractGatewaySessionHandler.java | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java index 91e7dedbf2..af8ed2aa6d 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java @@ -412,7 +412,7 @@ public abstract class AbstractGatewaySessionHandler processPostTelemetryMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId), - t -> failedToProcessLog(deviceName, TELEMETRY, t)); + t -> processFailure(msgId, deviceName, TELEMETRY, t)); } } @@ -444,7 +444,7 @@ public abstract class AbstractGatewaySessionHandler { String deviceName = checkDeviceName(telemetryMsg.getDeviceName()); process(deviceName, deviceCtx -> processPostTelemetryMsg(deviceCtx, telemetryMsg.getMsg(), deviceName, msgId), - t -> failedToProcessLog(deviceName, TELEMETRY, t)); + t -> processFailure(msgId, deviceName, TELEMETRY, t)); }); } catch (RuntimeException | InvalidProtocolBufferException e) { throw new AdaptorException(e); @@ -483,7 +483,7 @@ public abstract class AbstractGatewaySessionHandler processClaimDeviceMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId), - t -> failedToProcessLog(deviceName, CLAIMING, t)); + t -> processFailure(msgId, deviceName, CLAIMING, t)); } } @@ -510,7 +510,7 @@ public abstract class AbstractGatewaySessionHandler { String deviceName = checkDeviceName(claimDeviceMsg.getDeviceName()); process(deviceName, deviceCtx -> processClaimDeviceMsg(deviceCtx, claimDeviceMsg.getClaimRequest(), deviceName, msgId), - t -> failedToProcessLog(deviceName, CLAIMING, t)); + t -> processFailure(msgId, deviceName, CLAIMING, t)); }); } catch (RuntimeException | InvalidProtocolBufferException e) { throw new AdaptorException(e); @@ -539,7 +539,7 @@ public abstract class AbstractGatewaySessionHandler processPostAttributesMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId), - t -> failedToProcessLog(deviceName, ATTRIBUTE, t)); + t -> processFailure(msgId, deviceName, ATTRIBUTE, t)); } } @@ -565,7 +565,7 @@ public abstract class AbstractGatewaySessionHandler { String deviceName = checkDeviceName(attributesMsg.getDeviceName()); process(deviceName, deviceCtx -> processPostAttributesMsg(deviceCtx, attributesMsg.getMsg(), deviceName, msgId), - t -> failedToProcessLog(deviceName, ATTRIBUTE, t)); + t -> processFailure(msgId, deviceName, ATTRIBUTE, t)); }); } catch (RuntimeException | InvalidProtocolBufferException e) { throw new AdaptorException(e); @@ -648,7 +648,7 @@ public abstract class AbstractGatewaySessionHandler processRpcResponseMsg(deviceCtx, requestId, data, deviceName, msgId), - t -> failedToProcessLog(deviceName, RPC_RESPONSE, t)); + t -> processFailure(msgId, deviceName, RPC_RESPONSE, t)); } private void processRpcResponseMsg(MqttDeviceAwareSessionContext deviceCtx, Integer requestId, String data, String deviceName, int msgId) { @@ -661,8 +661,7 @@ public abstract class AbstractGatewaySessionHandler processGetAttributeRequestMessage(deviceCtx, requestMsg, deviceName, msgId), t -> { - failedToProcessLog(deviceName, ATTRIBUTES_REQUEST, t); - ack(mqttMsg, MqttReasonCodes.PubAck.IMPLEMENTATION_SPECIFIC_ERROR); + processFailure(msgId, deviceName, ATTRIBUTES_REQUEST, t, MqttReasonCodes.PubAck.IMPLEMENTATION_SPECIFIC_ERROR); }); } @@ -790,10 +789,19 @@ public abstract class AbstractGatewaySessionHandler Date: Mon, 29 Sep 2025 13:35:22 +0300 Subject: [PATCH 167/216] AI models: add base URL to OpenAI --- .../Langchain4jChatModelConfigurerImpl.java | 1 + .../controller/AiModelControllerTest.java | 12 ++++++-- .../ai/DefaultTbAiModelServiceTest.java | 2 +- .../sql/BaseTbResourceServiceTest.java | 6 +++- .../ai/provider/OpenAiProviderConfig.java | 30 +++++++++++++++++-- .../rule/engine/ai/TbAiNodeTest.java | 5 +++- 6 files changed, 47 insertions(+), 9 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java index 2cb6c2097f..c631f1a3d0 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -70,6 +70,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur @Override public ChatModel configureChatModel(OpenAiChatModelConfig chatModelConfig) { return OpenAiChatModel.builder() + .baseUrl(chatModelConfig.providerConfig().baseUrl()) .apiKey(chatModelConfig.providerConfig().apiKey()) .modelName(chatModelConfig.modelId()) .temperature(chatModelConfig.temperature()) diff --git a/application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java index ae2972b0cc..53648af441 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AiModelControllerTest.java @@ -104,7 +104,10 @@ public class AiModelControllerTest extends AbstractControllerTest { var model = doPost("/api/ai/model", constructValidOpenAiModel("Test model"), AiModel.class); var newModelConfig = OpenAiChatModelConfig.builder() - .providerConfig(new OpenAiProviderConfig("test-api-key-updated")) + .providerConfig(OpenAiProviderConfig.builder() + .baseUrl(OpenAiProviderConfig.OPENAI_OFFICIAL_BASE_URL) + .apiKey("test-api-key-updated") + .build()) .modelId("o4-mini") .temperature(0.2) .topP(0.4) @@ -270,7 +273,7 @@ public class AiModelControllerTest extends AbstractControllerTest { .tenantId(tenantId) .name("Test model 1") .configuration(OpenAiChatModelConfig.builder() - .providerConfig(new OpenAiProviderConfig("test-api-key")) + .providerConfig(OpenAiProviderConfig.builder().apiKey("test-api-key").build()) .modelId("o3-pro") .build()) .build(), AiModel.class); @@ -594,7 +597,10 @@ public class AiModelControllerTest extends AbstractControllerTest { private AiModel constructValidOpenAiModel(String name) { var modelConfig = OpenAiChatModelConfig.builder() - .providerConfig(new OpenAiProviderConfig("test-api-key")) + .providerConfig(OpenAiProviderConfig.builder() + .baseUrl(OpenAiProviderConfig.OPENAI_OFFICIAL_BASE_URL) + .apiKey("test-api-key") + .build()) .modelId("gpt-4o") .temperature(0.5) .topP(0.3) diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelServiceTest.java index 2321446b44..b6be136f82 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/ai/DefaultTbAiModelServiceTest.java @@ -253,7 +253,7 @@ class DefaultTbAiModelServiceTest { private static AiModelConfig constructValidOpenAiModelConfig() { return OpenAiChatModelConfig.builder() - .providerConfig(new OpenAiProviderConfig("test-api-key")) + .providerConfig(OpenAiProviderConfig.builder().apiKey("test-api-key").build()) .modelId("gpt-4o") .temperature(0.5) .topP(0.3) diff --git a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java index da20b9489c..a146730465 100644 --- a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java @@ -682,7 +682,10 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { private AiModel constructValidOpenAiModel(String name) { var modelConfig = OpenAiChatModelConfig.builder() - .providerConfig(new OpenAiProviderConfig("test-api-key")) + .providerConfig(OpenAiProviderConfig.builder() + .baseUrl(OpenAiProviderConfig.OPENAI_OFFICIAL_BASE_URL) + .apiKey("test-api-key") + .build()) .modelId("gpt-4o") .temperature(0.5) .topP(0.3) @@ -699,6 +702,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { .configuration(modelConfig) .build(); } + @Test public void testFindTenantResourcesByTenantId() throws Exception { loginSysAdmin(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java index 09ffda837b..f7864db6e3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OpenAiProviderConfig.java @@ -15,8 +15,32 @@ */ package org.thingsboard.server.common.data.ai.provider; -import jakarta.validation.constraints.NotNull; +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.constraints.AssertTrue; +import lombok.Builder; +import org.apache.commons.lang3.StringUtils; +import java.util.Objects; + +@Builder public record OpenAiProviderConfig( - @NotNull String apiKey -) implements AiProviderConfig {} + String baseUrl, + String apiKey +) implements AiProviderConfig { + + public static final String OPENAI_OFFICIAL_BASE_URL = "https://api.openai.com/v1"; + + public OpenAiProviderConfig { + baseUrl = Objects.requireNonNullElse(baseUrl, OPENAI_OFFICIAL_BASE_URL); + } + + @JsonIgnore + @AssertTrue(message = "API key is required when using the official OpenAI API") + public boolean isValid() { + if (baseUrl.equals(OPENAI_OFFICIAL_BASE_URL)) { + return StringUtils.isNotBlank(apiKey); + } + return true; + } + +} diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java index c5b7f2c44b..a786cdd536 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java @@ -129,7 +129,10 @@ class TbAiNodeTest { config = new TbAiNodeConfiguration(); modelConfig = OpenAiChatModelConfig.builder() - .providerConfig(new OpenAiProviderConfig("test-api-key")) + .providerConfig(OpenAiProviderConfig.builder() + .baseUrl(OpenAiProviderConfig.OPENAI_OFFICIAL_BASE_URL) + .apiKey("test-api-key") + .build()) .modelId("gpt-4o") .temperature(0.5) .topP(0.3) From 5e9ae421bea3b510e09fedc0932e0046b1a0a763 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Mon, 29 Sep 2025 16:16:15 +0300 Subject: [PATCH 168/216] added tests --- ...AbstractMqttTimeseriesIntegrationTest.java | 39 ++++++++++++++++++- ...ractMqttTimeseriesJsonIntegrationTest.java | 11 ++++++ ...actMqttTimeseriesProtoIntegrationTest.java | 25 ++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java index 6543955ed5..9856aa18cb 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesIntegrationTest.java @@ -25,10 +25,10 @@ import org.springframework.boot.test.mock.mockito.SpyBean; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.msg.gateway.metrics.GatewayMetadata; import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; import org.thingsboard.server.transport.mqtt.gateway.GatewayMetricsService; -import org.thingsboard.server.common.msg.gateway.metrics.GatewayMetadata; import org.thingsboard.server.transport.mqtt.gateway.metrics.GatewayMetricsState; import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestCallback; import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient; @@ -43,6 +43,7 @@ import java.util.Random; import java.util.Set; import java.util.concurrent.TimeUnit; +import static org.awaitility.Awaitility.await; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.mockito.Mockito.any; @@ -112,6 +113,42 @@ public abstract class AbstractMqttTimeseriesIntegrationTest extends AbstractMqtt processGatewayTelemetryTest(GATEWAY_TELEMETRY_TOPIC, expectedKeys, payload.getBytes(), deviceName1, deviceName2); } + @Test + public void testAckIsReceivedOnFailedPublishMessage() throws Exception { + String devicePayload = "[{\"ts\": 10000, \"values\": " + PAYLOAD_VALUES_STR + "}]"; + String payloadA = "{\"Device A\": " + devicePayload + "}"; + + String deviceBPayload = "[{\"ts\": 10000, \"values\": " + PAYLOAD_VALUES_STR + "}]"; + String payloadB = "{\"Device B\": " + deviceBPayload + "}"; + + testAckIsReceivedOnFailedPublishMessage("Device A", payloadA.getBytes(), "Device B", payloadB.getBytes()); + } + + protected void testAckIsReceivedOnFailedPublishMessage(String deviceName1, byte[] payload1, String deviceName2, byte[] payload2) throws Exception { + updateDefaultTenantProfileConfig(profileConfiguration -> { + profileConfiguration.setMaxDevices(3); + }); + + MqttTestClient client = new MqttTestClient(); + client.connectAndWait(gatewayAccessToken); + client.publishAndWait(GATEWAY_TELEMETRY_TOPIC, payload1); + + // check device is created + await().atMost(TIMEOUT, TimeUnit.SECONDS).untilAsserted(() -> { + assertNotNull(doGet("/api/tenant/devices?deviceName=" + deviceName1, Device.class)); + }); + + client.publishAndWait(GATEWAY_TELEMETRY_TOPIC, payload2); + client.disconnectAndWait(); + + // check device was not created due to limit + doGet("/api/tenant/devices?deviceName=" + deviceName2).andExpect(status().isNotFound()); + + updateDefaultTenantProfileConfig(profileConfiguration -> { + profileConfiguration.setMaxDevices(0); + }); + } + @Test public void testGatewayConnect() throws Exception { String payload = "{\"device\":\"Device A\"}"; diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java index a23506a160..92b35d896c 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesJsonIntegrationTest.java @@ -186,4 +186,15 @@ public abstract class AbstractMqttTimeseriesJsonIntegrationTest extends Abstract assertFalse(callback.isPubAckReceived()); } + @Override + public void testAckIsReceivedOnFailedPublishMessage() throws Exception { + MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder() + .deviceName("Test Post Telemetry device json payload") + .gatewayName("Test Post Telemetry gateway json payload") + .transportPayloadType(TransportPayloadType.JSON) + .telemetryTopicFilter(POST_DATA_TELEMETRY_TOPIC) + .build(); + processBeforeTest(configProperties); + super.testAckIsReceivedOnFailedPublishMessage(); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java index 1b6c247d96..60e5857c04 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/telemetry/timeseries/AbstractMqttTimeseriesProtoIntegrationTest.java @@ -459,6 +459,31 @@ public abstract class AbstractMqttTimeseriesProtoIntegrationTest extends Abstrac assertFalse(callback.isPubAckReceived()); } + @Override + public void testAckIsReceivedOnFailedPublishMessage() throws Exception { + MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder() + .deviceName("Test Post Telemetry device proto payload") + .gatewayName("Test Post Telemetry gateway proto payload") + .transportPayloadType(TransportPayloadType.PROTOBUF) + .telemetryTopicFilter(POST_DATA_TELEMETRY_TOPIC) + .build(); + processBeforeTest(configProperties); + + TransportApiProtos.GatewayTelemetryMsg.Builder gatewayTelemetryMsgProtoBuilder = TransportApiProtos.GatewayTelemetryMsg.newBuilder(); + List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + String deviceName1 = "Device A"; + String deviceName2 = "Device B"; + TransportApiProtos.TelemetryMsg deviceATelemetryMsgProto = getDeviceTelemetryMsgProto(deviceName1, expectedKeys, 10000, 20000); + gatewayTelemetryMsgProtoBuilder.addAllMsg(List.of(deviceATelemetryMsgProto)); + TransportApiProtos.GatewayTelemetryMsg payload1 = gatewayTelemetryMsgProtoBuilder.build(); + + TransportApiProtos.TelemetryMsg deviceBTelemetryMsgProto = getDeviceTelemetryMsgProto(deviceName2, expectedKeys, 10000, 20000); + TransportApiProtos.GatewayTelemetryMsg payload2 = TransportApiProtos.GatewayTelemetryMsg.newBuilder() + .addAllMsg(List.of(deviceBTelemetryMsgProto)) + .build(); + super.testAckIsReceivedOnFailedPublishMessage(deviceName1, payload1.toByteArray(), deviceName2, payload2.toByteArray()); + } + private DynamicSchema getDynamicSchema() { DeviceProfileTransportConfiguration transportConfiguration = deviceProfile.getProfileData().getTransportConfiguration(); assertTrue(transportConfiguration instanceof MqttDeviceProfileTransportConfiguration); From 22dc482b9ee43672ad99138833929de1e5c9aa33 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Tue, 30 Sep 2025 09:47:30 +0300 Subject: [PATCH 169/216] UI: Add baseUrl for openAI provider --- .../ai-model/ai-model-dialog.component.html | 24 +++++++++--------- .../ai-model/ai-model-dialog.component.ts | 25 ++++++++++++++++++- .../src/app/shared/models/ai-model.models.ts | 2 +- .../assets/locale/locale.constant-en_US.json | 1 + 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html index abfe8500b4..5aa323fe7d 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html @@ -116,14 +116,24 @@
} + @if (providerFieldsList.includes('baseUrl')) { + + ai-models.baseurl + + + {{ 'ai-models.baseurl-required' | translate }} + + + } @if (providerFieldsList.includes('apiKey')) { ai-models.api-key - + - {{ 'ai-models.api-key-required' | translate }} + {{ ( provider === aiProvider.OPENAI ? 'ai-models.api-key-open-ai-required' : 'ai-models.api-key-required') | translate }} } @@ -158,16 +168,6 @@ } - @if (providerFieldsList.includes('baseUrl')) { - - ai-models.baseurl - - - {{ 'ai-models.baseurl-required' | translate }} - - - } @if (provider === aiProvider.OLLAMA) {
diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts index 9d0d28e627..935f35ab5f 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts @@ -74,6 +74,8 @@ export class AIModelDialogComponent extends DialogComponent, protected router: Router, protected dialogRef: MatDialogRef, @@ -107,7 +109,7 @@ export class AIModelDialogComponent extends DialogComponent { + if (this.provider === AiProvider.OPENAI) { + this.updateApiKeyValidatorForOpenAIProvider(url); + } }); this.aiModelForms.get('configuration.providerConfig.auth.type').valueChanges.pipe( @@ -161,6 +175,15 @@ export class AIModelDialogComponent extends DialogComponent Date: Tue, 30 Sep 2025 10:34:37 +0300 Subject: [PATCH 170/216] UI: Add for apiKey required value --- .../home/components/ai-model/ai-model-dialog.component.html | 2 +- .../home/components/ai-model/ai-model-dialog.component.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html index 5aa323fe7d..44fe9dcda7 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html @@ -129,7 +129,7 @@ @if (providerFieldsList.includes('apiKey')) { ai-models.api-key - + diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts index 935f35ab5f..02f18e60b5 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts @@ -74,6 +74,7 @@ export class AIModelDialogComponent extends DialogComponent, @@ -178,8 +179,10 @@ export class AIModelDialogComponent extends DialogComponent Date: Tue, 30 Sep 2025 10:37:57 +0300 Subject: [PATCH 171/216] UI: Improve Markdown widget: hide unused data settings block in configuration --- .../system/widget_types/markdown_html_card.json | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/application/src/main/data/json/system/widget_types/markdown_html_card.json b/application/src/main/data/json/system/widget_types/markdown_html_card.json index 64a847952a..b66e3ee88e 100644 --- a/application/src/main/data/json/system/widget_types/markdown_html_card.json +++ b/application/src/main/data/json/system/widget_types/markdown_html_card.json @@ -11,16 +11,10 @@ "resources": [], "templateHtml": "\n", "templateCss": "#container tb-markdown-widget {\n height: 100%;\n display: block;\n}\n\n#container tb-markdown-widget .tb-markdown-view {\n height: 100%;\n overflow: auto;\n}\n", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.markdownWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true,\n hasDataPageLink: true\n };\n}\n\nself.onDestroy = function() {\n}\n\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.markdownWidget.onDataUpdated();\n}\n\nself.actionSources = function() {\n return {\n 'elementClick': {\n name: 'widget-action.element-click',\n multiple: true\n }\n };\n}\n\nself.typeParameters = function() {\n return {\n dataKeysOptional: true,\n datasourcesOptional: true,\n hasDataPageLink: true,\n hideDataSettings: true\n };\n}\n\nself.onDestroy = function() {\n}\n\n", "settingsDirective": "tb-markdown-widget-settings", - "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"markdownTextPattern\":\"### Markdown/HTML card\\n - **Current entity**: ${entityName}.\\n - **Current value**: ${Random}.\",\"markdownTextFunction\":\"return '# Some title\\\\n - Entity name: ' + data[0]['entityName'];\",\"useMarkdownTextFunction\":false},\"title\":\"Markdown/HTML Card\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false}" + "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Temperature\",\"color\":\"#2196f3\",\"settings\":{},\"_hash\":0.15479322438769105,\"funcBody\":\"const baseTemp = 20;\\nconst dailySwing = 10;\\nconst hourlyVariation = Math.sin((time % 24) * Math.PI / 12) * dailySwing;\\nconst randomness = (Math.random() - 0.5) * 2;\\nconst smoothingFactor = 0.8;\\nreturn (prevValue * smoothingFactor) + ((baseTemp + hourlyVariation + randomness) * (1 - smoothingFactor));\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":false,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"0px\",\"settings\":{\"useMarkdownTextFunction\":false,\"markdownTextPattern\":\"### Markdown/HTML card\\n - **Current entity**: ${entityName}.\\n - **Current value**: ${Temperature}.\",\"markdownTextFunction\":\"return '# Some title\\\\n - Entity name: ' + data[0]['entityName'];\",\"applyDefaultMarkdownStyle\":true,\"markdownCss\":\"\"},\"title\":\"Markdown/HTML Card\",\"showTitleIcon\":false,\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"dropShadow\":true,\"enableFullscreen\":true,\"widgetStyle\":{},\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"showLegend\":false,\"useDashboardTimewindow\":true,\"displayTimewindow\":true}" }, - "tags": [ - "web", - "markup" - ], "resources": [ { "link": "/api/images/system/markdown_html_card_system_widget_image.png", @@ -33,5 +27,10 @@ "data": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAABdFBMVEX////u7u7g4ODf398nJydISEiCs/Tx8fE/Pz+amppgYGC6urr7+/upqan6+vrW1tbKysr19fX9/f3j4+M5OTn39/fs7OxSUlJxcXHFxcVCQkI8PDyMjIxFRUV+fn40NDTa2trp6el3d3dVVVXAwMCKiorQ0NClpaX6/P8vLy+SkpJbW1srKyvl5eWhoaHn5+dMTEytra2Hh4dlZWVPT0+3t7eenp6Pj49CjO7j7v3MzMx7e3uBgYE2NjabwvbR0dHIyMjV5vvY2NgyMjJra2tUl/A0hO3a6fzA2fqEhIQ4hu1XV1f3+v7Dw8OXl5eUlJT0+P6y0Pi0tLTc3NzV1dWysrKwsLCnyfeIt/U+ie6vr690dHRJSUkqfezy9/641Pnz8/NkoPJIj++rq6vg7fyszfh8r/Tt7e28vLySvfZZmvDR4/uNuvV2q/Pp8v1ppPJMku9oaGifxfctf+zT09NnZ2dfnvHL3/vG3fpup/Lt9P4vgO3lU88CAAAQR0lEQVR42uyby2/aQBDGJ7jINti8DOb9dAA1lEASHhIQJWp7QSiJ1JYcaA6VckivkdL/v4aPLbs1bkqLlZTmOwzKesa7P8WKP80QIq1s+P5xGeWQzWGYez+L/jFpsqFR2dz750GI5DIZe7sAohnk2wkQ8r2ArBQ6o6XCUXpEMS7TS5DmLQcyjaVNIS6UmBFdC8eQ/LSUX3psu6K0NjP2absg5538CiSRp6QpxIVKr3zGq7d/CJJQE2sz5fhWQcz7+gokeJxX3wX5iIqLRu+wkaaj1HBAdH6c+tyyQUKXTaoNC3GJSoVRifQy0Z0u51KFMEFYCedyBtEy8+0oVZXJN6pEyBi/zuV823y0HlYgtUrl0qrxERXX6sm9ek1vS8HTL3RyUIskJH/ivUrNbPBrUTKVYFcxP5+3moOUpNSOsiFUYcVMdyK0zGxmu2eKZOaDEcWQg8V0WvYIhIb7el+I0GG98rneIznYzZfoJE1EktI4IqoX5g/MPA71/mxU+dCX/KtHCCtEgQjLPFxEPdDtBo7waHkEcnU6ydxc8REVB7pp6ld7mWovG16CZK0DIrU6P9g8VtXZQaBxNZ2DJMOowgpAlpnHi9hQVbXmKYisyFY4ykdUHM+IZtXIhMhiIP7EtxxF2lH7YOfJUKgTKSnqpVLiQbBC9D5Iy8wzJfwpLkU6i9tqrxNbBUkpbzIXS5DwSUyJCREqTImmhUSgHSiWGAiFrBnlFOudQceZTJW0otkqajwIVoiC8c7+MvM68L5o0FXWurHXh5nk/hO9EOVyiESFFivRKP1K0btlZixEzdO5+8Z9ZPnp3+x/pmYmf/PheVmUZ6EdBQkW+h93ASQcOCp0dgHEVvmNuRsg9cBu/EaCmfBOgKSVwU781bq7+bobf37DrzKZzHQHQHbnhfgC8vRyAdlqLwuKkSBPQczZqC+tQD49IG7WyxJrhY4WL09ByuPbyoSBaL7DnqEhbtbLEmuFjpYoLx+tj0UGIrUtqy0hbtjLEmqxz7KjNaikrgjC3bwC0U8uGQiVG4EW4sa9LL4WQkcrEb/4eE4Q7uYVyEHg3mQgh7p+iLhxL4uvhdAIit1UDWLC3bx6tO7a+wBh+vNeFiSAkKz6RwThbt6A2L+MVubcCbJ5L8shdLRiIdKyPvyMu3kDEslPlJHwHvmLXpYodLTKmUb7RMPPuJtHj1brobX43GIvS+xoaS1ZuNvTv9mfnf4HkNbt7YDM5dwiJrgsp9xz3N2apyCRDz9A9i19QGeFbER0SpLfZernmuMUcrwEOcsGViAp9vcfTok7gHPq55LzVCDliboWBE4JUz/ptFJ4u5z6wV9hDuiWg30Gw1x/JDvdWvmQqLcnV1Nj3zZBqr2va0HglDD1k7KRo6yGqR/8FeaAbjlLb3A1HI9Up1sLJ+feQFLObrPmFpsPlrkeBBFTv/kjkffhsYG/whzQLYeZnPpld+xwawsQ5Ff07YEcZxqd00tXEEz98O7GIeGvMAd05ogg1/Xu2OnWSh2Wn1O3ByI9POgTSQCBR0LE1A+HxNQP/gpzQLccBkLUHTvdmlz8MgfJx7RkeksgkPhoMY+EiKkfDompH/wV5oBuOTzIGrfWVyw7/40/M/TohQgQeCRENvWDuKkf5oAuOY+6tYSp2VBRmTwDuYhbqnPq58lkUPK/eK1HQeCyNtHg9raFp4fmmkZ/x00hE1H0YLHNHJoIct7r9VoAgcvaCES39oloL9ldWE4lxjsuRKeQiYgcVLn0wXD1cZDxZwbC3h0bKbU/Dz20Lo55x4XoFDIRkYMqOLc/Bxle20EAgTtCtwo+Cu4I69x3rhhIqY1nohEh3nEhwlOx7lYwyDIRkYMqODd+X1RJ8dGwRuik4QxrQe5TelkEgTtCtwo+CqYC6/x3rpYgl2jAmVmNeMeFCE/FulsTi2UiIgdVcG78vqiSil+D2RY6aTjD+tGbfp8sCyBwR+hWwUcBBOvCd64AMpwSETpevONChKdi3a1WE5mIyGFV2J3fd16FlUIdnTScwQkCtfcFELgjvKnho+COsC585wogYx21F0So4kHgqfjuFjIRkYMq7M7vi6r5SlVFJw1nWAticn0teCe4I9wQPgruCOvCd64AclTBsUNEvONChKdi3a16HZmIyGFV2J3fF1X2SqIdQScNZ1gHUspwfS14J7gj3BA+Cu4I68J3rgAiZw2ycT7bgXdciPBUrLsVaCCTReSgCrvz+6LKeNdQcoROGs7wWF8L3klwR/BRcEdYF75zBRC6tWSi+6BgsGREeCp0t9ANQybiKpPtLuyLKjTSUIszbG5R4KPcV1QrfrH41Cl6Gv09T4VMxF/7txevxSRMCWMOj4SrLg6q/IVc5TGIGTx0gvglZ+cK7ghXXR3ULOluO70FCXfe99xA4H9Ej+QOgqv9CrnKU5BJffVoofuEKaEwB4TPgUfCVazIl6mKAUfEHJQGSo/lBKkV+/0BA0H3CVNCfg4InwN3xM8QE+1cLS3DEeGqrQOVvJcTZHZzVI+XfoCkiU0J+fEZfA4++RniRYOITQlxxdYUH95LBJme2AZYF0DU6s8g8DlwR/wMcZoiYlNCXLWlV8l7OUHCSuuu81UAwZQQvgj+Bz4H7oifIfriBrEpIa7aGl6T5xJBoFy+k9oTQDAlhC+C/4HPgTvCVbaSTbbZlBBXqalEyVO5/48VfnKdEkbvBI8kXo21ovBR7Gp0sr1fiCcWBe7o8at6j9z1TECenV5Anpt2CeQ7u3b0mjYQwHH8l/kTE03aNUZtrVFrO0Ps1TnbKjilYvIioxaq60v7IPRBXwvr/79LRre6MdhgGemW70vCwR33gQsEkn8kSfknSiBxK4HErQQStxJI3EogcSuBxK3/DTLR8njqvPcby28hSNEG+K6IIVbNCfddq13gefe8xFOP7q+vzj6C5mzh+ybzKCGCJci6ZPNPQPRZ/acQ9UOkkBEngG5ZAWQ7/OicN1LOpYTImz2YudnrEDK+ck6AG8m73AOObnD/eqI5O5AVjRRM4xV0Y5AxLoCTdnsQQF71nKKxDSjObIyCMbKNQoSQW68LrFmVkDbv0mwjx31OJWRlW69wS8v1JKTKsueNcWrDVPdNlE/hqp7KLGRL0UWTQ2zTCI5WfSRUT0KanlAtaphbns1K0RaurUQJeW/p8NOOhOxWTbNjS8hpannPXMlb4IINM2O7GHMKxS6bPb79RC4OpNctK/CZgez4EH2W4fAkgOyKBQwJybopOBKSHR2gK/JRHy1/wLUitJ6EoNmu2J6EXAP3LPMcaHMQPiNvKGe1mJrwulXqXJ2zDvcxGJlDdiV2Og1OdtOQkHDDc7ZM9wFYUNtiyTCmzEUNyeLOb4viTEKmwn8GEWIXmLEeQobMfFGpw1KrX5qqeA5p8h0ndzN1GkJqjyFki40QsqJVkv0FiCbSPlps7ogGcPoV0uvzBjfMyTE3NKDKAzRULi5YbmxA9JFaRsXmOoSc2SbqbGH/EPhEDbVjADqihxQF1wFky+2Me8J9glzqh7V8xkuvg7Fi7WzPCLb+kappqrzegMDnFE2KZQjR2D3KSsgb9uVVk9fKQLNXOLOOCpFC8GDpAQQfPd5lefIEwdz9oB9Z7GRdYM8mfQVQOASGXG1CDI5hqscIIYVbIaoSsvQpHiSk0HXpXQGGYD4yyGZ6ET9krvAlZYlfbVkIZxYzhWuef/tLeJlB2Et7aXSs/sxSNzb/MiHLSid9m8fXXixkswSSQF5KCSRuJZC4lUDiVgL53N699jQNxXEc/6HHTWVDGXL1Ak6Ezg0F5dISLRTsWmvFFZWuKqut28LuczO7wJv3dJTBiGgMooh8k2U77U6yT5om/ydLT1udEO/u3zOXbs1hv14yg936iR9HNt6N7xRNMnEVu/FpHFFdgxsr/BZIHyErAJ6T4K9DhsePgGQOQfQ8DpczD0Gy0nEhq4+BrsgqhYz51/sxMHfB76WQt3O9uO5/40CWJ4cDWP6KT3P0bamve+BzdwC0lWvAxOR4AE6P3s94XUjAZKBqWlGmEEEXkCvUGskcUN9GTAYkvQE466Rel5Jgc2YqwZgKzx0TErzhw8ORcBCTkScjsz3D5NnqAJn5unoL8+TeZQp5Ebq6OoQ7YWyQCdyc95PpMLkF2q3HmJy+QneCgm6v3Rnpda/IZgy1Wl5kwbOFfDQnW6JWF8EoFcgWtDKfzaNSQE6JFwwerBgvZRleiUvHhHSvbmHw9b0gNrawSN4Ok/foJQs3PgR8o4/paXpR1unx5e5Qz1Bo4QIZ85MLGBp0Ic8n0fWgBZkAPl5vQxosgwaFlIA0Dz5NFYmGWEO8wigSElGVQpo8nBcrQ6VyVsBxIcHnK2RqOoi+4PRlcn2YvKGQCLmIKbLl3CMbpAtYveKNLI9emX1/M+An/XhxyYUsjoQnP4HWvxAeDI23IXoTEFhHgHim9dY0PWZ5W0wmq5Zl7UgUUq47ypagWvwNkPXrkQ9XcTuIl0/uruxBwrNhXxeZcyDjZAo9kYd4efVSb+hJEB0QBCaC90BbCPZjcB+ilQ5DUh5bzVdsFHeSHMcxFCI2fjMkcJusO5DLQwMLZHkXMjMWWsN0eMpP/N5nT7vWIhfxgNzHF/KuEzL7ClMRH4ChBxgb3WpDOIVDpQ2hvxrFaBM5xQOUU2B0B5IvQBXbELt+bAjWRvsdyEaIfCGTLgTzkYmlm+RqyI/hy+TZDLBIFjFDBjohr27P3ngI2tLNkcHZ+TYEFcW22hAhWgLEFMDKgFQuGy1bosba+5C4oh0D0lnPJ3QW8KJVlw9H5vV1ftctwahmFnupsY5zKpwYNYGahr22mdM1omQ8rRQxG7U8P8xSsmUlvb/mTxfELaFVJPwkSU/FzofG/wsS22TwneJ5nKbOKCQhcykZgt4AwKU0FaqcTDkQoQ5niDXB5QRdaEOSKXo00Sim5NaGhiRB3gZMZ6Mute7gnMABgl7HCdcJkaKlDGuVMoYG2eYLIrgd0UMhdUPA5jaKVWhKM6+YLkQrV5oWBKWUsXlIiidtZGDngCinimneqKOh5JuGDr3MizwOdbIQhYHGMqgU0JAQiHLczjZim3Ujhz1IFtBEFyInoVYTghKDVkMhAxT2IEUNyHhQ00FfapRDMcqgs5OFGIAsAqkSEvGsWJW4KCjEKKANsQAu6kK2PVlxsyiwgJyFKAPxPQhMK8umYUhAUxd2LMuqcujsT0EKeQaGC0mx5kGIZLiQEs+gugepmQcgOZsDn6YLB5JUONrfuiLZFOo7wi6EyRkcjDpkCrFjiDddSNmEvMm5kEyJUbMZ1FKQqpwpMqpVQNqDhK0ztgZVx6H+FERWWIs1XQh4kUkpdsmBiHaZcyGmwpaUhguJWYYtZug+u0Zv9prBNrMoimy5rEOgI+6fvNk7YxJo1Tm0ahYOHGdiBz8nAiW9vS/BOFvoQpTd1cnWCfH8NIs96kyJjq/RdOexmpEtG+5nE0f0d2YtyfzR+JrAoYSKpuKHnQ+N55BT3jnktHUOOW2dIUifD2cgXx/uenEG8t5FT5/3n78mzkO0z8hjzX34BpPgTEZLPbVzAAAAAElFTkSuQmCC", "public": true } + ], + "scada": false, + "tags": [ + "web", + "markup" ] } \ No newline at end of file From 7eb22255f6b04d538752cf8cda0faa97b1a00183 Mon Sep 17 00:00:00 2001 From: Ekaterina Chantsova Date: Tue, 23 Sep 2025 18:45:55 +0300 Subject: [PATCH 172/216] Timewindow: fix options displaying when switching between Realtime and History --- .../timewindow-config-dialog.component.ts | 5 +- .../time/timewindow-panel.component.ts | 5 +- .../src/app/shared/models/time/time.models.ts | 52 +++++++++++-------- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts index 1e1f831c4f..541b584682 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-config-dialog.component.ts @@ -411,9 +411,10 @@ export class TimewindowConfigDialogComponent extends PageComponent implements On const timewindowFormValue = this.timewindowForm.getRawValue(); const realtimeDisableCustomInterval = timewindowFormValue.realtime.disableCustomInterval; const historyDisableCustomInterval = timewindowFormValue.history.disableCustomInterval; - updateFormValuesOnTimewindowTypeChange(selectedTab, this.quickIntervalOnly, this.timewindowForm, + updateFormValuesOnTimewindowTypeChange(selectedTab, this.timewindowForm, realtimeDisableCustomInterval, historyDisableCustomInterval, - timewindowFormValue.realtime.advancedParams, timewindowFormValue.history.advancedParams); + timewindowFormValue.realtime.advancedParams, timewindowFormValue.history.advancedParams, + this.realtimeTimewindowOptions, this.historyTimewindowOptions); this.timewindowForm.patchValue({ hideAggregation: timewindowFormValue.hideAggregation, hideAggInterval: timewindowFormValue.hideAggInterval, diff --git a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts index d7a0d44013..50d02cdd3e 100644 --- a/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts +++ b/ui-ngx/src/app/shared/components/time/timewindow-panel.component.ts @@ -400,9 +400,10 @@ export class TimewindowPanelComponent extends PageComponent implements OnInit, O } private onTimewindowTypeChange(selectedTab: TimewindowType) { - updateFormValuesOnTimewindowTypeChange(selectedTab, this.quickIntervalOnly, this.timewindowForm, + updateFormValuesOnTimewindowTypeChange(selectedTab, this.timewindowForm, this.realtimeDisableCustomInterval, this.historyDisableCustomInterval, - this.realtimeAdvancedParams, this.historyAdvancedParams); + this.realtimeAdvancedParams, this.historyAdvancedParams, + this.realtimeTimewindowOptions, this.historyTimewindowOptions); } update() { diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index c204cb6867..e3a5eaef37 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -20,6 +20,7 @@ import moment_ from 'moment'; import * as momentTz from 'moment-timezone'; import { IntervalType } from '@shared/models/telemetry/telemetry.models'; import { FormGroup } from '@angular/forms'; +import { ToggleHeaderOption } from '@shared/components/toggle-header.component'; const moment = moment_; @@ -526,17 +527,20 @@ export const timewindowTypeChanged = (newTimewindow: Timewindow, oldTimewindow: }; export const updateFormValuesOnTimewindowTypeChange = (selectedTab: TimewindowType, - quickIntervalOnly: boolean, timewindowForm: FormGroup, + timewindowForm: FormGroup, realtimeDisableCustomInterval: boolean, historyDisableCustomInterval: boolean, - realtimeAdvancedParams?: TimewindowAdvancedParams, - historyAdvancedParams?: TimewindowAdvancedParams) => { + realtimeAdvancedParams: TimewindowAdvancedParams, + historyAdvancedParams: TimewindowAdvancedParams, + realtimeTimewindowOptions: ToggleHeaderOption[], + historyTimewindowOptions: ToggleHeaderOption[]) => { const timewindowFormValue = timewindowForm.getRawValue(); if (selectedTab === TimewindowType.REALTIME) { - if (timewindowFormValue.history.historyType !== HistoryWindowType.FIXED - && !(quickIntervalOnly && timewindowFormValue.history.historyType === HistoryWindowType.LAST_INTERVAL)) { - if (Object.keys(RealtimeWindowType).includes(HistoryWindowType[timewindowFormValue.history.historyType])) { - timewindowForm.get('realtime.realtimeType').patchValue(RealtimeWindowType[HistoryWindowType[timewindowFormValue.history.historyType]]); - } + const sameWindowTypeOptionAvailable = realtimeTimewindowOptions.some( + option => { + return option.value === RealtimeWindowType[HistoryWindowType[timewindowFormValue.history.historyType]] + }); + if (sameWindowTypeOptionAvailable) { + timewindowForm.get('realtime.realtimeType').patchValue(RealtimeWindowType[HistoryWindowType[timewindowFormValue.history.historyType]]); if (!realtimeDisableCustomInterval || !realtimeAdvancedParams?.allowedLastIntervals?.length || realtimeAdvancedParams.allowedLastIntervals.includes(timewindowFormValue.history.timewindowMs)) { timewindowForm.get('realtime.timewindowMs').patchValue(timewindowFormValue.history.timewindowMs); @@ -554,20 +558,26 @@ export const updateFormValuesOnTimewindowTypeChange = (selectedTab: TimewindowTy } } } else { - timewindowForm.get('history.historyType').patchValue(HistoryWindowType[RealtimeWindowType[timewindowFormValue.realtime.realtimeType]]); - if (!historyDisableCustomInterval || + const sameWindowTypeOptionAvailable = historyTimewindowOptions.some( + option => { + return option.value === HistoryWindowType[RealtimeWindowType[timewindowFormValue.realtime.realtimeType]] + }); + if (sameWindowTypeOptionAvailable) { + timewindowForm.get('history.historyType').patchValue(HistoryWindowType[RealtimeWindowType[timewindowFormValue.realtime.realtimeType]]); + if (!historyDisableCustomInterval || !historyAdvancedParams?.allowedLastIntervals?.length || historyAdvancedParams.allowedLastIntervals?.includes(timewindowFormValue.realtime.timewindowMs)) { - timewindowForm.get('history.timewindowMs').patchValue(timewindowFormValue.realtime.timewindowMs); - } - if (!historyAdvancedParams?.allowedQuickIntervals?.length || historyAdvancedParams.allowedQuickIntervals?.includes(timewindowFormValue.realtime.quickInterval)) { - timewindowForm.get('history.quickInterval').patchValue(timewindowFormValue.realtime.quickInterval); - } - const defaultAggInterval = historyDefaultAggInterval(timewindowForm.getRawValue(), historyAdvancedParams); - const allowedAggIntervals = historyAllowedAggIntervals(timewindowForm.getRawValue(), historyAdvancedParams); - if (defaultAggInterval || !allowedAggIntervals.length || allowedAggIntervals.includes(timewindowFormValue.realtime.interval)) { - setTimeout(() => timewindowForm.get('history.interval').patchValue( - defaultAggInterval ?? timewindowFormValue.realtime.interval - )); + timewindowForm.get('history.timewindowMs').patchValue(timewindowFormValue.realtime.timewindowMs); + } + if (!historyAdvancedParams?.allowedQuickIntervals?.length || historyAdvancedParams.allowedQuickIntervals?.includes(timewindowFormValue.realtime.quickInterval)) { + timewindowForm.get('history.quickInterval').patchValue(timewindowFormValue.realtime.quickInterval); + } + const defaultAggInterval = historyDefaultAggInterval(timewindowForm.getRawValue(), historyAdvancedParams); + const allowedAggIntervals = historyAllowedAggIntervals(timewindowForm.getRawValue(), historyAdvancedParams); + if (defaultAggInterval || !allowedAggIntervals.length || allowedAggIntervals.includes(timewindowFormValue.realtime.interval)) { + setTimeout(() => timewindowForm.get('history.interval').patchValue( + defaultAggInterval ?? timewindowFormValue.realtime.interval + )); + } } } timewindowForm.patchValue({ From 5a6ddce8f54ec5ec8ac0987fb5d672feea262e92 Mon Sep 17 00:00:00 2001 From: VIacheslavKlimov Date: Fri, 26 Sep 2025 13:16:31 +0300 Subject: [PATCH 173/216] AI Request Node: added ability to attach files (#13910) --- .../server/actors/ActorSystemContext.java | 5 + .../actors/ruleChain/DefaultTbContext.java | 21 +- .../controller/TbResourceController.java | 17 ++ ...faultTbCalculatedFieldConsumerService.java | 4 +- .../queue/DefaultTbClusterService.java | 9 +- .../queue/DefaultTbCoreConsumerService.java | 4 +- .../queue/DefaultTbEdgeConsumerService.java | 2 +- .../DefaultTbRuleEngineConsumerService.java | 4 +- .../processing/AbstractConsumerService.java | 5 + ...AbstractPartitionBasedConsumerService.java | 4 +- .../resource/DefaultTbResourceService.java | 1 - .../src/main/resources/thingsboard.yml | 3 + .../controller/TbResourceControllerTest.java | 23 +- .../DefaultResourceDataCacheTest.java | 83 +++++++ .../sql/BaseTbResourceServiceTest.java | 121 +++++++++- .../server/dao/resource/ResourceService.java | 5 + .../dao/resource/TbResourceDataCache.java | 28 +++ .../common/data/GeneralFileDescriptor.java | 29 +++ .../server/common/data/ResourceType.java | 3 +- .../server/common/data/TbResource.java | 6 + .../common/data/TbResourceDataInfo.java | 31 +++ .../common/data/TbResourceDeleteResult.java | 3 +- .../thingsboard/common/util/DonAsynchron.java | 15 ++ .../server/dao/ResourceContainerDao.java | 5 +- .../server/dao/resource/BaseImageService.java | 5 +- .../dao/resource/BaseResourceService.java | 72 ++++-- .../resource/DefaultTbResourceDataCache.java | 72 ++++++ .../server/dao/resource/TbResourceDao.java | 2 + .../dao/resource/TbResourceInfoDao.java | 2 + .../server/dao/rule/RuleChainDao.java | 4 +- .../dashboard/DashboardInfoRepository.java | 16 +- .../sql/dashboard/JpaDashboardInfoDao.java | 10 +- .../dao/sql/resource/JpaTbResourceDao.java | 6 + .../sql/resource/JpaTbResourceInfoDao.java | 8 + .../resource/TbResourceInfoRepository.java | 6 + .../sql/resource/TbResourceRepository.java | 3 + .../server/dao/sql/rule/JpaRuleChainDao.java | 12 + .../dao/sql/rule/RuleChainRepository.java | 15 ++ .../dao/sql/widget/JpaWidgetTypeDao.java | 10 +- .../sql/widget/WidgetTypeInfoRepository.java | 15 +- .../dao/sql/rule/JpaRuleNodeDaoTest.java | 1 + .../rule/engine/api/TbContext.java | 7 + .../thingsboard/rule/engine/ai/TbAiNode.java | 121 +++++++++- .../rule/engine/ai/TbAiNodeConfiguration.java | 6 +- .../rule/engine/ai/TbAiNodeTest.java | 223 ++++++++++++++++++ 45 files changed, 965 insertions(+), 82 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java create mode 100644 common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceDataCache.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/GeneralFileDescriptor.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDataInfo.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/resource/DefaultTbResourceDataCache.java diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java index ea46ce86eb..b23015a1fe 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java @@ -97,6 +97,7 @@ import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.resource.TbResourceDataCache; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleNodeStateService; @@ -511,6 +512,10 @@ public class ActorSystemContext { @Getter private ResourceService resourceService; + @Autowired + @Getter + private TbResourceDataCache resourceDataCache; + @Lazy @Autowired(required = false) @Getter diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 6374e4016d..88b04c7613 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -51,6 +51,7 @@ import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasRuleEngineProfile; +import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.alarm.Alarm; @@ -60,6 +61,7 @@ import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -110,6 +112,7 @@ import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.resource.TbResourceDataCache; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; @@ -770,6 +773,11 @@ public class DefaultTbContext implements TbContext { return mainCtx.getResourceService(); } + @Override + public TbResourceDataCache getTbResourceDataCache() { + return mainCtx.getResourceDataCache(); + } + @Override public OtaPackageService getOtaPackageService() { return mainCtx.getOtaPackageService(); @@ -1054,7 +1062,18 @@ public class DefaultTbContext implements TbContext { @Override public void checkTenantEntity(EntityId entityId) throws TbNodeException { - if (!this.getTenantId().equals(TenantIdLoader.findTenantId(this, entityId))) { + TenantId actualTenantId = TenantIdLoader.findTenantId(this, entityId); + assertSameTenantId(actualTenantId, entityId); + } + + @Override + public & HasTenantId, I extends EntityId> void checkTenantEntity(E entity) throws TbNodeException { + TenantId actualTenantId = entity.getTenantId(); + assertSameTenantId(actualTenantId, entity.getId()); + } + + private void assertSameTenantId(TenantId tenantId, EntityId entityId) throws TbNodeException { + if (!getTenantId().equals(tenantId)) { throw new TbNodeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant.", true); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java index b23603f6a2..54d2494679 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TbResourceController.java @@ -55,14 +55,17 @@ import org.thingsboard.server.common.data.util.ThrowingSupplier; import org.thingsboard.server.config.annotations.ApiOperation; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.resource.TbResourceService; +import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.UUID; import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; import static org.thingsboard.server.controller.ControllerConstants.LWM2M_OBJECT_DESCRIPTION; @@ -263,6 +266,20 @@ public class TbResourceController extends BaseController { } } + @ApiOperation(value = "Get Resource Infos by ids (getSystemOrTenantResourcesByIds)") + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @GetMapping(value = "/resource", params = {"resourceIds"}) + public List getSystemOrTenantResourcesByIds( + @Parameter(description = "A list of resource ids, separated by comma ','", array = @ArraySchema(schema = @Schema(type = "string"))) + @RequestParam("resourceIds") Set resourceUuids) throws ThingsboardException { + SecurityUser user = getCurrentUser(); + List resourceIds = new ArrayList<>(); + for (UUID resourceId : resourceUuids) { + resourceIds.add(new TbResourceId(resourceId)); + } + return resourceService.findSystemOrTenantResourcesByIds(user.getTenantId(), resourceIds); + } + @ApiOperation(value = "Get All Resource Infos (getAllResources)", notes = "Returns a page of Resource Info objects owned by tenant. " + PAGE_DATA_PARAMETERS + RESOURCE_INFO_DESCRIPTION + TENANT_AUTHORITY_PARAGRAPH) diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java index 8d4ab25578..acb36449e8 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCalculatedFieldConsumerService.java @@ -35,6 +35,7 @@ import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.dao.resource.TbResourceDataCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldLinkedTelemetryMsgProto; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; @@ -83,6 +84,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa ActorSystemContext actorContext, TbDeviceProfileCache deviceProfileCache, TbAssetProfileCache assetProfileCache, + TbResourceDataCache tbResourceDataCache, TbTenantProfileCache tenantProfileCache, TbApiUsageStateService apiUsageStateService, PartitionService partitionService, @@ -90,7 +92,7 @@ public class DefaultTbCalculatedFieldConsumerService extends AbstractPartitionBa JwtSettingsService jwtSettingsService, CalculatedFieldCache calculatedFieldCache, CalculatedFieldStateService stateService) { - super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, calculatedFieldCache, apiUsageStateService, partitionService, + super(actorContext, tenantProfileCache, deviceProfileCache, assetProfileCache, tbResourceDataCache, calculatedFieldCache, apiUsageStateService, partitionService, eventPublisher, jwtSettingsService); this.queueFactory = tbQueueFactory; this.stateService = stateService; diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 265f14c4e2..86d00c77ca 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -48,6 +48,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.page.PageData; @@ -435,8 +436,9 @@ public class DefaultTbClusterService implements TbClusterService { @Override public void onResourceChange(TbResourceInfo resource, TbQueueCallback callback) { + TenantId tenantId = resource.getTenantId(); + TbResourceId resourceId = resource.getId(); if (resource.getResourceType() == ResourceType.LWM2M_MODEL) { - TenantId tenantId = resource.getTenantId(); log.trace("[{}][{}][{}] Processing change resource", tenantId, resource.getResourceType(), resource.getResourceKey()); ResourceUpdateMsg resourceUpdateMsg = ResourceUpdateMsg.newBuilder() .setTenantIdMSB(tenantId.getId().getMostSignificantBits()) @@ -447,6 +449,7 @@ public class DefaultTbClusterService implements TbClusterService { ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceUpdateMsg(resourceUpdateMsg).build(); broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback); } + broadcastEntityStateChangeEvent(tenantId, resourceId, ComponentLifecycleEvent.UPDATED); } @Override @@ -462,6 +465,7 @@ public class DefaultTbClusterService implements TbClusterService { ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setResourceDeleteMsg(resourceDeleteMsg).build(); broadcast(transportMsg, DataConstants.LWM2M_TRANSPORT_NAME, callback); } + broadcastEntityStateChangeEvent(resource.getTenantId(), resource.getId(), ComponentLifecycleEvent.DELETED); } private void broadcastEntityChangeToTransport(TenantId tenantId, EntityId entityid, T entity, TbQueueCallback callback) { @@ -592,7 +596,8 @@ public class DefaultTbClusterService implements TbClusterService { EntityType.TENANT_PROFILE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE, - EntityType.JOB) + EntityType.JOB, + EntityType.TB_RESOURCE) || (entityType == EntityType.ASSET && msg.getEvent() == ComponentLifecycleEvent.UPDATED) || (entityType == EntityType.DEVICE && msg.getEvent() == ComponentLifecycleEvent.UPDATED) ) { diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java index f0b1a4d7d2..9ab5a062eb 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java @@ -55,6 +55,7 @@ import org.thingsboard.server.common.stats.StatsFactory; import org.thingsboard.server.common.util.KvProtoUtil; import org.thingsboard.server.common.util.ProtoUtils; import org.thingsboard.server.dao.resource.ImageCacheKey; +import org.thingsboard.server.dao.resource.TbResourceDataCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.DeviceStateServiceMsgProto; @@ -176,10 +177,11 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService>>() { + var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { }); Assert.assertNotNull(widgetTypeInfos); Assert.assertFalse(widgetTypeInfos.isEmpty()); Assert.assertEquals(1, widgetTypeInfos.size()); - var dashboardInfo = widgetTypeInfos.get(EntityType.WIDGET_TYPE.name()).get(0); - Assert.assertNotNull(dashboardInfo); - - WidgetTypeInfo foundedWidgetType = doGet("/api/widgetTypeInfo/" + savedWidgetType.getId().getId().toString(), WidgetTypeInfo.class); - Assert.assertNotNull(foundedWidgetType); - Assert.assertEquals(foundedWidgetType, dashboardInfo); + var widgetTypeInfo = widgetTypeInfos.get(EntityType.WIDGET_TYPE.name()).get(0); + Assert.assertNotNull(widgetTypeInfo); + Assert.assertEquals(new EntityInfo(savedWidgetType.getId(), savedWidgetType.getName()), widgetTypeInfo); } @Test @@ -372,7 +370,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { Assert.assertTrue(isSuccess); var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); - var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { + var widgetTypeInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { }); Assert.assertNull(widgetTypeInfos); } @@ -417,7 +415,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); Assert.assertNotNull(referenceValues); - var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { + var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { }); Assert.assertNotNull(dashboardInfos); Assert.assertFalse(dashboardInfos.isEmpty()); @@ -425,10 +423,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { var dashboardInfo = dashboardInfos.get(EntityType.DASHBOARD.name()).get(0); Assert.assertNotNull(dashboardInfo); - - DashboardInfo foundDashboard = doGet("/api/dashboard/info/" + savedDashboard.getId().getId().toString(), DashboardInfo.class); - Assert.assertNotNull(foundDashboard); - Assert.assertEquals(foundDashboard, dashboardInfo); + Assert.assertEquals(new EntityInfo(savedDashboard.getId(), savedDashboard.getName()), dashboardInfo); } @Test @@ -469,7 +464,7 @@ public class TbResourceControllerTest extends AbstractControllerTest { Assert.assertTrue(isSuccess); var referenceValues = JacksonUtil.toJsonNode(deleteResponse).get("references"); - var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { + var dashboardInfos = JacksonUtil.readValue(referenceValues.toString(), new TypeReference>>() { }); Assert.assertNull(dashboardInfos); } diff --git a/application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java b/application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java new file mode 100644 index 0000000000..f12a8d3c5d --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/resource/DefaultResourceDataCacheTest.java @@ -0,0 +1,83 @@ +/** + * 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.service.resource; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.GeneralFileDescriptor; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDataInfo; +import org.thingsboard.server.common.data.TbResourceInfo; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.resource.ResourceService; +import org.thingsboard.server.dao.resource.TbResourceDataCache; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +@DaoSqlTest +public class DefaultResourceDataCacheTest extends AbstractControllerTest { + + @MockitoSpyBean + private ResourceService resourceService; + @Autowired + private TbResourceService tbResourceService; + @MockitoSpyBean + private TbResourceDataCache resourceDataCache; + + @Test + public void testGetCachedResourceData() throws Exception { + loginTenantAdmin(); + + TbResource resource = new TbResource(); + resource.setTenantId(tenantId); + resource.setTitle("File for AI request"); + resource.setResourceType(ResourceType.GENERAL); + resource.setFileName("myTestJson.json"); + GeneralFileDescriptor descriptor = new GeneralFileDescriptor("application/json"); + resource.setDescriptorValue(descriptor); + byte[] data = "This is a test prompt for AI request.".getBytes(); + resource.setData(data); + TbResourceInfo savedResource = tbResourceService.save(resource); + verify(resourceDataCache, timeout(2000).times(1)).evictResourceData(tenantId, savedResource.getId()); + + TbResourceDataInfo cachedData = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get(); + assertThat(cachedData.getData()).isEqualTo(data); + assertThat(JacksonUtil.treeToValue(cachedData.getDescriptor(), GeneralFileDescriptor.class)).isEqualTo(descriptor); + verify(resourceService).getResourceDataInfo(tenantId, savedResource.getId()); + + // retrieve resource data second time + clearInvocations(resourceService); + TbResourceDataInfo cachedData2 = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get(); + assertThat(cachedData2.getData()).isEqualTo(data); + verifyNoMoreInteractions(resourceService); + + // delete resource, check cache + TbResource resourceById = resourceService.findResourceById(tenantId, savedResource.getId()); + tbResourceService.delete(resourceById, true, null); + verify(resourceDataCache, timeout(2000).times(2)).evictResourceData(tenantId, savedResource.getId()); + TbResourceDataInfo cachedDataAfterDeletion = resourceDataCache.getResourceDataInfoAsync(tenantId, savedResource.getId()).get(); + assertThat(cachedDataAfterDeletion).isEqualTo(null); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java index fe416eacd5..da20b9489c 100644 --- a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.service.resource.sql; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -24,8 +25,10 @@ import org.junit.Test; import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.rule.engine.ai.TbAiNode; +import org.thingsboard.rule.engine.ai.TbAiNodeConfiguration; +import org.thingsboard.rule.engine.ai.TbResponseFormat; import org.thingsboard.server.common.data.Dashboard; -import org.thingsboard.server.common.data.DashboardInfo; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; @@ -37,26 +40,40 @@ import org.thingsboard.server.common.data.TbResourceInfoFilter; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.ai.AiModel; +import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig; +import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; +import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.RuleChainId; +import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.rule.RuleChain; +import org.thingsboard.server.common.data.rule.RuleChainMetaData; +import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.widget.WidgetTypeDetails; -import org.thingsboard.server.common.data.widget.WidgetTypeInfo; import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.ai.AiModelService; import org.thingsboard.server.dao.dashboard.DashboardService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.resource.ResourceService; +import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.service.resource.TbResourceService; import java.util.ArrayList; +import java.util.Arrays; import java.util.Base64; import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.function.Function; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -135,6 +152,10 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { private WidgetTypeService widgetTypeService; @Autowired private DashboardService dashboardService; + @Autowired + private RuleChainService ruleChainService; + @Autowired + private AiModelService aiModelService; private Tenant savedTenant; private User tenantAdmin; @@ -453,11 +474,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { Assert.assertFalse(result.getReferences().isEmpty()); Assert.assertEquals(1, result.getReferences().size()); - WidgetTypeInfo widgetTypeInfo = (WidgetTypeInfo) result.getReferences().get(EntityType.WIDGET_TYPE.name()).get(0); - WidgetTypeInfo foundWidgetTypeInfo = new WidgetTypeInfo(foundWidgetType); + EntityInfo widgetTypeInfo = (EntityInfo) result.getReferences().get(EntityType.WIDGET_TYPE.name()).get(0); Assert.assertNotNull(widgetTypeInfo); - Assert.assertNotNull(foundWidgetTypeInfo); - Assert.assertEquals(widgetTypeInfo, foundWidgetTypeInfo); + Assert.assertEquals(widgetTypeInfo, new EntityInfo(foundWidgetType.getId(), foundWidgetType.getName())); TbResourceInfo foundResourceInfo = resourceService.findResourceInfoById(savedTenant.getId(), savedResource.getId()); Assert.assertNotNull(foundResource); @@ -546,11 +565,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { Assert.assertNotNull(result.getReferences()); Assert.assertEquals(1, result.getReferences().size()); - DashboardInfo dashboardInfo = (DashboardInfo) result.getReferences().get(EntityType.DASHBOARD.name()).get(0); - DashboardInfo foundDashboardInfo = dashboardService.findDashboardInfoById(savedTenant.getId(), savedDashboard.getId()); + EntityInfo dashboardInfo = (EntityInfo) result.getReferences().get(EntityType.DASHBOARD.name()).get(0); Assert.assertNotNull(dashboardInfo); - Assert.assertNotNull(foundDashboardInfo); - Assert.assertEquals(foundDashboardInfo, dashboardInfo); + Assert.assertEquals(new EntityInfo(savedDashboard.getId(), savedDashboard.getName()), dashboardInfo); foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); Assert.assertNotNull(foundResource); @@ -598,6 +615,90 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { Assert.assertNull(foundResource); } + @Test + public void testShouldNotDeleteResourceIfUsedInAiNode() throws Exception { + TbResource resource = new TbResource(); + resource.setResourceType(ResourceType.GENERAL); + resource.setTitle("My resource"); + resource.setFileName("test.json"); + resource.setTenantId(savedTenant.getId()); + resource.setData("".getBytes()); + TbResourceInfo savedResource = tbResourceService.save(resource); + RuleChainMetaData ruleChain = createRuleChainReferringResource(savedResource.getId()); + + TbResourceDeleteResult result = tbResourceService.delete(savedResource, false, null); + assertThat(result).isNotNull(); + assertThat(result.isSuccess()).isFalse(); + assertThat(result.getReferences()).isNotEmpty().hasSize(1); + EntityInfo entityInfo = (EntityInfo) result.getReferences().get(EntityType.RULE_CHAIN.name()).get(0); + assertThat(entityInfo).isEqualTo(new EntityInfo(ruleChain.getRuleChainId(), "Test")); + + TbResource foundResource = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); + assertThat(foundResource).isNotNull(); + + // force delete + TbResourceDeleteResult deleteResult = tbResourceService.delete(savedResource, true, null); + assertThat(deleteResult).isNotNull(); + assertThat(deleteResult.isSuccess()).isTrue(); + + TbResource resourceAfterDeletion = resourceService.findResourceById(savedTenant.getId(), savedResource.getId()); + assertThat(resourceAfterDeletion).isNull(); + } + + private RuleChainMetaData createRuleChainReferringResource(TbResourceId resourceId) { + AiModel model = constructValidOpenAiModel("Test model"); + AiModel saved = aiModelService.save(model); + + RuleChain ruleChain = new RuleChain(); + ruleChain.setTenantId(tenantId); + ruleChain.setName("Test"); + ruleChain.setType(RuleChainType.CORE); + ruleChain.setDebugMode(true); + ruleChain.setConfiguration(JacksonUtil.newObjectNode().set("a", new TextNode("b"))); + ruleChain = ruleChainService.saveRuleChain(ruleChain); + RuleChainId ruleChainId = ruleChain.getId(); + + RuleChainMetaData metaData = new RuleChainMetaData(); + metaData.setRuleChainId(ruleChainId); + + RuleNode aiNode = new RuleNode(); + aiNode.setName("Ai request"); + aiNode.setType(org.thingsboard.rule.engine.ai.TbAiNode.class.getName()); + aiNode.setConfigurationVersion(TbAiNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version()); + aiNode.setDebugSettings(DebugSettings.all()); + TbAiNodeConfiguration configuration = new TbAiNodeConfiguration(); + configuration.setResourceIds(Set.of(resourceId.getId())); + configuration.setModelId(saved.getId()); + configuration.setResponseFormat(new TbResponseFormat.TbJsonResponseFormat()); + configuration.setTimeoutSeconds(1); + configuration.setUserPrompt("What is temp"); + aiNode.setConfiguration(JacksonUtil.valueToTree(configuration)); + + metaData.setNodes(Arrays.asList(aiNode)); + metaData.setFirstNodeIndex(0); + ruleChainService.saveRuleChainMetaData(tenantId, metaData, Function.identity()); + return ruleChainService.loadRuleChainMetaData(tenantId, ruleChainId); + } + + private AiModel constructValidOpenAiModel(String name) { + var modelConfig = OpenAiChatModelConfig.builder() + .providerConfig(new OpenAiProviderConfig("test-api-key")) + .modelId("gpt-4o") + .temperature(0.5) + .topP(0.3) + .frequencyPenalty(0.1) + .presencePenalty(0.2) + .maxOutputTokens(1000) + .timeoutSeconds(60) + .maxRetries(2) + .build(); + + return AiModel.builder() + .tenantId(tenantId) + .name(name) + .configuration(modelConfig) + .build(); + } @Test public void testFindTenantResourcesByTenantId() throws Exception { loginSysAdmin(); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java index ee187db46b..65211ec17a 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/ResourceService.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.ResourceExportData; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDataInfo; import org.thingsboard.server.common.data.TbResourceDeleteResult; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfoFilter; @@ -46,6 +47,8 @@ public interface ResourceService extends EntityDaoService { byte[] getResourceData(TenantId tenantId, TbResourceId resourceId); + TbResourceDataInfo getResourceDataInfo(TenantId tenantId, TbResourceId resourceId); + ResourceExportData exportResource(TbResourceInfo resourceInfo); List exportResources(TenantId tenantId, Collection resources); @@ -90,4 +93,6 @@ public interface ResourceService extends EntityDaoService { TbResource createOrUpdateSystemResource(ResourceType resourceType, ResourceSubType resourceSubType, String resourceKey, byte[] data); + List findSystemOrTenantResourcesByIds(TenantId tenantId, List resourceIds); + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceDataCache.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceDataCache.java new file mode 100644 index 0000000000..23485684af --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/resource/TbResourceDataCache.java @@ -0,0 +1,28 @@ +/** + * 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.resource; + +import com.google.common.util.concurrent.FluentFuture; +import org.thingsboard.server.common.data.TbResourceDataInfo; +import org.thingsboard.server.common.data.id.TbResourceId; +import org.thingsboard.server.common.data.id.TenantId; + +public interface TbResourceDataCache { + + FluentFuture getResourceDataInfoAsync(TenantId tenantId, TbResourceId resourceId); + + void evictResourceData(TenantId tenantId, TbResourceId resourceId); +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/GeneralFileDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/GeneralFileDescriptor.java new file mode 100644 index 0000000000..94edd4fa01 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/GeneralFileDescriptor.java @@ -0,0 +1,29 @@ +/** + * 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.common.data; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Data +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor +public class GeneralFileDescriptor { + private String mediaType; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java index 77b17198e9..f7579b6878 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ResourceType.java @@ -25,7 +25,8 @@ public enum ResourceType { PKCS_12("application/x-pkcs12", false, false), JS_MODULE("application/javascript", true, true), IMAGE(null, true, true), - DASHBOARD("application/json", true, true); + DASHBOARD("application/json", true, true), + GENERAL(null, false, true); @Getter private final String mediaType; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java index ba37067106..457d30e263 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResource.java @@ -16,6 +16,7 @@ package org.thingsboard.server.common.data; import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonSetter; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -86,6 +87,11 @@ public class TbResource extends TbResourceInfo { .orElse(null); } + @JsonIgnore + public TbResourceDataInfo toResourceDataInfo() { + return new TbResourceDataInfo(data, getDescriptor()); + } + @Override public String toString() { return super.toString(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDataInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDataInfo.java new file mode 100644 index 0000000000..039478470d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDataInfo.java @@ -0,0 +1,31 @@ +/** + * 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.common.data; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TbResourceDataInfo { + + private byte[] data; + private JsonNode descriptor; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java index edc5a2f539..76945a97ed 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TbResourceDeleteResult.java @@ -17,7 +17,6 @@ package org.thingsboard.server.common.data; import lombok.Builder; import lombok.Data; -import org.thingsboard.server.common.data.id.HasId; import java.util.List; import java.util.Map; @@ -27,6 +26,6 @@ import java.util.Map; public class TbResourceDeleteResult { private boolean success; - private Map>> references; + private Map> references; } diff --git a/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java b/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java index 0f1a56cb17..79b8181548 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java +++ b/common/util/src/main/java/org/thingsboard/common/util/DonAsynchron.java @@ -15,12 +15,15 @@ */ package org.thingsboard.common.util; +import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -71,4 +74,16 @@ public class DonAsynchron { return future; } + public static FluentFuture toFluentFuture(CompletableFuture completable) { + SettableFuture future = SettableFuture.create(); + completable.whenComplete((result, exception) -> { + if (exception != null) { + future.setException(exception); + } else { + future.set(result); + } + }); + return FluentFuture.from(future); + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java b/dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java index 6a952fd501..93cc64b2db 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/ResourceContainerDao.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; @@ -22,8 +23,8 @@ import java.util.List; public interface ResourceContainerDao> { - List findByTenantIdAndResourceLink(TenantId tenantId, String link, int limit); + List findByTenantIdAndResource(TenantId tenantId, String reference, int limit); - List findByResourceLink(String link, int limit); + List findByResource(String reference, int limit); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java index 3be7f5b91c..c16e37d30f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseImageService.java @@ -50,6 +50,7 @@ import org.thingsboard.server.dao.ImageContainerDao; import org.thingsboard.server.dao.asset.AssetProfileDao; import org.thingsboard.server.dao.dashboard.DashboardInfoDao; import org.thingsboard.server.dao.device.DeviceProfileDao; +import org.thingsboard.server.dao.rule.RuleChainDao; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.validator.ResourceDataValidator; import org.thingsboard.server.dao.util.ImageUtils; @@ -109,8 +110,8 @@ public class BaseImageService extends BaseResourceService implements ImageServic public BaseImageService(TbResourceDao resourceDao, TbResourceInfoDao resourceInfoDao, ResourceDataValidator resourceValidator, AssetProfileDao assetProfileDao, DeviceProfileDao deviceProfileDao, WidgetsBundleDao widgetsBundleDao, - WidgetTypeDao widgetTypeDao, DashboardInfoDao dashboardInfoDao) { - super(resourceDao, resourceInfoDao, resourceValidator, widgetTypeDao, dashboardInfoDao); + WidgetTypeDao widgetTypeDao, DashboardInfoDao dashboardInfoDao, RuleChainDao ruleChainDao) { + super(resourceDao, resourceInfoDao, resourceValidator, widgetTypeDao, dashboardInfoDao, ruleChainDao); this.assetProfileDao = assetProfileDao; this.deviceProfileDao = deviceProfileDao; this.widgetsBundleDao = widgetsBundleDao; diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java index bf941256f5..7f0dd4a6bf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/BaseResourceService.java @@ -35,11 +35,13 @@ import org.thingsboard.server.cache.resourceInfo.ResourceInfoCacheKey; import org.thingsboard.server.cache.resourceInfo.ResourceInfoEvictEvent; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DataConstants; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ResourceExportData; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDataInfo; import org.thingsboard.server.common.data.TbResourceDeleteResult; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfoFilter; @@ -56,6 +58,7 @@ import org.thingsboard.server.dao.entity.AbstractCachedEntityService; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.rule.RuleChainDao; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.service.validator.ResourceDataValidator; @@ -92,13 +95,16 @@ public class BaseResourceService extends AbstractCachedEntityService> resourceContainerDaoMap = new HashMap<>(); + protected final RuleChainDao ruleChainDao; + private final Map> resourceLinkContainerDaoMap = new HashMap<>(); + private final Map> generalResourceContainerDaoMap = new HashMap<>(); protected static final int MAX_ENTITIES_TO_FIND = 10; @PostConstruct public void init() { - resourceContainerDaoMap.put(EntityType.WIDGET_TYPE, widgetTypeDao); - resourceContainerDaoMap.put(EntityType.DASHBOARD, dashboardInfoDao); + resourceLinkContainerDaoMap.put(EntityType.WIDGET_TYPE, widgetTypeDao); + resourceLinkContainerDaoMap.put(EntityType.DASHBOARD, dashboardInfoDao); + generalResourceContainerDaoMap.put(EntityType.RULE_CHAIN, ruleChainDao); } @Autowired @Lazy @@ -206,6 +212,12 @@ public class BaseResourceService extends AbstractCachedEntityService>> affectedEntities = new HashMap<>(); - - resourceContainerDaoMap.forEach((entityType, resourceContainerDao) -> { - var entities = tenantId.isSysTenantId() ? resourceContainerDao.findByResourceLink(link, MAX_ENTITIES_TO_FIND) : - resourceContainerDao.findByTenantIdAndResourceLink(tenantId, link, MAX_ENTITIES_TO_FIND); - if (!entities.isEmpty()) { - affectedEntities.put(entityType.name(), entities); - } - }); - - if (!affectedEntities.isEmpty()) { - success = false; - result.references(affectedEntities); - } + Map> references = findResourceReferences(tenantId, resource); + if (!references.isEmpty()) { + success = false; + result.references(references); } } if (success) { resourceDao.removeById(tenantId, resourceId.getId()); + publishEvictEvent(new ResourceInfoEvictEvent(tenantId, resourceId)); eventPublisher.publishEvent(DeleteEntityEvent.builder().tenantId(tenantId).entity(resource).entityId(resourceId).build()); } return result.success(success).build(); } + private Map> findResourceReferences(TenantId tenantId, TbResourceInfo resource) { + Map> references = new HashMap<>(); + + if (resource.getResourceType() == ResourceType.JS_MODULE) { + var ref = resource.getLink(); + findReferences(tenantId, references, ref, resourceLinkContainerDaoMap); + } + + if (resource.getResourceType() == ResourceType.GENERAL) { + var ref = resource.getId().getId().toString(); + findReferences(tenantId, references, ref, generalResourceContainerDaoMap); + } + + return references; + } + + private void findReferences(TenantId tenantId, Map> references, String ref, Map> resourceLinkContainerDaoMap) { + resourceLinkContainerDaoMap.forEach((entityType, dao) -> { + List entities = tenantId.isSysTenantId() + ? dao.findByResource(ref, MAX_ENTITIES_TO_FIND) + : dao.findByTenantIdAndResource(tenantId, ref, MAX_ENTITIES_TO_FIND); + if (!entities.isEmpty()) { + references.put(entityType.name(), entities); + } + }); + } + @Override public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { deleteResource(tenantId, (TbResourceId) id, force); @@ -663,6 +691,12 @@ public class BaseResourceService extends AbstractCachedEntityService findSystemOrTenantResourcesByIds(TenantId tenantId, List resourceIds) { + log.trace("Executing findSystemOrTenantResourcesByIds, tenantId [{}], resourceIds [{}]", tenantId, resourceIds); + return resourceInfoDao.findSystemOrTenantResourcesByIds(tenantId, resourceIds); + } + @Override public String calculateEtag(byte[] data) { return Hashing.sha256().hashBytes(data).toString(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/DefaultTbResourceDataCache.java b/dao/src/main/java/org/thingsboard/server/dao/resource/DefaultTbResourceDataCache.java new file mode 100644 index 0000000000..452f86b1a6 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/DefaultTbResourceDataCache.java @@ -0,0 +1,72 @@ +/** + * 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.resource; + +import com.github.benmanes.caffeine.cache.AsyncLoadingCache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.google.common.util.concurrent.FluentFuture; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.thingsboard.common.util.DonAsynchron; +import org.thingsboard.server.common.data.TbResourceDataInfo; +import org.thingsboard.server.common.data.id.TbResourceId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.sql.JpaExecutorService; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +@Service +@RequiredArgsConstructor +@Slf4j +public class DefaultTbResourceDataCache implements TbResourceDataCache { + + private final ResourceService resourceService; + private final JpaExecutorService executorService; + + @Value("${cache.tbResourceData.maxSize:100000}") + private int cacheMaxSize; + @Value("${cache.tbResourceData.timeToLiveInMinutes:44640}") + private int cacheValueTtl; + private AsyncLoadingCache cache; + + @PostConstruct + private void init() { + cache = Caffeine.newBuilder() + .maximumSize(cacheMaxSize) + .expireAfterAccess(cacheValueTtl, TimeUnit.MINUTES) + .executor(executorService) + .buildAsync((key, executor) -> CompletableFuture.supplyAsync(() -> resourceService.getResourceDataInfo(key.tenantId(), key.resourceId()), executor)); + } + + @Override + public FluentFuture getResourceDataInfoAsync(TenantId tenantId, TbResourceId resourceId) { + log.trace("Retrieving resource data info by id [{}], tenant id [{}] from cache", resourceId, tenantId); + return DonAsynchron.toFluentFuture(cache.get(new ResourceDataKey(tenantId, resourceId))); + } + + @Override + public void evictResourceData(TenantId tenantId, TbResourceId resourceId) { + cache.asMap().remove(new ResourceDataKey(tenantId, resourceId)); + log.trace("Evicted resource data info with id [{}], tenant id [{}]", resourceId, tenantId); + } + + record ResourceDataKey (TenantId tenantId, TbResourceId resourceId) {} + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java index 23b59b5658..1b9f250521 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceDao.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.resource; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDataInfo; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -51,4 +52,5 @@ public interface TbResourceDao extends Dao, TenantEntityWithDataDao, long getResourceSize(TenantId tenantId, TbResourceId resourceId); + TbResourceDataInfo getResourceDataInfo(TenantId tenantId, TbResourceId resourceId); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java index 8e97738501..f4fe02843d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/resource/TbResourceInfoDao.java @@ -18,6 +18,7 @@ package org.thingsboard.server.dao.resource; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.TbResourceInfoFilter; +import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -46,4 +47,5 @@ public interface TbResourceInfoDao extends Dao { TbResourceInfo findPublicResourceByKey(ResourceType resourceType, String publicResourceKey); + List findSystemOrTenantResourcesByIds(TenantId tenantId, List resourceIds); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java index 5b09eec42a..ac716bb4fc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/RuleChainDao.java @@ -21,8 +21,10 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainType; +import org.thingsboard.server.common.data.rule.RuleNode; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.ExportableEntityDao; +import org.thingsboard.server.dao.ResourceContainerDao; import org.thingsboard.server.dao.TenantEntityDao; import java.util.Collection; @@ -31,7 +33,7 @@ import java.util.UUID; /** * Created by igor on 3/12/18. */ -public interface RuleChainDao extends Dao, TenantEntityDao, ExportableEntityDao { +public interface RuleChainDao extends Dao, TenantEntityDao, ExportableEntityDao, ResourceContainerDao { /** * Find rule chains by tenantId and page link. diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java index 7624ddc738..32ac596562 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/DashboardInfoRepository.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.dao.model.sql.DashboardInfoEntity; import java.util.List; @@ -87,12 +88,15 @@ public interface DashboardInfoRepository extends JpaRepository findByImageLink(@Param("imageLink") String imageLink, @Param("limit") int limit); - @Query(value = "SELECT * FROM dashboard d WHERE d.tenant_id = :tenantId and d.configuration ILIKE CONCAT('%', :link, '%') limit :limit", - nativeQuery = true) - List findDashboardInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId, @Param("link") String link, @Param("limit") int limit); + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(d.id, 'DASHBOARD', d.title) " + + "FROM DashboardEntity d WHERE d.tenantId = :tenantId AND ilike(cast(d.configuration as string), CONCAT('%', :link, '%')) = true") + List findDashboardInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId, + @Param("link") String link, + Pageable pageable); - @Query(value = "SELECT * FROM dashboard d WHERE d.configuration ILIKE CONCAT('%', :link, '%') limit :limit", - nativeQuery = true) - List findDashboardInfosByResourceLink(@Param("link") String link, @Param("limit") int limit); + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(d.id, 'DASHBOARD', d.title) " + + "FROM DashboardEntity d WHERE ilike(cast(d.configuration as string), CONCAT('%', :link, '%')) = true") + List findDashboardInfosByResourceLink(@Param("link") String link, + Pageable pageable); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java index bc07139725..0e04c94a46 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/dashboard/JpaDashboardInfoDao.java @@ -17,9 +17,11 @@ package org.thingsboard.server.dao.sql.dashboard; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.DashboardInfo; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; @@ -135,13 +137,13 @@ public class JpaDashboardInfoDao extends JpaAbstractDao findByTenantIdAndResourceLink(TenantId tenantId, String url, int limit) { - return DaoUtil.convertDataList(dashboardInfoRepository.findDashboardInfosByTenantIdAndResourceLink(tenantId.getId(), url, limit)); + public List findByTenantIdAndResource(TenantId tenantId, String reference, int limit) { + return dashboardInfoRepository.findDashboardInfosByTenantIdAndResourceLink(tenantId.getId(), reference, PageRequest.of(0, limit)); } @Override - public List findByResourceLink(String link, int limit) { - return DaoUtil.convertDataList(dashboardInfoRepository.findDashboardInfosByResourceLink(link, limit)); + public List findByResource(String reference, int limit) { + return dashboardInfoRepository.findDashboardInfosByResourceLink(reference, PageRequest.of(0, limit)); } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java index 6cce9d76c2..48e41c8553 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/JpaTbResourceDao.java @@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ResourceSubType; import org.thingsboard.server.common.data.ResourceType; import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDataInfo; import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -115,6 +116,11 @@ public class JpaTbResourceDao extends JpaAbstractDao findSystemOrTenantResourcesByIds(TenantId tenantId, List resourceIds) { + return DaoUtil.convertDataList(resourceInfoRepository.findSystemOrTenantResourcesByIdIn(tenantId.getId(), TenantId.NULL_UUID, toUUIDs(resourceIds))); + } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java index 6eea20a287..97b1e56527 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceInfoRepository.java @@ -79,4 +79,10 @@ public interface TbResourceInfoRepository extends JpaRepository findSystemOrTenantResourcesByIdIn(@Param("tenantId") UUID tenantId, + @Param("systemTenantId") UUID systemTenantId, + @Param("resourceIds") List resourceIds); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java index 1c642d2069..4aa699174f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/resource/TbResourceRepository.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.TbResourceDataInfo; import org.thingsboard.server.dao.ExportableEntityRepository; import org.thingsboard.server.dao.model.sql.TbResourceEntity; @@ -101,4 +102,6 @@ public interface TbResourceRepository extends JpaRepository findIdsByTenantId(@Param("tenantId") UUID tenantId, Pageable pageable); + @Query("SELECT new org.thingsboard.server.common.data.TbResourceDataInfo(r.data, r.descriptor) FROM TbResourceEntity r WHERE r.id = :id") + TbResourceDataInfo getDataInfoById(UUID id); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java index 77044d41dc..4a6427a7e5 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleChainDao.java @@ -18,8 +18,10 @@ package org.thingsboard.server.dao.sql.rule; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; +import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.edqs.fields.RuleChainFields; import org.thingsboard.server.common.data.id.RuleChainId; @@ -141,6 +143,16 @@ public class JpaRuleChainDao extends JpaAbstractDao return findRuleChainsByTenantId(tenantId.getId(), pageLink); } + @Override + public List findByTenantIdAndResource(TenantId tenantId, String reference, int limit) { + return ruleChainRepository.findRuleChainsByTenantIdAndResource(tenantId.getId(), reference, PageRequest.of(0, limit)); + } + + @Override + public List findByResource(String reference, int limit) { + return ruleChainRepository.findRuleChainsByResource(reference, PageRequest.of(0, limit)); + } + @Override public List findNextBatch(UUID id, int batchSize) { return ruleChainRepository.findNextBatch(id, Limit.of(batchSize)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java index cfa06caf14..4bf648cbbd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleChainRepository.java @@ -17,10 +17,12 @@ package org.thingsboard.server.dao.sql.rule; import org.springframework.data.domain.Limit; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.edqs.fields.RuleChainFields; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.ExportableEntityRepository; @@ -72,6 +74,19 @@ public interface RuleChainRepository extends JpaRepository findRuleChainsByTenantIdAndResource(@Param("tenantId") UUID tenantId, + @Param("resourceId") String resourceId, + PageRequest of); + + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(rc.id, 'RULE_CHAIN', rc.name) " + + "FROM RuleChainEntity rc WHERE EXISTS " + + "(SELECT 1 FROM RuleNodeEntity rn WHERE rn.ruleChainId = rc.id AND cast(rn.configuration as string) LIKE CONCAT('%', :resourceId, '%'))") + List findRuleChainsByResource(@Param("resourceId") String resourceId, + Pageable pageable); + @Query("SELECT new org.thingsboard.server.common.data.edqs.fields.RuleChainFields(r.id, r.createdTime, r.tenantId," + "r.name, r.version, r.additionalInfo) FROM RuleChainEntity r WHERE r.id > :id ORDER BY r.id") List findNextBatch(@Param("id") UUID id, Limit limit); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java index c728f5d006..18fb544dcb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/JpaWidgetTypeDao.java @@ -17,8 +17,10 @@ package org.thingsboard.server.dao.sql.widget; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Limit; +import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.edqs.fields.WidgetTypeFields; import org.thingsboard.server.common.data.id.TenantId; @@ -269,13 +271,13 @@ public class JpaWidgetTypeDao extends JpaAbstractDao findByTenantIdAndResourceLink(TenantId tenantId, String link, int limit) { - return DaoUtil.convertDataList(widgetTypeInfoRepository.findWidgetTypeInfosByTenantIdAndResourceLink(tenantId.getId(), link, limit)); + public List findByTenantIdAndResource(TenantId tenantId, String reference, int limit) { + return widgetTypeInfoRepository.findWidgetTypeInfosByTenantIdAndResourceLink(tenantId.getId(), reference, PageRequest.of(0, limit)); } @Override - public List findByResourceLink(String link, int limit) { - return DaoUtil.convertDataList(widgetTypeInfoRepository.findWidgetTypeInfosByResourceLink(link, limit)); + public List findByResource(String reference, int limit) { + return widgetTypeInfoRepository.findWidgetTypeInfosByResourceLink(reference, PageRequest.of(0, limit)); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java index dc79280bcf..b97b42a6b9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/widget/WidgetTypeInfoRepository.java @@ -20,6 +20,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.dao.model.sql.WidgetTypeInfoEntity; import java.util.List; @@ -214,10 +215,14 @@ public interface WidgetTypeInfoRepository extends JpaRepository findByImageUrl(@Param("imageLink") String imageLink, @Param("limit") int limit); - @Query(value = "SELECT * FROM widget_type_info_view w WHERE w.tenant_id = :tenantId AND w.descriptor ILIKE CONCAT('%', :link, '%') LIMIT :limit ", nativeQuery = true) - List findWidgetTypeInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId, @Param("link") String link, @Param("limit") int limit); - - @Query(value = "SELECT * FROM widget_type_info_view w WHERE w.descriptor ILIKE CONCAT('%', :link, '%') LIMIT :limit ", nativeQuery = true) - List findWidgetTypeInfosByResourceLink(@Param("link") String link, @Param("limit") int limit); + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(w.id, 'WIDGET_TYPE', w.name) " + + "FROM WidgetTypeEntity w WHERE w.tenantId = :tenantId AND ilike(cast(w.descriptor as string), CONCAT('%', :link, '%')) = true") + List findWidgetTypeInfosByTenantIdAndResourceLink(@Param("tenantId") UUID tenantId, + @Param("link") String link, + Pageable pageable); + @Query("SELECT new org.thingsboard.server.common.data.EntityInfo(w.id, 'WIDGET_TYPE', w.name) " + + "FROM WidgetTypeEntity w WHERE ilike(cast(w.descriptor as string), CONCAT('%', :link, '%')) = true") + List findWidgetTypeInfosByResourceLink(@Param("link") String link, + Pageable pageable); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java index f5aa8a3af5..59c0db9eee 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDaoTest.java @@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index d2687a1b10..920f00ed27 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -24,6 +24,7 @@ import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.asset.Asset; @@ -32,6 +33,7 @@ import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TenantId; @@ -78,6 +80,7 @@ import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.queue.QueueStatsService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.resource.ResourceService; +import org.thingsboard.server.dao.resource.TbResourceDataCache; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.timeseries.TimeseriesService; @@ -252,6 +255,8 @@ public interface TbContext { void checkTenantEntity(EntityId entityId) throws TbNodeException; + & HasTenantId, I extends EntityId> void checkTenantEntity(E entity) throws TbNodeException; + boolean isLocalEntity(EntityId entityId); RuleNodeId getSelfId(); @@ -308,6 +313,8 @@ public interface TbContext { ResourceService getResourceService(); + TbResourceDataCache getTbResourceDataCache(); + OtaPackageService getOtaPackageService(); RuleEngineDeviceProfileCache getDeviceProfileCache(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index 3497795771..bd02089204 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -18,12 +18,20 @@ package org.thingsboard.rule.engine.ai; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.FluentFuture; import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.data.message.Content; +import dev.langchain4j.data.message.ImageContent; +import dev.langchain4j.data.message.PdfFileContent; import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.TextContent; import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.model.chat.request.ChatRequest; import dev.langchain4j.model.chat.request.ResponseFormat; import dev.langchain4j.model.chat.response.ChatResponse; +import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.NonNull; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; @@ -33,24 +41,38 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.rule.engine.external.TbAbstractExternalNode; +import org.thingsboard.server.common.data.GeneralFileDescriptor; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResourceDataInfo; +import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.ai.AiModel; import org.thingsboard.server.common.data.ai.model.AiModelType; import org.thingsboard.server.common.data.ai.model.chat.AiChatModelConfig; import org.thingsboard.server.common.data.id.AiModelId; +import org.thingsboard.server.common.data.id.TbResourceId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.resource.TbResourceDataCache; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Base64; +import java.util.HashSet; import java.util.List; import java.util.NoSuchElementException; +import java.util.Objects; import java.util.Optional; +import java.util.Set; +import java.util.UUID; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbResponseFormatType; import static org.thingsboard.server.dao.service.ConstraintValidator.validateFields; +@Slf4j @RuleNode( type = ComponentType.EXTERNAL, name = "AI request", @@ -77,6 +99,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { private String systemPrompt; private String userPrompt; + private Set resourceIds; private ResponseFormat responseFormat; private int timeoutSeconds; private AiModelId modelId; @@ -111,6 +134,14 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { // LangChain4j AnthropicChatModel rejects requests with non-null ResponseFormat even if ResponseFormatType is TEXT responseFormat = config.getResponseFormat().toLangChainResponseFormat(); } + if (config.getResourceIds() != null && !config.getResourceIds().isEmpty()) { + resourceIds = new HashSet<>(config.getResourceIds().size()); + for (UUID resourceId : config.getResourceIds()) { + TbResourceId tbResourceId = new TbResourceId(resourceId); + validateResource(ctx, tbResourceId); + resourceIds.add(tbResourceId); + } + } systemPrompt = config.getSystemPrompt(); userPrompt = config.getUserPrompt(); @@ -126,12 +157,42 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { @Override public void onMsg(TbContext ctx, TbMsg msg) { var ackedMsg = ackIfNeeded(ctx, msg); + final String processedUserPrompt = TbNodeUtils.processPattern(this.userPrompt, ackedMsg); + + final ListenableFuture userMessageFuture = + resourceIds == null + ? Futures.immediateFuture(UserMessage.from(processedUserPrompt)) + : Futures.transform( + loadResources(ctx), + resources -> UserMessage.from(buildContents(processedUserPrompt, resources)), + ctx.getDbCallbackExecutor() + ); + + Futures.addCallback( + userMessageFuture, + new FutureCallback<>() { + @Override + public void onSuccess(UserMessage userMessage) { + buildAndSendRequest(ctx, ackedMsg, userMessage); + } + @Override + public void onFailure(Throwable t) { + tellFailure(ctx, ackedMsg, t); + } + }, + MoreExecutors.directExecutor() + ); + } + + private void buildAndSendRequest(TbContext ctx, TbMsg ackedMsg, UserMessage userMessage) { List chatMessages = new ArrayList<>(2); - if (systemPrompt != null) { + + if (systemPrompt != null && !systemPrompt.isBlank()) { chatMessages.add(SystemMessage.from(TbNodeUtils.processPattern(systemPrompt, ackedMsg))); } - chatMessages.add(UserMessage.from(TbNodeUtils.processPattern(userPrompt, ackedMsg))); + + chatMessages.add(userMessage); var chatRequest = ChatRequest.builder() .messages(chatMessages) @@ -192,11 +253,67 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { return JacksonUtil.newObjectNode().put("response", response).toString(); } + private void validateResource(TbContext ctx, TbResourceId tbResourceId) throws TbNodeException { + TbResourceInfo resource = ctx.getResourceService().findResourceInfoById(ctx.getTenantId(), tbResourceId); + if (resource == null) { + throw new TbNodeException("[" + ctx.getTenantId() + "] Resource with ID: [" + tbResourceId + "] was not found", true); + } + if (!ResourceType.GENERAL.equals(resource.getResourceType())) { + throw new TbNodeException("[" + ctx.getTenantId() + "] Resource with ID: [" + tbResourceId + "] has unsupported resource type: " + resource.getResourceType(), true); + } + ctx.checkTenantEntity(resource); + } + + private ListenableFuture> loadResources(TbContext ctx) { + final TenantId tenantId = ctx.getTenantId(); + final TbResourceDataCache cache = ctx.getTbResourceDataCache(); + List> futures = resourceIds.stream() + .map(id -> cache.getResourceDataInfoAsync(tenantId, id)) + .toList(); + return Futures.allAsList(futures); + } + + private List buildContents(String userPrompt, List resources) { + List contents = new ArrayList<>(1 + resources.size()); + contents.add(new TextContent(userPrompt)); // user prompt first + + resources.stream() + .filter(Objects::nonNull) + .map(this::toContent) + .forEach(contents::add); + + return contents; + } + + private Content toContent(TbResourceDataInfo resource) { + if (resource.getDescriptor() == null) { + throw new RuntimeException("Missing descriptor for resource"); + } + GeneralFileDescriptor descriptor = JacksonUtil.treeToValue(resource.getDescriptor(), GeneralFileDescriptor.class); + String mediaType = descriptor.getMediaType(); + if (mediaType == null) { + throw new RuntimeException("Missing mediaType in resource descriptor " + resource.getDescriptor()); + } + byte[] data = resource.getData(); + if (mediaType.startsWith("text/")) { + return new TextContent(new String(data, StandardCharsets.UTF_8)); + } + if (mediaType.equals("application/pdf")) { + return new PdfFileContent(Base64.getEncoder().encodeToString(data), mediaType); + } + if (mediaType.startsWith("image/")) { + return new ImageContent(Base64.getEncoder().encodeToString(data), mediaType); + } + log.debug("Trying to create text content for {}", resource.getDescriptor()); + return new TextContent(new String(data, StandardCharsets.UTF_8)); + } + @Override public void destroy() { super.destroy(); systemPrompt = null; userPrompt = null; + resourceIds = null; responseFormat = null; modelId = null; } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java index 48392aa76e..f51983ecb1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNodeConfiguration.java @@ -20,12 +20,14 @@ import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; import lombok.Data; import org.thingsboard.rule.engine.api.NodeConfiguration; import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.validation.Length; +import java.util.Set; +import java.util.UUID; + import static org.thingsboard.rule.engine.ai.TbResponseFormat.TbJsonResponseFormat; @Data @@ -41,6 +43,8 @@ public class TbAiNodeConfiguration implements NodeConfiguration resourceIds; + @NotNull @Valid private TbResponseFormat responseFormat; diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java index f21aa559d4..c5b7f2c44b 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/ai/TbAiNodeTest.java @@ -17,8 +17,11 @@ package org.thingsboard.rule.engine.ai; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.Futures; import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.data.message.ImageContent; import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.TextContent; import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.model.chat.request.ResponseFormat; import dev.langchain4j.model.chat.request.ResponseFormatType; @@ -32,6 +35,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.thingsboard.common.util.JacksonUtil; @@ -43,6 +47,10 @@ import org.thingsboard.rule.engine.api.RuleEngineAiChatModelService; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; +import org.thingsboard.server.common.data.GeneralFileDescriptor; +import org.thingsboard.server.common.data.ResourceType; +import org.thingsboard.server.common.data.TbResource; +import org.thingsboard.server.common.data.TbResourceDataInfo; import org.thingsboard.server.common.data.ai.AiModel; import org.thingsboard.server.common.data.ai.model.AiModelConfig; import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig; @@ -52,6 +60,7 @@ import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; import org.thingsboard.server.common.data.id.AiModelId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.RuleNodeId; +import org.thingsboard.server.common.data.id.TbResourceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.rule.RuleNode; @@ -59,9 +68,14 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.ai.AiModelService; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.resource.ResourceService; +import org.thingsboard.server.dao.resource.TbResourceDataCache; +import java.util.Base64; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.stream.Stream; @@ -76,16 +90,23 @@ import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.never; +import static org.thingsboard.server.common.data.ResourceType.GENERAL; @ExtendWith(MockitoExtension.class) class TbAiNodeTest { + private static final byte[] PNG_IMAGE = Base64.getDecoder().decode("iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAMAAAB+IdObAAAC9FBMVEUAAAABAQEBAgICAgICAwMCBAQDAwMDBQUDBgYEBAQEBwcECAgFCQkFCgoGBgYGCwsGDAwHBwcHDQ0HDg4ICAgIDw8IEBAJCQkJEREKEhIKExMLFBQLFRUMFhYMFxcNDQ0NGBgNGRkODg4OGhoOGxsPDw8PHBwPHR0QEBAQHh4QHx8RERERICARISESEhISIiITExMTIyMTJCQUJSUUJiYVKCgWFhYWKSkXFxcXGhwYGBgYLC0ZGRkaMDEaMTIbGxsbMjMcMzQdNTYfOTogICAgOzwiP0AiQEEjIyMjQkMkQ0QnJycnSEkoS0wpKSkrUFErUVIsLCwvV1gvWFkwWlszMzMzYGE1NTU2NjY3Zmc4aWo5OTk5ams5a2w6Ojo6bG07bm88cXI9cnM9c3Q/dndAQEBAeHlBeXpCQkJCe3xCfH1DQ0NEREREf4FFRUVFgIJGg4VHhYdISEhIhohJh4lLi41LjI5MTExMjpBNj5FNkJJOkpRQUFBQlZdRUVFSUlJTU1NTmpxUVFRUnZ9VVVVVnqBWVlZYpadZWVlZp6laqKpbW1tbqatbqqxcXFxcrK5dra9drrBeXl5er7FfsbNfsrRgs7VhYWFiYmJiuLpjubtku71lvL5lvb9mvsBnwcNowsRpxMZpxcdra2tryctsysxubm5uzc9vb29vz9Fw0dNx0tVy1Ndy1dhz1tlz19p0dHR02Nt02dx12t1229523N93d3d33eB33uF5eXl54eR6enp64+Z65Od75eh75ul8fHx85+p86Ot96ex96u2AgICA7vGA7/KB8fSC8/aD9PeD9fiEhISE9/qF+PuF+fyGhoaG+v2G+/6Hh4eH/P+IiIiMjIyNjY2Ojo6QkJCRkZGSkpKTk5Obm5ucnJyfn5+lpaWnp6eoqKipqamqqqqwsLCzs7O1tbW4uLi5ubm6urq7u7u8vLy/v7/BwcHCwsLFxcXGxsbPz8/Y2Nji4uLj4+Pv7+/4+Pj5+fn+/v7/75T///+GLm1tAAAAAWJLR0T7omo23AAABJtJREFUeNrt3Wd8E3UYB/CH0oqm1dJaS5N0IKu0qQSVinXG4gKlKFi3uMC9FVwoVQnQqCBgBVxFnKCoFFFExFGhliWt/zoYLuIMKEpB7b3xuf9dQu+MvAjXcsTf7/PJk/ul1/S+TS53r3KkNFfk0V6evDHbFGruQ3EQTzNVUFxkHOXFB6QbIQiCIAiC/GeSs/QkR6vkCPeUaNUeSUjkkdR1npCp6a7VV7U6P1dbKfNFrS89rJNas/T6rlZtkUS/i2evhw99Q92y9/r7nVzzw7VfeDX3y2qv893plTVb1uW+uw6xiyNpspAQ8bjLy8l5REiImOlUq3Pniunyxw8Ib+vqF7aB5AgdItLVmit0iOgc9W0owhDt1RSAABL3EGeDDqmXhwRXgw6pj3qESFhtgHC1DYSGrJCQjweFq4SEqzkD67zGah8Inay+p1yl4XqKWt2lF69UDxQrzzevXZprrDn2gfTIUs85Iv/oHpny8HKHdugeVZhpXNudu6u6J1P8lmpIX1ys10X6myVfPeLl919UZFi74JXjWtfCecfa5sj+odx908XSg9Taqdaw+3I1QuYLA6RG2AbiEDpE9JJnvcYP1BRhgiw3QuoAASTuIQnP6JCF8hQlcbYBwrWIKgPDIg9UGSGP2QdCnZ+QkDneKQs4swqe1CDJ09RaXfBUETWKm3a+gFMMEMc0+0AoJVX9nM1+VDsCznLurz64b5VWq7nWLLi81QfygYZfNlU7nAUP0nOwrLnGiiAIgiAIgiAIgiDI/zstLS3tMEtKSiycgAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIAAAggggAACCCCAAAIIIIBYAkEQBEEQBEEQBEEQBGmrdLwuyLmhg703km8Z63k7N2Tw0jnqFt/f0bROn69WBYOfbuxiyR+8MXC9vB8QCBTQkEAgMOG2gVyvDmTzdAWuifFp077m8f503vwZr/PSd28Hg+uaTjVDlOFEIxVrINVijfwi4glCHE1XioXPz6kX9xHNFIUkvyM/xqeduIPHup95bGni8edYotOUqJCrrII0iMv4LnNFg4Sczd/9/Zw4abchD0Ygv0pIBVFZG0Nq587lu/PE02EIXSQuaSfI92l88bfNFkHqLxUnEM1+bXQEMloMY8hgn893esyQIzbzWHtveXn51GW89AtfTeyATWZIWm919s6wBtLYdfXdVCyuuEdCHhoxwr/mAzdDtMQKoaP4duQmRVG+kUtyu83X3OuylX09f+9r0c6eOvkjx82fdPdLiHrdjsrD1Z39LP5W06ExQ475g8eqSR6PZ+oXvLSVNWk/nmmGKNcSXaBYBXEPFkMXV1GlhFyYlSof3t19ZOxfPJp+4/HTeh47JhGdqLQxJDtpyRJxBgUi+0g7QkYSlVsHoVtFrcNiyO0SsoXHDxIykej4v/8F+XxDKLRxmXWQfo2jyGJIh894PDs9FArNeIGXvlwbCn37Upl5rXObOMPtf1K4z5u8ne/sx0tl6hbfgtNkBEGQPZs4uUBwTxoTH5DxtM0TD46+20lpHrfXX7e52/jtyj9kFKbIT2L3FQAAAABJRU5ErkJggg=="); + @Mock TbContext ctxMock; @Mock AiModelService aiModelServiceMock; @Mock RuleEngineAiChatModelService aiChatModelServiceMock; + @Mock + TbResourceDataCache tbResourceDataCacheMock; + @Mock + ResourceService resourceServiceMock; TbAiNode aiNode; TbAiNodeConfiguration config; @@ -141,6 +162,8 @@ class TbAiNodeTest { lenient().when(ctxMock.getAiModelService()).thenReturn(aiModelServiceMock); lenient().when(ctxMock.getAiChatModelService()).thenReturn(aiChatModelServiceMock); lenient().when(ctxMock.getDbCallbackExecutor()).thenReturn(new TestDbCallbackExecutor()); + lenient().when(ctxMock.getTbResourceDataCache()).thenReturn(tbResourceDataCacheMock); + lenient().when(ctxMock.getResourceService()).thenReturn(resourceServiceMock); } @Test @@ -158,6 +181,7 @@ class TbAiNodeTest { assertThat(config.getResponseFormat()).isEqualTo(new TbJsonResponseFormat()); assertThat(config.getTimeoutSeconds()).isEqualTo(60); assertThat(config.isForceAck()).isTrue(); + assertThat(config.getResourceIds()).isNull(); } /* -- Node initialization tests -- */ @@ -373,6 +397,36 @@ class TbAiNodeTest { .matches(e -> ((TbNodeException) e).isUnrecoverable()); } + @Test + void givenNotExistingResources_whenInit_thenThrowsException() { + // GIVEN + config = constructValidConfig(); + UUID resourceId = UUID.randomUUID(); + config.setResourceIds(Set.of(resourceId)); + + // WHEN-THEN + assertThatThrownBy(() -> aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(TbNodeException.class) + .hasMessageContaining("[" + tenantId + "] Resource with ID: [" + resourceId + "] was not found"); + } + + @Test + void givenResourceOfWrongType_whenInit_thenThrowsException() { + // GIVEN + config = constructValidConfig(); + UUID resourceId = UUID.randomUUID(); + config.setResourceIds(Set.of(resourceId)); + + // WHEN-THEN + TbResource tbResource = new TbResource(); + tbResource.setResourceType(ResourceType.DASHBOARD); + given(resourceServiceMock.findResourceInfoById(any(), any())).willReturn(tbResource); + + assertThatThrownBy(() -> aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)))) + .isInstanceOf(TbNodeException.class) + .hasMessageContaining("[" + tenantId + "] Resource with ID: [" + resourceId + "] has unsupported resource type: " + ResourceType.DASHBOARD); + } + /* -- Message processing tests -- */ @Test @@ -560,6 +614,166 @@ class TbAiNodeTest { ); } + @Test + void givenSystemPromptAndUserPromptAndResourcesConfigured_whenOnMsg_thenRequestContainsSystemAndUserAndResourceContent() throws TbNodeException { + String systemPrompt = "Respond with valid JSON"; + String userPrompt = "Tell me a joke"; + String textData = "Text resource content for AI request."; + String xmlData = ""; + + // GIVEN + config = constructValidConfig(); + config.setSystemPrompt(systemPrompt); + config.setUserPrompt(userPrompt); + UUID resourceId = UUID.randomUUID(); + UUID resourceId2 = UUID.randomUUID(); + UUID resourceId3 = UUID.randomUUID(); + + config.setResourceIds(Set.of(resourceId, resourceId2, resourceId3)); + + // WHEN-THEN + TbResource textResource = buildGeneralResource(textData.getBytes(), "text/plain"); + TbResource xmlResource = buildGeneralResource(xmlData.getBytes(), "application/xml"); + TbResource imageResource = buildGeneralResource(PNG_IMAGE, "image/png"); + + given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId)))).willReturn(textResource); + given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId2)))).willReturn(xmlResource); + given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId3)))).willReturn(imageResource); + + given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId)))).willReturn(FluentFuture.from(Futures.immediateFuture(textResource.toResourceDataInfo()))); + given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId2)))).willReturn(FluentFuture.from(Futures.immediateFuture(xmlResource.toResourceDataInfo()))); + given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId3)))).willReturn(FluentFuture.from(Futures.immediateFuture(imageResource.toResourceDataInfo()))); + + aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + var msg = TbMsg.newMsg() + .originator(deviceId) + .data(TbMsg.EMPTY_JSON_OBJECT) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + var chatResponse = ChatResponse.builder() + .aiMessage(AiMessage.from("{\"type\":\"joke\",\"setup\":\"Why did the scarecrow win an award?\",\"punchline\":\"Because he was outstanding in his field.\"}")) + .build(); + + given(aiChatModelServiceMock.sendChatRequestAsync(any(), any())).willReturn(FluentFuture.from(immediateFuture(chatResponse))); + + // WHEN + aiNode.onMsg(ctxMock, msg); + + // THEN + then(aiChatModelServiceMock).should().sendChatRequestAsync(any(), + argThat(actualChatRequest -> { + assertThat(actualChatRequest.messages()).hasSize(2); + assertThat(actualChatRequest.messages().get(0)).isEqualTo(SystemMessage.from(systemPrompt)); + assertThat(((UserMessage)actualChatRequest.messages().get(1)).contents()) + .containsAll(List.of(new TextContent(userPrompt), new TextContent(textData), + new TextContent(xmlData), new ImageContent(Base64.getEncoder().encodeToString(PNG_IMAGE), "image/png"))); + return true; + }) + ); + } + + @Test + void givenNullResource_whenOnMsg_thenRequestContainsSystemAndUserPrompt() throws TbNodeException { + // GIVEN + config = constructValidConfig(); + UUID resourceId = UUID.randomUUID(); + config.setResourceIds(Set.of(resourceId)); + + // WHEN-THEN + TbResource tbResource = buildGeneralResource("Text resource content for AI request.".getBytes(), "text/plain"); + + given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId)))).willReturn(tbResource); + given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId)))).willReturn(FluentFuture.from(Futures.immediateFuture(null))); + + aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + var msg = TbMsg.newMsg() + .originator(deviceId) + .data(TbMsg.EMPTY_JSON_OBJECT) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + aiNode.onMsg(ctxMock, msg); + + // THEN + then(aiChatModelServiceMock).should().sendChatRequestAsync(any(), + argThat(actualChatRequest -> { + assertThat(actualChatRequest.messages()).hasSize(2); + assertThat(actualChatRequest.messages().get(0)).isEqualTo(SystemMessage.from(config.getSystemPrompt())); + assertThat(((UserMessage)actualChatRequest.messages().get(1)).contents()) + .containsAll(List.of(new TextContent(config.getUserPrompt()))); + return true; + }) + ); + } + + @Test + void givenResourceWithNoDescriptor_whenOnMsg_thenEnqueueForTellFailure() throws TbNodeException { + // GIVEN + config = constructValidConfig(); + UUID resourceId = UUID.randomUUID(); + config.setResourceIds(Set.of(resourceId)); + + // WHEN-THEN + TbResource tbResource = buildGeneralResource("Text resource content for AI request.".getBytes(), "text/plain"); + TbResourceDataInfo resourceDataInfo = new TbResourceDataInfo(tbResource.getData(), null); + + given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId)))).willReturn(tbResource); + given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId)))).willReturn(FluentFuture.from(Futures.immediateFuture(resourceDataInfo))); + + aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + var msg = TbMsg.newMsg() + .originator(deviceId) + .data(TbMsg.EMPTY_JSON_OBJECT) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + aiNode.onMsg(ctxMock, msg); + + // THEN + var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class); + then(ctxMock).should().enqueueForTellFailure(any(), exceptionCaptor.capture()); + Throwable actualException = exceptionCaptor.getValue(); + assertThat(actualException.getMessage()).isEqualTo("Missing descriptor for resource"); + } + + @Test + void givenResourceWithNoMediaType_whenOnMsg_thenEnqueueForTellFailure() throws TbNodeException { + // GIVEN + config = constructValidConfig(); + UUID resourceId = UUID.randomUUID(); + config.setResourceIds(Set.of(resourceId)); + + // WHEN-THEN + TbResource tbResource = buildGeneralResource("Text resource content for AI request.".getBytes(), "text/plain"); + TbResourceDataInfo resourceDataInfo = new TbResourceDataInfo(tbResource.getData(), JacksonUtil.newObjectNode()); + + given(resourceServiceMock.findResourceInfoById(any(), eq(new TbResourceId(resourceId)))).willReturn(tbResource); + given(tbResourceDataCacheMock.getResourceDataInfoAsync(any(), eq(new TbResourceId(resourceId)))).willReturn(FluentFuture.from(Futures.immediateFuture(resourceDataInfo))); + + aiNode.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config))); + + var msg = TbMsg.newMsg() + .originator(deviceId) + .data(TbMsg.EMPTY_JSON_OBJECT) + .metaData(TbMsgMetaData.EMPTY) + .build(); + + // WHEN + aiNode.onMsg(ctxMock, msg); + + // THEN + var exceptionCaptor = ArgumentCaptor.forClass(Throwable.class); + then(ctxMock).should().enqueueForTellFailure(any(), exceptionCaptor.capture()); + Throwable actualException = exceptionCaptor.getValue(); + assertThat(actualException.getMessage()).isEqualTo("Missing mediaType in resource descriptor {}"); + } + @Test void givenTemplatedPrompts_whenOnMsg_thenRequestContainsSubstitutedMessages() throws TbNodeException { // GIVEN @@ -950,4 +1164,13 @@ class TbAiNodeTest { then(ctxMock).should(never()).tellFailure(any(), any()); } + private TbResource buildGeneralResource(byte[] data, String mediaType) { + TbResource tbResource = new TbResource(); + tbResource.setResourceType(GENERAL); + GeneralFileDescriptor descriptor = new GeneralFileDescriptor(mediaType); + tbResource.setDescriptorValue(descriptor); + tbResource.setData(data); + return tbResource; + } + } From b9a9348eac7251d7e53b941c0caf6e945dfd82df Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Tue, 2 Sep 2025 14:30:09 +0300 Subject: [PATCH 174/216] UI: Add new resource type text --- ui-ngx/src/app/core/http/entity.service.ts | 8 +- ui-ngx/src/app/core/http/resource.service.ts | 8 +- .../home/components/home-components.module.ts | 6 + .../resources/resources-dialog.component.html | 54 +++++++++ .../resources/resources-dialog.component.scss | 24 ++++ .../resources/resources-dialog.component.ts | 113 ++++++++++++++++++ .../resources-library.component.html | 2 +- .../resources}/resources-library.component.ts | 18 ++- .../external/ai-config.component.html | 10 ++ .../rule-node/external/ai-config.component.ts | 24 ++++ .../modules/home/pages/admin/admin.module.ts | 2 - .../resources-library-table-config.resolve.ts | 2 +- .../resources-table-header.component.ts | 2 +- .../entity/entity-list.component.html | 11 ++ .../entity/entity-list.component.ts | 29 ++++- .../src/app/shared/models/resource.models.ts | 12 +- .../assets/locale/locale.constant-en_US.json | 6 +- 17 files changed, 310 insertions(+), 21 deletions(-) create mode 100644 ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html create mode 100644 ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.scss create mode 100644 ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts rename ui-ngx/src/app/modules/home/{pages/admin/resource => components/resources}/resources-library.component.html (98%) rename ui-ngx/src/app/modules/home/{pages/admin/resource => components/resources}/resources-library.component.ts (89%) diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 572d6cc473..53a55bfe7f 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -100,6 +100,7 @@ import { OAuth2Service } from '@core/http/oauth2.service'; import { MobileAppService } from '@core/http/mobile-app.service'; import { PlatformType } from '@shared/models/oauth2.models'; import { AiModelService } from '@core/http/ai-model.service'; +import { ResourceType } from "@shared/models/resource.models"; @Injectable({ providedIn: 'root' @@ -297,6 +298,11 @@ export class EntityService { (id) => this.ruleChainService.getRuleChain(id, config), entityIds); break; + case EntityType.TB_RESOURCE: + observable = this.getEntitiesByIdsObservable( + (id) => this.resourceService.getResource(id, config), + entityIds); + break; } return observable; } @@ -472,7 +478,7 @@ export class EntityService { break; case EntityType.TB_RESOURCE: pageLink.sortOrder.property = 'title'; - entitiesObservable = this.resourceService.getTenantResources(pageLink, config); + entitiesObservable = this.resourceService.getTenantResources(pageLink, subType as ResourceType, config); break; case EntityType.QUEUE_STATS: pageLink.sortOrder.property = 'createdTime'; diff --git a/ui-ngx/src/app/core/http/resource.service.ts b/ui-ngx/src/app/core/http/resource.service.ts index 615b721b97..52c92d96e4 100644 --- a/ui-ngx/src/app/core/http/resource.service.ts +++ b/ui-ngx/src/app/core/http/resource.service.ts @@ -47,8 +47,12 @@ export class ResourceService { return this.http.get>(url, defaultHttpOptionsFromConfig(config)); } - public getTenantResources(pageLink: PageLink, config?: RequestConfig): Observable> { - return this.http.get>(`/api/resource/tenant${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + public getTenantResources(pageLink: PageLink, resourceType?: ResourceType, config?: RequestConfig): Observable> { + let url = `/api/resource${pageLink.toQuery()}`; + if (isNotEmptyStr(resourceType)) { + url += `&resourceType=${resourceType}`; + } + return this.http.get>(url, defaultHttpOptionsFromConfig(config)); } public getResource(resourceId: string, config?: RequestConfig): Observable { diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts index 31a3066edf..060f804cdf 100644 --- a/ui-ngx/src/app/modules/home/components/home-components.module.ts +++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts @@ -205,6 +205,8 @@ import { } from '@home/components/calculated-fields/components/test-arguments/calculated-field-test-arguments.component'; import { CheckConnectivityDialogComponent } from '@home/components/ai-model/check-connectivity-dialog.component'; import { AIModelDialogComponent } from '@home/components/ai-model/ai-model-dialog.component'; +import { ResourcesDialogComponent } from "@home/components/resources/resources-dialog.component"; +import { ResourcesLibraryComponent } from "@home/components/resources/resources-library.component"; @NgModule({ declarations: @@ -358,6 +360,8 @@ import { AIModelDialogComponent } from '@home/components/ai-model/ai-model-dialo CalculatedFieldTestArgumentsComponent, CheckConnectivityDialogComponent, AIModelDialogComponent, + ResourcesDialogComponent, + ResourcesLibraryComponent, ], imports: [ CommonModule, @@ -505,6 +509,8 @@ import { AIModelDialogComponent } from '@home/components/ai-model/ai-model-dialo CalculatedFieldTestArgumentsComponent, CheckConnectivityDialogComponent, AIModelDialogComponent, + ResourcesDialogComponent, + ResourcesLibraryComponent, ], providers: [ WidgetComponentService, diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html new file mode 100644 index 0000000000..c6813ff0f2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html @@ -0,0 +1,54 @@ + +
+ +

{{ 'resource.add' | translate }}

+ + +
+ + +
+
+ + +
+
+ + +
+ diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.scss b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.scss new file mode 100644 index 0000000000..b32c3933c5 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.scss @@ -0,0 +1,24 @@ +/** + * 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. + */ + +:host ::ng-deep { + .mat-mdc-dialog-content { + display: flex; + flex-direction: column; + height: 100%; + padding: 0 !important; + } +} diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts new file mode 100644 index 0000000000..a06c72827a --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts @@ -0,0 +1,113 @@ +/// +/// 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. +/// + +import { AfterViewInit, Component, Inject, SkipSelf, ViewChild } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { Router } from '@angular/router'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { FormGroupDirective, NgForm, UntypedFormControl } from '@angular/forms'; +import { EntityType } from '@shared/models/entity-type.models'; +import { map } from 'rxjs/operators'; +import { ResourcesLibraryComponent } from "@home/components/resources/resources-library.component"; +import { ErrorStateMatcher } from "@angular/material/core"; +import { Resource, ResourceType } from "@shared/models/resource.models"; +import { ResourceService } from "@core/http/resource.service"; + +export interface ResourcesDialogData { + resources?: Resource; + isAdd?: boolean; +} + +@Component({ + selector: 'tb-resources-dialog', + templateUrl: './resources-dialog.component.html', + providers: [{provide: ErrorStateMatcher, useExisting: ResourcesDialogComponent}], + styleUrls: ['./resources-dialog.component.scss'] +}) +export class ResourcesDialogComponent extends DialogComponent implements ErrorStateMatcher, AfterViewInit { + + readonly entityType = EntityType; + + ResourceType = ResourceType; + + isAdd = false; + + submitted = false; + + resources: Resource; + + @ViewChild('resourcesComponent', {static: true}) resourcesComponent: ResourcesLibraryComponent; + + constructor(protected store: Store, + protected router: Router, + protected dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: ResourcesDialogData, + @SkipSelf() private errorStateMatcher: ErrorStateMatcher, + private resourceService: ResourceService) { + super(store, router, dialogRef); + + if (this.data.isAdd) { + this.isAdd = true; + } + + if (this.data.resources) { + this.resources = this.data.resources; + } + } + + ngAfterViewInit(): void { + if (this.isAdd) { + setTimeout(() => { + this.resourcesComponent.entityForm.markAsDirty(); + }, 0); + } + } + + isErrorState(control: UntypedFormControl | null, form: FormGroupDirective | NgForm | null): boolean { + const originalErrorState = this.errorStateMatcher.isErrorState(control, form); + const customErrorState = !!(control && control.invalid && this.submitted); + return originalErrorState || customErrorState; + } + + cancel(): void { + this.dialogRef.close(null); + } + + save(): void { + this.submitted = true; + if (this.resourcesComponent.entityForm.valid) { + const resource = {...this.resourcesComponent.entityFormValue()}; + if (Array.isArray(resource.data)) { + const resources = []; + resource.data.forEach((data, index) => { + resources.push({ + resourceType: resource.resourceType, + data, + fileName: resource.fileName[index], + title: resource.title + }); + }); + this.resourceService.saveResources(resources, {resendRequest: true}).pipe( + map((response) => response[0]) + ).subscribe(result => this.dialogRef.close(result)); + } else { + this.resourceService.saveResource(resource).subscribe(result => this.dialogRef.close(result)); + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html similarity index 98% rename from ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html rename to ui-ngx/src/app/modules/home/components/resources/resources-library.component.html index ebb946ccbc..c602906be1 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library.component.html +++ b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts index bfba25afa1..a4d973e998 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts @@ -24,6 +24,8 @@ import { AiModel, AiRuleNodeResponseFormatTypeOnlyText, ResponseFormat } from '@ import { deepTrim } from '@core/utils'; import { TranslateService } from '@ngx-translate/core'; import { jsonRequired } from '@shared/components/json-object-edit.component'; +import { Resource, ResourceType } from "@shared/models/resource.models"; +import { ResourcesDialogComponent, ResourcesDialogData } from "@home/components/resources/resources-dialog.component"; @Component({ selector: 'tb-external-node-ai-config', @@ -38,6 +40,9 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { responseFormat = ResponseFormat; + EntityType = EntityType; + ResourceType = ResourceType; + constructor(private fb: UntypedFormBuilder, private translate: TranslateService, private dialog: MatDialog) { @@ -53,6 +58,7 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { modelId: [configuration?.modelId ?? null, [Validators.required]], systemPrompt: [configuration?.systemPrompt ?? '', [Validators.maxLength(500_000), Validators.pattern(/.*\S.*/)]], userPrompt: [configuration?.userPrompt ?? '', [Validators.required, Validators.maxLength(500_000), Validators.pattern(/.*\S.*/)]], + resourceIds: [configuration?.resourceIds ?? []], responseFormat: this.fb.group({ type: [configuration?.responseFormat?.type ?? ResponseFormat.JSON, []], schema: [configuration?.responseFormat?.schema ?? null, [jsonRequired]], @@ -116,5 +122,23 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { this.aiConfigForm.get(formControl).markAsDirty(); } }); + }; + + createAiResources(name: string, formControl: string) { + this.dialog.open(ResourcesDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + resources: {title: name, resourceType: ResourceType.TEXT}, + isAdd: true + } + }).afterClosed() + .subscribe((resource) => { + if (resource) { + const resourceIds = [...(this.aiConfigForm.get(formControl).value || []), resource.id.id]; + this.aiConfigForm.get(formControl).patchValue(resourceIds); + this.aiConfigForm.get(formControl).markAsDirty(); + } + }); } } diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts index 10721ade5a..60790edd74 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts @@ -26,7 +26,6 @@ import { HomeComponentsModule } from '@modules/home/components/home-components.m import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component'; import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component'; import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component'; -import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-library.component'; import { ResourceTabsComponent } from '@home/pages/admin/resource/resource-tabs.component'; import { ResourcesTableHeaderComponent } from '@home/pages/admin/resource/resources-table-header.component'; import { QueueComponent } from '@home/pages/admin/queue/queue.component'; @@ -49,7 +48,6 @@ import { ResourceLibraryTabsComponent } from '@home/pages/admin/resource/resourc SendTestSmsDialogComponent, SecuritySettingsComponent, HomeSettingsComponent, - ResourcesLibraryComponent, ResourceTabsComponent, ResourceLibraryTabsComponent, ResourcesTableHeaderComponent, diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts index f39ed9ff7b..dc85ca4914 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts @@ -32,7 +32,7 @@ import { getCurrentAuthUser } from '@core/auth/auth.selectors'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { Authority } from '@shared/models/authority.enum'; -import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-library.component'; +import { ResourcesLibraryComponent } from '@home/components/resources/resources-library.component'; import { PageLink } from '@shared/models/page/page-link'; import { EntityAction } from '@home/models/entity/entity-component.models'; import { map } from 'rxjs/operators'; diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts index 80c760c6ca..d4b5d18493 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts @@ -28,7 +28,7 @@ import { PageLink } from '@shared/models/page/page-link'; }) export class ResourcesTableHeaderComponent extends EntityTableHeaderComponent { - readonly resourceTypes = [ResourceType.LWM2M_MODEL, ResourceType.PKCS_12, ResourceType.JKS]; + readonly resourceTypes = [ResourceType.LWM2M_MODEL, ResourceType.PKCS_12, ResourceType.JKS, ResourceType.TEXT]; readonly resourceTypesTranslationMap = ResourceTypeTranslationMap; constructor(protected store: Store) { diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.html b/ui-ngx/src/app/shared/components/entity/entity-list.component.html index 6bf7cdb78d..e0e7dfe3f7 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.html @@ -40,6 +40,12 @@ [matAutocompleteConnectedTo]="origin" [matAutocomplete]="entityAutocomplete" [matChipInputFor]="chipList"> + {{ 'entity.no-entities-matching' | translate: {entity: searchText} }} + @if (allowCreateNew) { + + entity.create-new-key + + }
diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts index 552c4f1f71..9d1de180e9 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts @@ -14,7 +14,18 @@ /// limitations under the License. /// -import { Component, ElementRef, forwardRef, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; +import { + Component, + ElementRef, + EventEmitter, + forwardRef, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, + ViewChild +} from '@angular/core'; import { ControlValueAccessor, NG_VALIDATORS, @@ -93,6 +104,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, OnChan } @Input() + @coerceBoolean() disabled: boolean; @Input() @@ -109,6 +121,13 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, OnChan @coerceBoolean() inlineField: boolean; + @Input() + @coerceBoolean() + allowCreateNew: boolean; + + @Output() + createNew = new EventEmitter(); + @ViewChild('entityInput') entityInput: ElementRef; @ViewChild('entityAutocomplete') matAutocomplete: MatAutocomplete; @ViewChild('chipList', {static: true}) chipList: MatChipGrid; @@ -136,6 +155,11 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, OnChan this.entityListFormGroup.get('entities').updateValueAndValidity(); } + createNewEntity($event: Event, searchText?: string) { + $event.stopPropagation(); + this.createNew.emit(searchText); + } + registerOnChange(fn: any): void { this.propagateChange = fn; } @@ -201,6 +225,9 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, OnChan this.modelValue = null; } this.dirty = true; + if (this.entityInput) { + this.entityInput.nativeElement.value = ''; + } } validate(): ValidationErrors | null { diff --git a/ui-ngx/src/app/shared/models/resource.models.ts b/ui-ngx/src/app/shared/models/resource.models.ts index 0419e40a7c..3495bf9eac 100644 --- a/ui-ngx/src/app/shared/models/resource.models.ts +++ b/ui-ngx/src/app/shared/models/resource.models.ts @@ -24,7 +24,8 @@ export enum ResourceType { LWM2M_MODEL = 'LWM2M_MODEL', PKCS_12 = 'PKCS_12', JKS = 'JKS', - JS_MODULE = 'JS_MODULE' + JS_MODULE = 'JS_MODULE', + TEXT = 'TEXT', } export enum ResourceSubType { @@ -57,7 +58,8 @@ export const ResourceTypeTranslationMap = new Map( [ResourceType.LWM2M_MODEL, 'resource.type.lwm2m-model'], [ResourceType.PKCS_12, 'resource.type.pkcs-12'], [ResourceType.JKS, 'resource.type.jks'], - [ResourceType.JS_MODULE, 'resource.type.js-module'] + [ResourceType.JS_MODULE, 'resource.type.js-module'], + [ResourceType.TEXT, 'resource.type.text'], ] ); @@ -76,8 +78,8 @@ export interface TbResourceInfo extends Omit, 'name' | title?: string; resourceType: ResourceType; resourceSubType?: ResourceSubType; - fileName: string; - public: boolean; + fileName?: string; + public?: boolean; publicResourceKey?: string; readonly link?: string; readonly publicLink?: string; @@ -87,7 +89,7 @@ export interface TbResourceInfo extends Omit, 'name' | export type ResourceInfo = TbResourceInfo; export interface Resource extends ResourceInfo { - data: string; + data?: string; name?: string; } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 80328181f6..d4bfd4ea06 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -4488,7 +4488,8 @@ "jks": "JKS", "js-module": "JS module", "lwm2m-model": "LWM2M model", - "pkcs-12": "PKCS #12" + "pkcs-12": "PKCS #12", + "text": "Text" }, "resource-sub-type": "Sub-type", "sub-type": { @@ -5467,7 +5468,8 @@ "timeout-required": "Timeout is required", "timeout-validation": "Must be from 1 second to 10 minutes.", "force-acknowledgement": "Force acknowledgement", - "force-acknowledgement-hint": "If enabled, the incoming message is acknowledged immediately. The model's response is then enqueued as a separate, new message." + "force-acknowledgement-hint": "If enabled, the incoming message is acknowledged immediately. The model's response is then enqueued as a separate, new message.", + "ai-resources": "AI resources" } }, "timezone": { From 055a1ae56d0361e9356a2a08bc394cfa20f04473 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 10 Sep 2025 09:41:47 +0300 Subject: [PATCH 175/216] UI: General resources --- ui-ngx/src/app/core/http/entity.service.ts | 6 +- ui-ngx/src/app/core/http/resource.service.ts | 14 +++-- .../resources/resources-dialog.component.html | 4 +- .../resources/resources-dialog.component.ts | 3 + .../resources-library.component.html | 61 ++++++++++--------- .../resources/resources-library.component.ts | 13 +++- .../external/ai-config.component.html | 2 +- .../rule-node/external/ai-config.component.ts | 2 +- .../resources-table-header.component.ts | 2 +- .../shared/components/file-input.component.ts | 14 ++++- .../src/app/shared/models/resource.models.ts | 4 +- .../assets/locale/locale.constant-en_US.json | 2 +- 12 files changed, 77 insertions(+), 50 deletions(-) diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index 53a55bfe7f..c652ba39ba 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -299,9 +299,7 @@ export class EntityService { entityIds); break; case EntityType.TB_RESOURCE: - observable = this.getEntitiesByIdsObservable( - (id) => this.resourceService.getResource(id, config), - entityIds); + observable = this.resourceService.getResourcesByIds(entityIds, config); break; } return observable; @@ -478,7 +476,7 @@ export class EntityService { break; case EntityType.TB_RESOURCE: pageLink.sortOrder.property = 'title'; - entitiesObservable = this.resourceService.getTenantResources(pageLink, subType as ResourceType, config); + entitiesObservable = this.resourceService.getResources(pageLink, subType as ResourceType, null, config); break; case EntityType.QUEUE_STATS: pageLink.sortOrder.property = 'createdTime'; diff --git a/ui-ngx/src/app/core/http/resource.service.ts b/ui-ngx/src/app/core/http/resource.service.ts index 52c92d96e4..90335758dd 100644 --- a/ui-ngx/src/app/core/http/resource.service.ts +++ b/ui-ngx/src/app/core/http/resource.service.ts @@ -24,6 +24,7 @@ import { Resource, ResourceInfo, ResourceSubType, ResourceType, TBResourceScope import { catchError, mergeMap } from 'rxjs/operators'; import { isNotEmptyStr } from '@core/utils'; import { ResourcesService } from '@core/services/resources.service'; +import { NotificationTarget } from "@shared/models/notification.models"; @Injectable({ providedIn: 'root' @@ -47,12 +48,8 @@ export class ResourceService { return this.http.get>(url, defaultHttpOptionsFromConfig(config)); } - public getTenantResources(pageLink: PageLink, resourceType?: ResourceType, config?: RequestConfig): Observable> { - let url = `/api/resource${pageLink.toQuery()}`; - if (isNotEmptyStr(resourceType)) { - url += `&resourceType=${resourceType}`; - } - return this.http.get>(url, defaultHttpOptionsFromConfig(config)); + public getTenantResources(pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/resource/tenant${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)) } public getResource(resourceId: string, config?: RequestConfig): Observable { @@ -98,4 +95,9 @@ export class ResourceService { return this.http.delete(`/api/resource/${resourceId}?force=${force}`, defaultHttpOptionsFromConfig(config)); } + public getResourcesByIds(ids: string[], config?: RequestConfig): Observable> { + return this.http.get>(`/api/resource?resourceIds=${ids.join(',')}`, + defaultHttpOptionsFromConfig(config)); + } + } diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html index c6813ff0f2..0062cfa9ac 100644 --- a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.html @@ -32,8 +32,8 @@
diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts index a06c72827a..6216f087b1 100644 --- a/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/resources/resources-dialog.component.ts @@ -106,6 +106,9 @@ export class ResourcesDialogComponent extends DialogComponent response[0]) ).subscribe(result => this.dialogRef.close(result)); } else { + if (resource.resourceType !== ResourceType.GENERAL) { + delete resource.descriptor; + } this.resourceService.saveResource(resource).subscribe(result => this.dialogRef.close(result)); } } diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html index c602906be1..4737b75ef2 100644 --- a/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html +++ b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html @@ -48,14 +48,16 @@
- - resource.resource-type - - - {{ resourceTypesTranslationMap.get(resourceType) | translate }} - - - + @if (resourceTypes.length > 1) { + + resource.resource-type + + + {{ resourceTypesTranslationMap.get(resourceType) | translate }} + + + + } resource.title @@ -66,26 +68,29 @@ {{ 'resource.title-max-length' | translate }} - - -
- - resource.file-name - - -
+ @if (isAdd || ((isAdd || isEdit) && entityForm.get('resourceType').value === resourceType.GENERAL)) { + + + } @else { +
+ + resource.file-name + + +
+ }
diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts index 0b50e45a7a..e3ad0e15f3 100644 --- a/ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts +++ b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts @@ -44,7 +44,7 @@ export class ResourcesLibraryComponent extends EntityComponent impleme standalone = false; @Input() - resourceTypes = [ResourceType.LWM2M_MODEL, ResourceType.PKCS_12, ResourceType.JKS, ResourceType.TEXT]; + resourceTypes = [ResourceType.LWM2M_MODEL, ResourceType.PKCS_12, ResourceType.JKS, ResourceType.GENERAL]; @Input() defaultResourceType = ResourceType.LWM2M_MODEL; @@ -90,10 +90,19 @@ export class ResourcesLibraryComponent extends EntityComponent impleme title: [entity ? entity.title : '', [Validators.required, Validators.maxLength(255)]], resourceType: [entity?.resourceType ? entity.resourceType : ResourceType.LWM2M_MODEL, Validators.required], fileName: [entity ? entity.fileName : null, Validators.required], - data: [entity ? entity.data : null, this.isAdd ? [Validators.required] : []] + data: [entity ? entity.data : null, this.isAdd ? [Validators.required] : []], + descriptor: this.fb.group({ + mediaType: [''] + }) }); } + mediaTypeChange(mediaType: string): void { + if (this.entityForm.get('resourceType').value === ResourceType.GENERAL) { + this.entityForm.get('descriptor').get('mediaType').patchValue(mediaType); + } + } + updateForm(entity: Resource): void { this.entityForm.patchValue(entity); } diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html index d259d57ef3..5381fc770b 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.html @@ -76,7 +76,7 @@ placeholderText="{{ 'rule-node-config.ai.ai-resources' | translate }}" [inlineField]="true" [entityType]="EntityType.TB_RESOURCE" - [subType]="ResourceType.TEXT" + [subType]="ResourceType.GENERAL" (createNew)="createAiResources($event, 'resourceIds')" formControlName="resourceIds"> diff --git a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts index a4d973e998..07bcb86fdf 100644 --- a/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/rule-node/external/ai-config.component.ts @@ -129,7 +129,7 @@ export class AiConfigComponent extends RuleNodeConfigurationComponent { disableClose: true, panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], data: { - resources: {title: name, resourceType: ResourceType.TEXT}, + resources: {title: name, resourceType: ResourceType.GENERAL}, isAdd: true } }).afterClosed() diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts index d4b5d18493..136c143e3c 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-table-header.component.ts @@ -28,7 +28,7 @@ import { PageLink } from '@shared/models/page/page-link'; }) export class ResourcesTableHeaderComponent extends EntityTableHeaderComponent { - readonly resourceTypes = [ResourceType.LWM2M_MODEL, ResourceType.PKCS_12, ResourceType.JKS, ResourceType.TEXT]; + readonly resourceTypes = [ResourceType.LWM2M_MODEL, ResourceType.PKCS_12, ResourceType.JKS, ResourceType.GENERAL]; readonly resourceTypesTranslationMap = ResourceTypeTranslationMap; constructor(protected store: Store) { diff --git a/ui-ngx/src/app/shared/components/file-input.component.ts b/ui-ngx/src/app/shared/components/file-input.component.ts index bbe68bb9c6..6960db73dd 100644 --- a/ui-ngx/src/app/shared/components/file-input.component.ts +++ b/ui-ngx/src/app/shared/components/file-input.component.ts @@ -129,10 +129,15 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, @Output() fileNameChanged = new EventEmitter(); + @Output() + mediaTypeChanged = new EventEmitter(); + fileName: string | string[]; fileContent: any; files: File[]; + mediaType: string; + @ViewChild('flow', {static: true}) flow: FlowDirective; @@ -180,6 +185,7 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, this.fileContent = files[0].fileContent; this.fileName = files[0].fileName; this.files = files[0].files; + this.mediaType = files[0].mediaType; this.updateModel(); } else if (files.length > 1) { this.fileContent = files.map(content => content.fileContent); @@ -203,6 +209,7 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, let fileName = null; let fileContent = null; let files = null; + let mediaType = null; if (reader.readyState === reader.DONE) { if (!this.workFromFileObj) { fileContent = reader.result; @@ -211,16 +218,18 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, fileContent = this.contentConvertFunction(fileContent); } fileName = fileContent ? file.name : null; + mediaType = file?.file?.type || null; } } else if (file.name || file.file){ files = file.file; fileName = file.name; + mediaType = file.file.type || null; } } - resolve({fileContent, fileName, files}); + resolve({fileContent, fileName, files, mediaType}); }; reader.onerror = () => { - resolve({fileContent: null, fileName: null, files: null}); + resolve({fileContent: null, fileName: null, files: null, mediaType: null}); }; if (this.readAsBinary) { reader.readAsBinaryString(file.file); @@ -283,6 +292,7 @@ export class FileInputComponent extends PageComponent implements AfterViewInit, this.propagateChange(this.files); } else { this.propagateChange(this.fileContent); + this.mediaTypeChanged.emit(this.mediaType); this.fileNameChanged.emit(this.fileName); } } diff --git a/ui-ngx/src/app/shared/models/resource.models.ts b/ui-ngx/src/app/shared/models/resource.models.ts index 3495bf9eac..12590e7608 100644 --- a/ui-ngx/src/app/shared/models/resource.models.ts +++ b/ui-ngx/src/app/shared/models/resource.models.ts @@ -25,7 +25,7 @@ export enum ResourceType { PKCS_12 = 'PKCS_12', JKS = 'JKS', JS_MODULE = 'JS_MODULE', - TEXT = 'TEXT', + GENERAL = 'GENERAL', } export enum ResourceSubType { @@ -59,7 +59,7 @@ export const ResourceTypeTranslationMap = new Map( [ResourceType.PKCS_12, 'resource.type.pkcs-12'], [ResourceType.JKS, 'resource.type.jks'], [ResourceType.JS_MODULE, 'resource.type.js-module'], - [ResourceType.TEXT, 'resource.type.text'], + [ResourceType.GENERAL, 'resource.type.general'], ] ); diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index d4bfd4ea06..d79bb81ae5 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -4489,7 +4489,7 @@ "js-module": "JS module", "lwm2m-model": "LWM2M model", "pkcs-12": "PKCS #12", - "text": "Text" + "general": "General" }, "resource-sub-type": "Sub-type", "sub-type": { From 8a561456b40fba456e0cac5649f0492e0b3b908a Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Fri, 12 Sep 2025 16:23:53 +0300 Subject: [PATCH 176/216] UI: Add resources in use dialog with force to delete --- .../resources-library-table-config.resolve.ts | 180 +++++++++++++++++- .../assets/locale/locale.constant-en_US.json | 7 +- 2 files changed, 177 insertions(+), 10 deletions(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts index dc85ca4914..d92355fbac 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts @@ -22,7 +22,13 @@ import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; import { Router } from '@angular/router'; -import { Resource, ResourceInfo, ResourceType, ResourceTypeTranslationMap } from '@shared/models/resource.models'; +import { + Resource, + ResourceInfo, ResourceInfoWithReferences, + ResourceType, + ResourceTypeTranslationMap, + toResourceDeleteResult +} from '@shared/models/resource.models'; import { EntityType, entityTypeResources, entityTypeTranslations } from '@shared/models/entity-type.models'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { DatePipe } from '@angular/common'; @@ -35,9 +41,19 @@ import { Authority } from '@shared/models/authority.enum'; import { ResourcesLibraryComponent } from '@home/components/resources/resources-library.component'; import { PageLink } from '@shared/models/page/page-link'; import { EntityAction } from '@home/models/entity/entity-component.models'; -import { map } from 'rxjs/operators'; +import { catchError, map } from 'rxjs/operators'; import { ResourcesTableHeaderComponent } from '@home/pages/admin/resource/resources-table-header.component'; import { ResourceLibraryTabsComponent } from '@home/pages/admin/resource/resource-library-tabs.component'; +import { forkJoin, of } from "rxjs"; +import { + ResourcesInUseDialogComponent, + ResourcesInUseDialogData +} from "@shared/components/resource/resources-in-use-dialog.component"; +import { parseHttpErrorMessage } from "@core/utils"; +import { ActionNotificationShow } from "@core/notification/notification.actions"; +import { ResourcesDatasource } from "@home/pages/admin/resource/resources-datasource"; +import { MatDialog } from "@angular/material/dialog"; +import { DialogService } from "@core/services/dialog.service"; @Injectable() export class ResourcesLibraryTableConfigResolver { @@ -49,6 +65,8 @@ export class ResourcesLibraryTableConfigResolver { private resourceService: ResourceService, private translate: TranslateService, private router: Router, + private dialog: MatDialog, + private dialogService: DialogService, private datePipe: DatePipe) { this.config.entityType = EntityType.TB_RESOURCE; @@ -76,19 +94,27 @@ export class ResourcesLibraryTableConfigResolver { icon: 'file_download', isEnabled: () => true, onAction: ($event, entity) => this.downloadResource($event, entity) - } + }, + { + name: this.translate.instant('resource.delete'), + icon: 'delete', + isEnabled: (resource) => this.config.deleteEnabled(resource), + onAction: ($event, entity) => this.deleteResource($event, entity) + }, ); - this.config.deleteEntityTitle = resource => this.translate.instant('resource.delete-resource-title', - { resourceTitle: resource.title }); - this.config.deleteEntityContent = () => this.translate.instant('resource.delete-resource-text'); - this.config.deleteEntitiesTitle = count => this.translate.instant('resource.delete-resources-title', {count}); - this.config.deleteEntitiesContent = () => this.translate.instant('resource.delete-resources-text'); + this.config.groupActionDescriptors = [{ + name: this.translate.instant('action.delete'), + icon: 'delete', + isEnabled: true, + onAction: ($event, entities) => this.deleteResources($event, entities) + }]; + + this.config.entitiesDeleteEnabled = false; this.config.entitiesFetchFunction = pageLink => this.resourceService.getResources(pageLink, this.config.componentsData.resourceType); this.config.loadEntity = id => this.resourceService.getResourceInfoById(id.id); this.config.saveEntity = resource => this.saveResource(resource); - this.config.deleteEntity = id => this.resourceService.deleteResource(id.id); this.config.onEntityAction = action => this.onResourceAction(action); } @@ -147,6 +173,8 @@ export class ResourcesLibraryTableConfigResolver { case 'downloadResource': this.downloadResource(action.event, action.entity); return true; + case 'deleteLibrary': + this.deleteResource(action.event, action.entity); } return false; } @@ -165,4 +193,138 @@ export class ResourcesLibraryTableConfigResolver { return authority === Authority.SYS_ADMIN; } } + + private deleteResource($event: Event, resource: ResourceInfo) { + if ($event) { + $event.stopPropagation(); + } + this.dialogService.confirm( + this.translate.instant('resource.delete-resource-title', { resourceTitle: resource.title }), + this.translate.instant('resource.delete-resource-text'), + this.translate.instant('action.no'), + this.translate.instant('action.yes'), + true + ).subscribe((result) => { + if (result) { + this.resourceService.deleteResource(resource.id.id, false, {ignoreErrors: true}).pipe( + map(() => toResourceDeleteResult(resource)), + catchError((err) => of(toResourceDeleteResult(resource, err))) + ).subscribe( + (deleteResult) => { + if (deleteResult.success) { + if (this.config.getEntityDetailsPage()) { + this.config.getEntityDetailsPage().goBack(); + } else { + this.config.updateData(true); + } + } else if (deleteResult.resourceIsReferencedError) { + const resources: ResourceInfoWithReferences[] = [{...resource, ...{references: deleteResult.references}}]; + const data = { + multiple: false, + resources, + configuration: { + title: 'resource.resource-is-in-use', + message: this.translate.instant('resource.resource-is-in-use-text', {title: resources[0].title}), + deleteText: 'resource.delete-resource-in-use-text', + selectedText: 'resource.selected-resources', + columns: ['select', 'title', 'references'] + } + }; + this.dialog.open(ResourcesInUseDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data + }).afterClosed().subscribe((resources) => { + if (resources) { + this.resourceService.deleteResource(resource.id.id, true).subscribe(() => { + if (this.config.getEntityDetailsPage()) { + this.config.getEntityDetailsPage().goBack(); + } else { + this.config.updateData(true); + } + }); + } + }); + } else { + const errorMessageWithTimeout = parseHttpErrorMessage(deleteResult.error, this.translate); + setTimeout(() => { + this.store.dispatch(new ActionNotificationShow({message: errorMessageWithTimeout.message, type: 'error'})); + }, errorMessageWithTimeout.timeout); + } + } + ); + } + }); + } + + private deleteResources($event: Event, resources: ResourceInfo[]) { + if ($event) { + $event.stopPropagation(); + } + if (resources && resources.length) { + const title = this.translate.instant('resource.delete-resources-title', {count: resources.length}); + const content = this.translate.instant('resource.delete-resources-text'); + this.dialogService.confirm(title, content, + this.translate.instant('action.no'), + this.translate.instant('action.yes')).subscribe((result) => { + if (result) { + const tasks = resources.map((resource) => + this.resourceService.deleteResource(resource.id.id, false, {ignoreErrors: true}).pipe( + map(() => toResourceDeleteResult(resource)), + catchError((err) => of(toResourceDeleteResult(resource, err))) + ) + ); + forkJoin(tasks).subscribe( + (deleteResults) => { + const anySuccess = deleteResults.some(res => res.success); + const referenceErrors = deleteResults.filter(res => res.resourceIsReferencedError); + const otherError = deleteResults.find(res => !res.success); + if (anySuccess) { + this.config.updateData(); + } + if (referenceErrors?.length) { + const resourcesWithReferences: ResourceInfoWithReferences[] = + referenceErrors.map(ref => ({...ref.resource, ...{references: ref.references}})); + const data = { + multiple: true, + resources: resourcesWithReferences, + configuration: { + title: 'resource.resources-are-in-use', + message: this.translate.instant('resource.resources-are-in-use-text'), + deleteText: 'resource.delete-resource-in-use-text', + selectedText: 'resource.selected-resources', + datasource: new ResourcesDatasource(this.resourceService, resourcesWithReferences, () => true), + columns: ['select', 'title', 'references'] + } + }; + this.dialog.open(ResourcesInUseDialogComponent, { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data + }).afterClosed().subscribe((forceDeleteResources) => { + if (forceDeleteResources && forceDeleteResources.length) { + const forceDeleteTasks = forceDeleteResources.map((resource) => + this.resourceService.deleteResource(resource.id.id, true) + ); + forkJoin(forceDeleteTasks).subscribe( + () => { + this.config.updateData(); + } + ); + } + }); + } else if (otherError) { + const errorMessageWithTimeout = parseHttpErrorMessage(otherError.error, this.translate); + setTimeout(() => { + this.store.dispatch(new ActionNotificationShow({message: errorMessageWithTimeout.message, type: 'error'})); + }, errorMessageWithTimeout.timeout); + } + } + ); + } + }); + } + } } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index d79bb81ae5..4639552658 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -4497,7 +4497,12 @@ "scada-symbol": "Scada symbol", "extension": "Extension", "module": "Module" - } + }, + "resource-is-in-use": "Resource is used by other entities", + "resources-are-in-use": "Resources are used by other entities", + "resource-is-in-use-text": "The Resource '{{title}}' was not deleted because it is used by the following entities:", + "resources-are-in-use-text": "Not all Resources have been deleted because they are used by other entities.
You can view referenced entities by clicking the References button in the corresponding resource row.
If you still want to delete these resources, select them in the table below and click the Delete selected button.", + "delete-resource-in-use-text": "If you still want to delete the resource, click the Delete anyway button." }, "javascript": { "add": "Add JavaScript resource", From e88114de720bf8171500fa0031cea84b0e7f64a3 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Fri, 12 Sep 2025 16:46:38 +0300 Subject: [PATCH 177/216] UI: oprimize import --- ui-ngx/src/app/core/http/resource.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ui-ngx/src/app/core/http/resource.service.ts b/ui-ngx/src/app/core/http/resource.service.ts index 90335758dd..168c63b3b1 100644 --- a/ui-ngx/src/app/core/http/resource.service.ts +++ b/ui-ngx/src/app/core/http/resource.service.ts @@ -24,7 +24,6 @@ import { Resource, ResourceInfo, ResourceSubType, ResourceType, TBResourceScope import { catchError, mergeMap } from 'rxjs/operators'; import { isNotEmptyStr } from '@core/utils'; import { ResourcesService } from '@core/services/resources.service'; -import { NotificationTarget } from "@shared/models/notification.models"; @Injectable({ providedIn: 'root' From 5c7f20a1514124cafbd9f26f6a94adc0fac516b5 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 15 Sep 2025 17:32:57 +0300 Subject: [PATCH 178/216] AI models: add support for Ollama --- application/pom.xml | 4 ++ .../Langchain4jChatModelConfigurerImpl.java | 16 ++++++ .../common/data/ai/dto/TbChatResponse.java | 6 +- .../common/data/ai/model/AiModelConfig.java | 8 ++- .../data/ai/model/chat/AiChatModelConfig.java | 2 +- .../chat/Langchain4jChatModelConfigurer.java | 2 + .../ai/model/chat/OllamaChatModelConfig.java | 57 +++++++++++++++++++ .../common/data/ai/provider/AiProvider.java | 3 +- .../data/ai/provider/AiProviderConfig.java | 2 +- .../ai/provider/OllamaProviderConfig.java | 22 +++++++ .../rule/engine/ai/TbResponseFormat.java | 8 +-- .../ai-model/ai-model-dialog.component.html | 9 +++ .../ai-model/ai-model-dialog.component.ts | 1 + .../src/app/shared/models/ai-model.models.ts | 21 +++++-- .../assets/locale/locale.constant-en_US.json | 5 +- 15 files changed, 146 insertions(+), 20 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java diff --git a/application/pom.xml b/application/pom.xml index 33bc0972d4..0413f7732c 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -419,6 +419,10 @@ + + dev.langchain4j + langchain4j-ollama + diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java index 69dd98f47f..7008e866f4 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -32,6 +32,7 @@ import dev.langchain4j.model.chat.request.ChatRequestParameters; import dev.langchain4j.model.github.GitHubModelsChatModel; import dev.langchain4j.model.googleai.GoogleAiGeminiChatModel; import dev.langchain4j.model.mistralai.MistralAiChatModel; +import dev.langchain4j.model.ollama.OllamaChatModel; import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel; import org.springframework.stereotype.Component; @@ -43,6 +44,7 @@ import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelC import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.Langchain4jChatModelConfigurer; import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.OllamaChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig; import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig; import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig; @@ -262,6 +264,20 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .build(); } + @Override + public ChatModel configureChatModel(OllamaChatModelConfig chatModelConfig) { + return OllamaChatModel.builder() + .baseUrl(chatModelConfig.providerConfig().baseUrl()) + .modelName(chatModelConfig.modelId()) + .temperature(chatModelConfig.temperature()) + .topP(chatModelConfig.topP()) + .topK(chatModelConfig.topK()) + .numPredict(chatModelConfig.maxOutputTokens()) + .timeout(toDuration(chatModelConfig.timeoutSeconds())) + .maxRetries(chatModelConfig.maxRetries()) + .build(); + } + private static Duration toDuration(Integer timeoutSeconds) { return timeoutSeconds != null ? Duration.ofSeconds(timeoutSeconds) : null; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java index 2cc17e4553..73e6557fb5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/dto/TbChatResponse.java @@ -22,7 +22,7 @@ import io.swagger.v3.oas.annotations.media.Schema; @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, property = "status", - include = JsonTypeInfo.As.PROPERTY, + include = JsonTypeInfo.As.EXISTING_PROPERTY, visible = true ) @JsonSubTypes({ @@ -51,9 +51,7 @@ public sealed interface TbChatResponse permits TbChatResponse.Success, TbChatRes } record Failure( - @Schema( - description = "A string containing details about the failure" - ) + @Schema(description = "A string containing details about the failure") String errorDetails ) implements TbChatResponse { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java index bfaa29a6e3..f18429e7cf 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/AiModelConfig.java @@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.ai.model.chat.GitHubModelsChatModelCon import org.thingsboard.server.common.data.ai.model.chat.GoogleAiGeminiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.GoogleVertexAiGeminiChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.MistralAiChatModelConfig; +import org.thingsboard.server.common.data.ai.model.chat.OllamaChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig; import org.thingsboard.server.common.data.ai.provider.AiProvider; import org.thingsboard.server.common.data.ai.provider.AiProviderConfig; @@ -34,6 +35,7 @@ import org.thingsboard.server.common.data.ai.provider.GitHubModelsProviderConfig import org.thingsboard.server.common.data.ai.provider.GoogleAiGeminiProviderConfig; import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; import org.thingsboard.server.common.data.ai.provider.MistralAiProviderConfig; +import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig; import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; @JsonTypeInfo( @@ -50,7 +52,8 @@ import org.thingsboard.server.common.data.ai.provider.OpenAiProviderConfig; @JsonSubTypes.Type(value = MistralAiChatModelConfig.class, name = "MISTRAL_AI"), @JsonSubTypes.Type(value = AnthropicChatModelConfig.class, name = "ANTHROPIC"), @JsonSubTypes.Type(value = AmazonBedrockChatModelConfig.class, name = "AMAZON_BEDROCK"), - @JsonSubTypes.Type(value = GitHubModelsChatModelConfig.class, name = "GITHUB_MODELS") + @JsonSubTypes.Type(value = GitHubModelsChatModelConfig.class, name = "GITHUB_MODELS"), + @JsonSubTypes.Type(value = OllamaChatModelConfig.class, name = "OLLAMA") }) public interface AiModelConfig { @@ -69,7 +72,8 @@ public interface AiModelConfig { @JsonSubTypes.Type(value = MistralAiProviderConfig.class, name = "MISTRAL_AI"), @JsonSubTypes.Type(value = AnthropicProviderConfig.class, name = "ANTHROPIC"), @JsonSubTypes.Type(value = AmazonBedrockProviderConfig.class, name = "AMAZON_BEDROCK"), - @JsonSubTypes.Type(value = GitHubModelsProviderConfig.class, name = "GITHUB_MODELS") + @JsonSubTypes.Type(value = GitHubModelsProviderConfig.class, name = "GITHUB_MODELS"), + @JsonSubTypes.Type(value = OllamaProviderConfig.class, name = "OLLAMA") }) AiProviderConfig providerConfig(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java index 2bc28cfce0..49126c1861 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AiChatModelConfig.java @@ -24,7 +24,7 @@ public sealed interface AiChatModelConfig> extend permits OpenAiChatModelConfig, AzureOpenAiChatModelConfig, GoogleAiGeminiChatModelConfig, GoogleVertexAiGeminiChatModelConfig, MistralAiChatModelConfig, AnthropicChatModelConfig, - AmazonBedrockChatModelConfig, GitHubModelsChatModelConfig { + AmazonBedrockChatModelConfig, GitHubModelsChatModelConfig, OllamaChatModelConfig { ChatModel configure(Langchain4jChatModelConfigurer configurer); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java index c9c1bc3173..828256dcdc 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/Langchain4jChatModelConfigurer.java @@ -35,4 +35,6 @@ public interface Langchain4jChatModelConfigurer { ChatModel configureChatModel(GitHubModelsChatModelConfig chatModelConfig); + ChatModel configureChatModel(OllamaChatModelConfig chatModelConfig); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java new file mode 100644 index 0000000000..360b514d6d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java @@ -0,0 +1,57 @@ +/** + * 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.common.data.ai.model.chat; + +import dev.langchain4j.model.chat.ChatModel; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.Builder; +import lombok.With; +import org.thingsboard.server.common.data.ai.provider.AiProvider; +import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig; + +@Builder +public record OllamaChatModelConfig( + @NotNull @Valid OllamaProviderConfig providerConfig, + @NotBlank String modelId, + @PositiveOrZero Double temperature, + @Positive @Max(1) Double topP, + @PositiveOrZero Integer topK, + @Positive Integer maxOutputTokens, + @With @Positive Integer timeoutSeconds, + @With @PositiveOrZero Integer maxRetries +) implements AiChatModelConfig { + + @Override + public AiProvider provider() { + return AiProvider.OLLAMA; + } + + @Override + public ChatModel configure(Langchain4jChatModelConfigurer configurer) { + return configurer.configureChatModel(this); + } + + @Override + public boolean supportsJsonMode() { + return true; + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java index d0a5bd0510..a9a6af4de8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProvider.java @@ -24,6 +24,7 @@ public enum AiProvider { MISTRAL_AI, ANTHROPIC, AMAZON_BEDROCK, - GITHUB_MODELS + GITHUB_MODELS, + OLLAMA } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java index bd32c88efb..5423b24410 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/AiProviderConfig.java @@ -19,4 +19,4 @@ public sealed interface AiProviderConfig permits OpenAiProviderConfig, AzureOpenAiProviderConfig, GoogleAiGeminiProviderConfig, GoogleVertexAiGeminiProviderConfig, MistralAiProviderConfig, AnthropicProviderConfig, - AmazonBedrockProviderConfig, GitHubModelsProviderConfig {} + AmazonBedrockProviderConfig, GitHubModelsProviderConfig, OllamaProviderConfig {} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java new file mode 100644 index 0000000000..fc0a2d6fd8 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java @@ -0,0 +1,22 @@ +/** + * 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.common.data.ai.provider; + +import jakarta.validation.constraints.NotBlank; + +public record OllamaProviderConfig( + @NotBlank String baseUrl +) implements AiProviderConfig {} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbResponseFormat.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbResponseFormat.java index 5c891a9c74..5107e613a4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbResponseFormat.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbResponseFormat.java @@ -60,9 +60,7 @@ public sealed interface TbResponseFormat permits TbTextResponseFormat, TbJsonRes @Override public ResponseFormat toLangChainResponseFormat() { - return ResponseFormat.builder() - .type(ResponseFormatType.TEXT) - .build(); + return ResponseFormat.TEXT; } } @@ -76,9 +74,7 @@ public sealed interface TbResponseFormat permits TbTextResponseFormat, TbJsonRes @Override public ResponseFormat toLangChainResponseFormat() { - return ResponseFormat.builder() - .type(ResponseFormatType.JSON) - .build(); + return ResponseFormat.JSON; } } diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html index 5f9c189399..1a3c2bf181 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html @@ -150,6 +150,15 @@ } + @if (providerFieldsList.includes('baseUrl')) { + + ai-models.baseurl + + + {{ 'ai-models.baseurl-required' | translate }} + + + } diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts index c459d66f12..e6490cd84d 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts @@ -100,6 +100,7 @@ export class AIModelDialogComponent extends DialogComponent, 'label'>, HasTenantId region?: string; accessKeyId?: string; secretAccessKey?: string; + baseUrl?: string; }; modelId: string; temperature?: number; @@ -57,7 +58,8 @@ export enum AiProvider { MISTRAL_AI = 'MISTRAL_AI', ANTHROPIC = 'ANTHROPIC', AMAZON_BEDROCK = 'AMAZON_BEDROCK', - GITHUB_MODELS = 'GITHUB_MODELS' + GITHUB_MODELS = 'GITHUB_MODELS', + OLLAMA = 'OLLAMA' } export const AiProviderTranslations = new Map( @@ -69,7 +71,8 @@ export const AiProviderTranslations = new Map( [AiProvider.MISTRAL_AI , 'ai-models.ai-providers.mistral-ai'], [AiProvider.ANTHROPIC , 'ai-models.ai-providers.anthropic'], [AiProvider.AMAZON_BEDROCK , 'ai-models.ai-providers.amazon-bedrock'], - [AiProvider.GITHUB_MODELS , 'ai-models.ai-providers.github-models'] + [AiProvider.GITHUB_MODELS , 'ai-models.ai-providers.github-models'], + [AiProvider.OLLAMA , 'ai-models.ai-providers.ollama'] ] ); @@ -84,7 +87,8 @@ export const ProviderFieldsAllList = [ 'serviceVersion', 'region', 'accessKeyId', - 'secretAccessKey' + 'secretAccessKey', + 'baseUrl' ]; export const ModelFieldsAllList = ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens']; @@ -191,6 +195,14 @@ export const AiModelMap = new Map Date: Tue, 16 Sep 2025 14:41:38 +0300 Subject: [PATCH 179/216] AI models: add context length support for Ollama --- .../Langchain4jChatModelConfigurerImpl.java | 1 + .../chat/AmazonBedrockChatModelConfig.java | 2 +- .../model/chat/AnthropicChatModelConfig.java | 2 +- .../chat/AzureOpenAiChatModelConfig.java | 2 +- .../chat/GitHubModelsChatModelConfig.java | 2 +- .../chat/GoogleAiGeminiChatModelConfig.java | 2 +- .../GoogleVertexAiGeminiChatModelConfig.java | 2 +- .../model/chat/MistralAiChatModelConfig.java | 2 +- .../ai/model/chat/OllamaChatModelConfig.java | 3 ++- .../ai/model/chat/OpenAiChatModelConfig.java | 2 +- .../ai-model/ai-model-dialog.component.html | 23 +++++++++++-------- .../ai-model/ai-model-dialog.component.ts | 3 ++- .../src/app/shared/models/ai-model.models.ts | 5 ++-- .../assets/locale/locale.constant-en_US.json | 3 ++- 14 files changed, 31 insertions(+), 23 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java index 7008e866f4..84b09b9188 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -272,6 +272,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .temperature(chatModelConfig.temperature()) .topP(chatModelConfig.topP()) .topK(chatModelConfig.topK()) + .numCtx(chatModelConfig.contextLength()) .numPredict(chatModelConfig.maxOutputTokens()) .timeout(toDuration(chatModelConfig.timeoutSeconds())) .maxRetries(chatModelConfig.maxRetries()) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java index 2bb4de5aa8..d2ab72086a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AmazonBedrockChatModelConfig.java @@ -33,7 +33,7 @@ public record AmazonBedrockChatModelConfig( @NotBlank String modelId, @PositiveOrZero Double temperature, @Positive @Max(1) Double topP, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java index 69b5578fb3..6d505f75a6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AnthropicChatModelConfig.java @@ -34,7 +34,7 @@ public record AnthropicChatModelConfig( @PositiveOrZero Double temperature, @Positive @Max(1) Double topP, @PositiveOrZero Integer topK, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java index 47e7e96c37..f70f2af539 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/AzureOpenAiChatModelConfig.java @@ -35,7 +35,7 @@ public record AzureOpenAiChatModelConfig( @Positive @Max(1) Double topP, Double frequencyPenalty, Double presencePenalty, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java index b509254f77..0aafd72197 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GitHubModelsChatModelConfig.java @@ -35,7 +35,7 @@ public record GitHubModelsChatModelConfig( @Positive @Max(1) Double topP, Double frequencyPenalty, Double presencePenalty, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java index fe11a11460..b5c3d4263d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleAiGeminiChatModelConfig.java @@ -36,7 +36,7 @@ public record GoogleAiGeminiChatModelConfig( @PositiveOrZero Integer topK, Double frequencyPenalty, Double presencePenalty, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java index 609e14f86e..944963ee27 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/GoogleVertexAiGeminiChatModelConfig.java @@ -36,7 +36,7 @@ public record GoogleVertexAiGeminiChatModelConfig( @PositiveOrZero Integer topK, Double frequencyPenalty, Double presencePenalty, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java index f603e99c53..8f67d93398 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/MistralAiChatModelConfig.java @@ -35,7 +35,7 @@ public record MistralAiChatModelConfig( @Positive @Max(1) Double topP, Double frequencyPenalty, Double presencePenalty, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java index 360b514d6d..ea48670b63 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OllamaChatModelConfig.java @@ -34,7 +34,8 @@ public record OllamaChatModelConfig( @PositiveOrZero Double temperature, @Positive @Max(1) Double topP, @PositiveOrZero Integer topK, - @Positive Integer maxOutputTokens, + Integer contextLength, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java index 00b5115d7d..23db9accc2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/model/chat/OpenAiChatModelConfig.java @@ -35,7 +35,7 @@ public record OpenAiChatModelConfig( @Positive @Max(1) Double topP, Double frequencyPenalty, Double presencePenalty, - @Positive Integer maxOutputTokens, + Integer maxOutputTokens, @With @Positive Integer timeoutSeconds, @With @PositiveOrZero Integer maxRetries ) implements AiChatModelConfig { diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html index 1a3c2bf181..c730850474 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html @@ -151,7 +151,7 @@ } @if (providerFieldsList.includes('baseUrl')) { - + ai-models.baseurl @@ -264,15 +264,18 @@ - - warning - + type="number" step="1" placeholder="{{ 'ai-models.set' | translate }}"> + + + } + @if (modelFieldsList.includes('contextLength')) { +
+
+ {{ 'ai-models.context-length' | translate }} +
+ +
} diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts index e6490cd84d..3294c6ac76 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts @@ -108,7 +108,8 @@ export class AIModelDialogComponent extends DialogComponent, 'label'>, HasTenantId frequencyPenalty?: number; presencePenalty?: number; maxOutputTokens?: number; + contextLength?: number; } } @@ -91,7 +92,7 @@ export const ProviderFieldsAllList = [ 'baseUrl' ]; -export const ModelFieldsAllList = ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens']; +export const ModelFieldsAllList = ['temperature', 'topP', 'topK', 'frequencyPenalty', 'presencePenalty', 'maxOutputTokens', 'contextLength']; export const AiModelMap = new Map([ [ @@ -200,7 +201,7 @@ export const AiModelMap = new Map Date: Mon, 22 Sep 2025 12:18:06 +0300 Subject: [PATCH 180/216] AI models: add auth support for Ollama --- .../Langchain4jChatModelConfigurerImpl.java | 28 +++++++++++++--- .../ai/provider/OllamaProviderConfig.java | 32 +++++++++++++++++-- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java index 84b09b9188..2cb6c2097f 100644 --- a/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java +++ b/application/src/main/java/org/thingsboard/server/service/ai/Langchain4jChatModelConfigurerImpl.java @@ -35,6 +35,7 @@ import dev.langchain4j.model.mistralai.MistralAiChatModel; import dev.langchain4j.model.ollama.OllamaChatModel; import dev.langchain4j.model.openai.OpenAiChatModel; import dev.langchain4j.model.vertexai.gemini.VertexAiGeminiChatModel; +import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.ai.model.chat.AmazonBedrockChatModelConfig; import org.thingsboard.server.common.data.ai.model.chat.AnthropicChatModelConfig; @@ -49,6 +50,7 @@ import org.thingsboard.server.common.data.ai.model.chat.OpenAiChatModelConfig; import org.thingsboard.server.common.data.ai.provider.AmazonBedrockProviderConfig; import org.thingsboard.server.common.data.ai.provider.AzureOpenAiProviderConfig; import org.thingsboard.server.common.data.ai.provider.GoogleVertexAiGeminiProviderConfig; +import org.thingsboard.server.common.data.ai.provider.OllamaProviderConfig; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.regions.Region; @@ -56,7 +58,11 @@ import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.Base64; + +import static java.util.Collections.singletonMap; @Component class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigurer { @@ -136,7 +142,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur // set request timeout from model config if (chatModelConfig.timeoutSeconds() != null) { - retrySettings.setTotalTimeout(org.threeten.bp.Duration.ofSeconds(chatModelConfig.timeoutSeconds())); + retrySettings.setTotalTimeoutDuration(Duration.ofSeconds(chatModelConfig.timeoutSeconds())); } // set updated retry settings @@ -266,7 +272,7 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur @Override public ChatModel configureChatModel(OllamaChatModelConfig chatModelConfig) { - return OllamaChatModel.builder() + var builder = OllamaChatModel.builder() .baseUrl(chatModelConfig.providerConfig().baseUrl()) .modelName(chatModelConfig.modelId()) .temperature(chatModelConfig.temperature()) @@ -275,8 +281,22 @@ class Langchain4jChatModelConfigurerImpl implements Langchain4jChatModelConfigur .numCtx(chatModelConfig.contextLength()) .numPredict(chatModelConfig.maxOutputTokens()) .timeout(toDuration(chatModelConfig.timeoutSeconds())) - .maxRetries(chatModelConfig.maxRetries()) - .build(); + .maxRetries(chatModelConfig.maxRetries()); + + var auth = chatModelConfig.providerConfig().auth(); + if (auth instanceof OllamaProviderConfig.OllamaAuth.Basic basicAuth) { + String credentials = basicAuth.username() + ":" + basicAuth.password(); + String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + builder.customHeaders(singletonMap(HttpHeaders.AUTHORIZATION, "Basic " + encodedCredentials)); + } else if (auth instanceof OllamaProviderConfig.OllamaAuth.Token tokenAuth) { + builder.customHeaders(singletonMap(HttpHeaders.AUTHORIZATION, "Bearer " + tokenAuth.token())); + } else if (auth instanceof OllamaProviderConfig.OllamaAuth.None) { + // do nothing + } else { + throw new UnsupportedOperationException("Unknown authentication type: " + auth.getClass().getSimpleName()); + } + + return builder.build(); } private static Duration toDuration(Integer timeoutSeconds) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java index fc0a2d6fd8..39bb57834c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ai/provider/OllamaProviderConfig.java @@ -15,8 +15,34 @@ */ package org.thingsboard.server.common.data.ai.provider; -import jakarta.validation.constraints.NotBlank; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; public record OllamaProviderConfig( - @NotBlank String baseUrl -) implements AiProviderConfig {} + @NotNull String baseUrl, + @NotNull @Valid OllamaAuth auth +) implements AiProviderConfig { + + @JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type" + ) + @JsonSubTypes({ + @JsonSubTypes.Type(value = OllamaAuth.None.class, name = "NONE"), + @JsonSubTypes.Type(value = OllamaAuth.Basic.class, name = "BASIC"), + @JsonSubTypes.Type(value = OllamaAuth.Token.class, name = "TOKEN") + }) + public sealed interface OllamaAuth { + + record None() implements OllamaAuth {} + + record Basic(@NotNull String username, @NotNull String password) implements OllamaAuth {} + + record Token(@NotNull String token) implements OllamaAuth {} + + } + +} From 8a6015f04e7b2b4471f9d801517c9e40da6ef0d3 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Thu, 25 Sep 2025 15:44:50 +0300 Subject: [PATCH 181/216] UI: Add authentication for Ollama model --- .../ai-model/ai-model-dialog.component.html | 87 +++++++++++++++---- .../ai-model/ai-model-dialog.component.ts | 67 +++++++++++--- .../src/app/shared/models/ai-model.models.ts | 11 +++ .../assets/locale/locale.constant-en_US.json | 16 +++- 4 files changed, 153 insertions(+), 28 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html index c730850474..abfe8500b4 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.html @@ -55,31 +55,34 @@
-
+
@if (providerFieldsList.includes('personalAccessToken')) { ai-models.personal-access-token - + {{ 'ai-models.personal-access-token-required' | translate }} } @if (providerFieldsList.includes('projectId')) { - + ai-models.project-id - + {{ 'ai-models.project-id-required' | translate }} } @if (providerFieldsList.includes('location')) { - + ai-models.location - + {{ 'ai-models.location-required' | translate }} @@ -98,16 +101,17 @@ } @if (providerFieldsList.includes('endpoint')) { - + ai-models.endpoint - + {{ 'ai-models.endpoint-required' | translate }} } @if (providerFieldsList.includes('serviceVersion')) { - + ai-models.service-version @@ -117,25 +121,28 @@ ai-models.api-key - + {{ 'ai-models.api-key-required' | translate }} } @if (providerFieldsList.includes('region')) { - + ai-models.region - + {{ 'ai-models.region-required' | translate }} } @if (providerFieldsList.includes('accessKeyId')) { - + ai-models.access-key-id - + {{ 'ai-models.access-key-id-required' | translate }} @@ -145,7 +152,8 @@ ai-models.secret-access-key - + {{ 'ai-models.secret-access-key-required' | translate }} @@ -154,11 +162,58 @@ ai-models.baseurl - + {{ 'ai-models.baseurl-required' | translate }} } + @if (provider === aiProvider.OLLAMA) { +
+
+
+ {{ 'ai-models.authentication' | translate }} +
+ + {{ 'ai-models.authentication-type.none' | translate }} + {{ 'ai-models.authentication-type.basic' | translate }} + {{ 'ai-models.authentication-type.token' | translate }} + +
+
+ @if (aiModelForms.get('configuration.providerConfig.auth.type').value === AuthenticationType.BASIC) { + + ai-models.username + + + {{ 'ai-models.username-required' | translate }} + + + + ai-models.password + + + + {{ 'ai-models.password-required' | translate }} + + + } + @if (aiModelForms.get('configuration.providerConfig.auth.type').value === AuthenticationType.TOKEN) { + + ai-models.token + + + + {{ 'ai-models.token-required' | translate }} + + + } +
+
+ }
diff --git a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts index 3294c6ac76..9d0d28e627 100644 --- a/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/ai-model/ai-model-dialog.component.ts @@ -30,6 +30,7 @@ import { AiModelMap, AiProvider, AiProviderTranslations, + AuthenticationType, ModelType, ProviderFieldsAllList } from '@shared/models/ai-model.models'; @@ -37,6 +38,7 @@ import { AiModelService } from '@core/http/ai-model.service'; import { CheckConnectivityDialogComponent } from '@home/components/ai-model/check-connectivity-dialog.component'; import { map } from 'rxjs/operators'; import { deepTrim } from '@core/utils'; +import { TranslateService } from '@ngx-translate/core'; export interface AIModelDialogData { AIModel?: AiModel; @@ -62,18 +64,23 @@ export class AIModelDialogComponent extends DialogComponent, protected router: Router, protected dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: AIModelDialogData, private fb: FormBuilder, private aiModelService: AiModelService, + private translate: TranslateService, private dialog: MatDialog) { super(store, router, dialogRef); @@ -89,18 +96,24 @@ export class AIModelDialogComponent extends DialogComponent { + this.getAuthenticationHint(type); + this.aiModelForms.get('configuration.providerConfig.auth.username').disable(); + this.aiModelForms.get('configuration.providerConfig.auth.password').disable(); + this.aiModelForms.get('configuration.providerConfig.auth.token').disable(); + if (type === AuthenticationType.BASIC) { + this.aiModelForms.get('configuration.providerConfig.auth.username').enable(); + this.aiModelForms.get('configuration.providerConfig.auth.password').enable(); + } + if (type === AuthenticationType.TOKEN) { + this.aiModelForms.get('configuration.providerConfig.auth.token').enable(); + } + }); this.updateValidation(this.provider); } @@ -132,6 +161,16 @@ export class AIModelDialogComponent extends DialogComponent { if (AiModelMap.get(provider).providerFieldsList.includes(key)) { @@ -139,7 +178,13 @@ export class AIModelDialogComponent extends DialogComponent, 'label'>, HasTenantId accessKeyId?: string; secretAccessKey?: string; baseUrl?: string; + auth?: { + type: AuthenticationType; + username?: string; + password?: string; + token?: string + } }; modelId: string; temperature?: number; @@ -242,3 +248,8 @@ export interface CheckConnectivityResult { status: string; errorDetails: string; } +export enum AuthenticationType { + NONE = 'NONE', + BASIC = 'BASIC', + TOKEN = 'TOKEN' +} diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index c1acd62c00..d39078f56a 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1169,7 +1169,21 @@ "check-connectivity-failed": "Test request failed", "no-model-matching": "No models matching '{{entity}}' were found.", "model-required": "Model is required.", - "no-model-text": "No models found." + "no-model-text": "No models found.", + "authentication": "Authentication", + "authentication-basic-hint": "Uses standard HTTP Basic authentication. The username and password will be combined, Base64-encoded, and sent in an \"Authorization\" header with each request to the Ollama server.", + "authentication-token-hint": "Uses Bearer token authentication. The provided token will be sent directly in an \"Authorization\" eader with each request to the Ollama server.", + "authentication-type": { + "none": "None", + "basic": "Basic", + "token": "Token" + }, + "username": "Username", + "username-required": "Username is required.", + "password": "Password", + "password-required": "Password is required.", + "token": "Token", + "token-required": "Token is required." }, "confirm-on-exit": { "message": "You have unsaved changes. Are you sure you want to leave this page?", From d989d1e7d75e1fa54f484bea444c2b2d08bc5d5a Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Tue, 30 Sep 2025 13:52:05 +0300 Subject: [PATCH 182/216] UI: Refactoring show total value in legend --- .../basic/chart/latest-chart-basic-config.component.html | 8 +++++--- .../widget/lib/chart/bar-chart-widget.models.ts | 1 + .../components/widget/lib/chart/doughnut-widget.models.ts | 1 + .../components/widget/lib/chart/latest-chart.component.ts | 5 +---- .../components/widget/lib/chart/latest-chart.models.ts | 5 ++--- .../home/components/widget/lib/chart/latest-chart.ts | 8 +++++--- .../widget/lib/chart/pie-chart-widget.models.ts | 1 + .../widget/lib/chart/polar-area-widget.models.ts | 1 + .../widget/lib/chart/radar-chart-widget.models.ts | 1 + .../chart/latest-chart-widget-settings.component.html | 8 +++++--- 10 files changed, 23 insertions(+), 16 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html index b5c1a460fb..31d0e6cd7e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/latest-chart-basic-config.component.html @@ -144,9 +144,11 @@
- - {{ 'legend.show-total' | translate }} - +
+ + {{ 'legend.show-total' | translate }} + +
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.models.ts index 036ba70c04..3f76eb18cf 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/bar-chart-widget.models.ts @@ -68,6 +68,7 @@ export const barChartWidgetBarsChartSettings = (settings: BarChartWidgetSettings showTotal: false, animation: settings.animation, showLegend: settings.showLegend, + legendShowTotal: settings.legendShowTotal, showTooltip: settings.showTooltip, tooltipValueType: settings.tooltipValueType, tooltipValueDecimals: settings.tooltipValueDecimals, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.models.ts index 0d8c1ba9fa..0ef277ea8b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/doughnut-widget.models.ts @@ -88,6 +88,7 @@ export const doughnutPieChartSettings = (settings: DoughnutWidgetSettings): Deep showTotal: settings.layout === DoughnutLayout.with_total, animation: settings.animation, showLegend: settings.showLegend, + legendShowTotal: settings.legendShowTotal, totalValueFont: settings.totalValueFont, totalValueColor: settings.totalValueColor, showLabel: false, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts index 80be75d98d..85905dfd3e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.component.ts @@ -82,8 +82,7 @@ export class LatestChartComponent implements OnInit, OnDestroy, AfterViewInit { padding: string; get legendItems(): LatestChartLegendItem[] { - let items = this.latestChart ? this.latestChart.getLegendItems() : []; - return this.legendShowTotal ? items : items.filter(item => !item.total); + return this.latestChart ? this.latestChart.getLegendItems() : []; } legendLabelStyle: ComponentStyle; @@ -93,7 +92,6 @@ export class LatestChartComponent implements OnInit, OnDestroy, AfterViewInit { private shapeResize$: ResizeObserver; private legendHorizontal: boolean; - private legendShowTotal: boolean; private latestChart: TbLatestChart; @@ -121,7 +119,6 @@ export class LatestChartComponent implements OnInit, OnDestroy, AfterViewInit { this.legendValueStyle = textStyle(this.settings.legendValueFont); this.disabledLegendValueStyle = textStyle(this.settings.legendValueFont); this.legendValueStyle.color = this.settings.legendValueColor; - this.legendShowTotal = this.settings.legendShowTotal; } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.models.ts index 639938bad6..aa1ac43644 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.models.ts @@ -87,6 +87,7 @@ export interface LatestChartSettings extends LatestChartTooltipSettings { sortSeries: boolean; showTotal?: boolean; showLegend: boolean; + legendShowTotal: boolean; animation: ChartAnimationSettings; } @@ -96,6 +97,7 @@ export const latestChartDefaultSettings: LatestChartSettings = { sortSeries: false, showTotal: false, showLegend: true, + legendShowTotal: true, animation: mergeDeep({} as ChartAnimationSettings, chartAnimationDefaultSettings) }; @@ -105,14 +107,12 @@ export interface LatestChartWidgetSettings extends LatestChartSettings { legendLabelColor: string; legendValueFont: Font; legendValueColor: string; - legendShowTotal: boolean; background: BackgroundSettings; padding: string; } export const latestChartWidgetDefaultSettings: LatestChartWidgetSettings = { ...latestChartDefaultSettings, - showLegend: true, legendPosition: LegendPosition.bottom, legendLabelFont: { family: 'Roboto', @@ -132,7 +132,6 @@ export const latestChartWidgetDefaultSettings: LatestChartWidgetSettings = { lineHeight: '20px' }, legendValueColor: 'rgba(0, 0, 0, 0.87)', - legendShowTotal: true, background: { type: BackgroundType.color, color: '#fff', diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts index b9f0940b52..9e525cd794 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/latest-chart.ts @@ -36,6 +36,7 @@ import { ValueFormatProcessor } from '@shared/models/widget-settings.models'; export abstract class TbLatestChart { private readonly shapeResize$: ResizeObserver; + private showTotalValueInLegend: boolean; protected readonly settings: S; @@ -121,7 +122,8 @@ export abstract class TbLatestChart { this.legendItems.sort((a, b) => a.label.localeCompare(b.label)); } } - if (this.settings.showLegend && !this.settings.showTotal) { + this.showTotalValueInLegend = this.settings.showLegend && !this.settings.showTotal && this.settings.legendShowTotal; + if (this.showTotalValueInLegend) { this.legendItems.push( { value: '--', @@ -252,11 +254,11 @@ export abstract class TbLatestChart { if (this.settings.showTotal || this.settings.showLegend) { if (hasValue) { this.totalText = this.valueFormatter.format(this.total); - if (this.settings.showLegend && !this.settings.showTotal) { + if (this.showTotalValueInLegend) { this.legendItems[this.legendItems.length - 1].hasValue = true; this.legendItems[this.legendItems.length - 1].value = this.totalText; } - } else if (this.settings.showLegend && !this.settings.showTotal) { + } else if (this.showTotalValueInLegend) { this.legendItems[this.legendItems.length - 1].hasValue = false; this.legendItems[this.legendItems.length - 1].value = '--'; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.models.ts index a98cdfdd4a..cddf75b9f9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/pie-chart-widget.models.ts @@ -64,6 +64,7 @@ export const pieChartWidgetPieChartSettings = (settings: PieChartWidgetSettings) showTotal: false, animation: settings.animation, showLegend: settings.showLegend, + legendShowTotal: settings.legendShowTotal, showLabel: settings.showLabel, labelPosition: settings.labelPosition, labelFont: settings.labelFont, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/polar-area-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/polar-area-widget.models.ts index 70b4520159..4b744ce9c3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/polar-area-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/polar-area-widget.models.ts @@ -72,6 +72,7 @@ export const polarAreaChartWidgetBarsChartSettings = (settings: PolarAreaChartWi showTotal: false, animation: settings.animation, showLegend: settings.showLegend, + legendShowTotal: settings.legendShowTotal, showTooltip: settings.showTooltip, tooltipValueType: settings.tooltipValueType, tooltipValueDecimals: settings.tooltipValueDecimals, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/radar-chart-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/radar-chart-widget.models.ts index ec42955b4d..b3be129489 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/radar-chart-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/radar-chart-widget.models.ts @@ -135,6 +135,7 @@ export const radarChartWidgetRadarChartSettings = (settings: RadarChartWidgetSet showTotal: false, animation: settings.animation, showLegend: settings.showLegend, + legendShowTotal: settings.legendShowTotal, showTooltip: settings.showTooltip, tooltipValueType: settings.tooltipValueType, tooltipValueDecimals: settings.tooltipValueDecimals, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html index fe071f16a0..66d0234914 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/latest-chart-widget-settings.component.html @@ -61,9 +61,11 @@ - - {{ 'legend.show-total' | translate }} - +
+ + {{ 'legend.show-total' | translate }} + +
From 94e098ab4d058fc7f157e3176f3cf43f408f24be Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Tue, 30 Sep 2025 14:50:00 +0300 Subject: [PATCH 183/216] AI models: update model ID autocomplete options --- ui-ngx/src/app/shared/models/ai-model.models.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/models/ai-model.models.ts b/ui-ngx/src/app/shared/models/ai-model.models.ts index b0a88d076e..5ba0327dfe 100644 --- a/ui-ngx/src/app/shared/models/ai-model.models.ts +++ b/ui-ngx/src/app/shared/models/ai-model.models.ts @@ -110,6 +110,9 @@ export const AiModelMap = new Map Date: Tue, 30 Sep 2025 17:02:23 +0200 Subject: [PATCH 184/216] Cassandra DAO: Safety trigger to fall back to use_ts_key_value_partitioning_on_read as true if estimated partitions count is greater than safety trigger value. --- .../src/main/resources/thingsboard.yml | 5 +- .../CassandraBaseTimeseriesDao.java | 29 ++++++++- .../dao/timeseries/NoSqlTsPartitionDate.java | 15 ++--- ...aoPartitioningMinutesAlwaysExistsTest.java | 12 ++++ ...DaoPartitioningMonthsAlwaysExistsTest.java | 63 ++++++++++++++++++- ...sDaoPartitioningYearsAlwaysExistsTest.java | 12 ++++ .../timeseries/NoSqlTsPartitionDateTest.java | 41 ++++++++++++ 7 files changed, 162 insertions(+), 15 deletions(-) create mode 100644 dao/src/test/java/org/thingsboard/server/dao/timeseries/NoSqlTsPartitionDateTest.java diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 9fcdd0c166..660e9e3a59 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -303,8 +303,11 @@ cassandra: default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}" # Specify partitioning size for timestamp key-value storage. Example: MINUTES, HOURS, DAYS, MONTHS, INDEFINITE ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}" - # Enable/Disable timestamp key-value partioning on read queries + # Enable/Disable timestamp key-value partitioning on read queries use_ts_key_value_partitioning_on_read: "${USE_TS_KV_PARTITIONING_ON_READ:true}" + # Safety trigger to fall back to use_ts_key_value_partitioning_on_read as true if estimated partitions count is greater than safety trigger value. + # It helps to prevent building huge partition list (OOM) for corner cases (like from 0 to infinity) and prefer fewer reads strategy from NoSQL database + use_ts_key_value_partitioning_on_read_max_estimated_partition_count: "${USE_TS_KV_PARTITIONING_ON_READ_MAX_ESTIMATED_PARTITION_COUNT:40}" # The number of partitions that are cached in memory of each service. It is useful to decrease the load of re-inserting the same partitions again ts_key_value_partitions_max_cache_size: "${TS_KV_PARTITIONS_MAX_CACHE_SIZE:100000}" # Timeseries Time To Live (in seconds) for Cassandra Record. 0 - record has never expired diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java index f7bd64bef8..6818994629 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDao.java @@ -113,6 +113,10 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD @Value("${cassandra.query.use_ts_key_value_partitioning_on_read:true}") private boolean useTsKeyValuePartitioningOnRead; + @Getter + @Value("${cassandra.query.use_ts_key_value_partitioning_on_read_max_estimated_partition_count:40}") // 3+ years for MONTHS + private int useTsKeyValuePartitioningOnReadMaxEstimatedPartitionCount; + @Value("${cassandra.query.ts_key_value_partitions_max_cache_size:100000}") private long partitionsCacheSize; @@ -415,22 +419,41 @@ public class CassandraBaseTimeseriesDao extends AbstractCassandraBaseTimeseriesD readResultsProcessingExecutor); } - private ListenableFuture> getPartitionsFuture(TenantId tenantId, TsKvQuery query, EntityId entityId, long minPartition, long maxPartition) { + ListenableFuture> getPartitionsFuture(TenantId tenantId, TsKvQuery query, EntityId entityId, long minPartition, long maxPartition) { if (isFixedPartitioning()) { //no need to fetch partitions from DB return Futures.immediateFuture(FIXED_PARTITION); } if (!isUseTsKeyValuePartitioningOnRead()) { - return Futures.immediateFuture(calculatePartitions(minPartition, maxPartition)); + final long estimatedPartitionCount = estimatePartitionCount(minPartition, maxPartition); + if (estimatedPartitionCount <= useTsKeyValuePartitioningOnReadMaxEstimatedPartitionCount) { + return Futures.immediateFuture(calculatePartitions(minPartition, maxPartition, (int) estimatedPartitionCount)); + } } + return getPartitionsFromDB(tenantId, query, entityId, minPartition, maxPartition); + } + + ListenableFuture> getPartitionsFromDB(TenantId tenantId, TsKvQuery query, EntityId entityId, long minPartition, long maxPartition) { TbResultSetFuture partitionsFuture = fetchPartitions(tenantId, entityId, query.getKey(), minPartition, maxPartition); return Futures.transformAsync(partitionsFuture, getPartitionsArrayFunction(), readResultsProcessingExecutor); } + // Optimistic estimation of partition count, expected to be never called for infinite partitioning + long estimatePartitionCount(long minPartition, long maxPartition) { + if (maxPartition > minPartition) { + return (maxPartition - minPartition) / tsFormat.getDurationMs() + 2; //at least 2 partitions, at max 2 partitions overestimated + } + return 1; // 1 or 0, but 1 is more optimistic + } + List calculatePartitions(long minPartition, long maxPartition) { + return calculatePartitions(minPartition, maxPartition, 0); + } + + List calculatePartitions(long minPartition, long maxPartition, int estimatedPartitionCount) { if (minPartition == maxPartition) { return Collections.singletonList(minPartition); } - List partitions = new ArrayList<>(); + List partitions = estimatedPartitionCount > 0 ? new ArrayList<>(estimatedPartitionCount) : new ArrayList<>(); long currentPartition = minPartition; LocalDateTime currentPartitionTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(currentPartition), ZoneOffset.UTC); diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/NoSqlTsPartitionDate.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/NoSqlTsPartitionDate.java index 873c33d4e3..c403a46df1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/NoSqlTsPartitionDate.java +++ b/dao/src/main/java/org/thingsboard/server/dao/timeseries/NoSqlTsPartitionDate.java @@ -15,32 +15,29 @@ */ package org.thingsboard.server.dao.timeseries; +import lombok.Getter; + import java.time.LocalDateTime; import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalUnit; import java.util.Optional; +import java.util.concurrent.TimeUnit; +@Getter public enum NoSqlTsPartitionDate { MINUTES("yyyy-MM-dd-HH-mm", ChronoUnit.MINUTES), HOURS("yyyy-MM-dd-HH", ChronoUnit.HOURS), DAYS("yyyy-MM-dd", ChronoUnit.DAYS), MONTHS("yyyy-MM", ChronoUnit.MONTHS), YEARS("yyyy", ChronoUnit.YEARS),INDEFINITE("",ChronoUnit.FOREVER); private final String pattern; private final transient TemporalUnit truncateUnit; + private final transient long durationMs; public final static LocalDateTime EPOCH_START = LocalDateTime.ofEpochSecond(0,0, ZoneOffset.UTC); NoSqlTsPartitionDate(String pattern, TemporalUnit truncateUnit) { this.pattern = pattern; this.truncateUnit = truncateUnit; - } - - - public String getPattern() { - return pattern; - } - - public TemporalUnit getTruncateUnit() { - return truncateUnit; + this.durationMs = TimeUnit.SECONDS.toMillis(this.truncateUnit.getDuration().getSeconds()); } public LocalDateTime truncatedTo(LocalDateTime time) { diff --git a/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMinutesAlwaysExistsTest.java b/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMinutesAlwaysExistsTest.java index cc76484d0b..8ec8c493af 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMinutesAlwaysExistsTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMinutesAlwaysExistsTest.java @@ -117,4 +117,16 @@ public class CassandraBaseTimeseriesDaoPartitioningMinutesAlwaysExistsTest { ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2022-10-10T00:10:00Z").getTime())); } + @Test + public void testEstimatePartitionCount() throws ParseException { + assertThat(tsDao.estimatePartitionCount(0, Long.MAX_VALUE)).as("centuries").isEqualTo(153_722_867_280_914L); + assertThat(tsDao.estimatePartitionCount(0, 0)).as("single").isEqualTo(1L); + long startTs = tsDao.toPartitionTs( + ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-12T00:00:00Z").getTime()); + long endTs = tsDao.toPartitionTs( + ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-31T23:59:59Z").getTime()); + assertThat(tsDao.estimatePartitionCount(startTs, endTs)).as("600,479 minutes + 2 spare periods").isEqualTo(600479 + 2); + assertThat(tsDao.estimatePartitionCount(endTs, startTs)).as("wrong period estimated as 1").isEqualTo(1L); + } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest.java b/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest.java index eb60000404..65ed8019c0 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest.java @@ -15,25 +15,35 @@ */ package org.thingsboard.server.dao.timeseries; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.context.junit4.SpringRunner; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.kv.TsKvQuery; import org.thingsboard.server.dao.cassandra.CassandraCluster; import org.thingsboard.server.dao.nosql.CassandraBufferedRateReadExecutor; import org.thingsboard.server.dao.nosql.CassandraBufferedRateWriteExecutor; import java.text.ParseException; import java.util.List; +import java.util.UUID; import static org.apache.commons.lang3.time.DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; @RunWith(SpringRunner.class) @SpringBootTest(classes = CassandraBaseTimeseriesDao.class) @@ -50,7 +60,7 @@ import static org.assertj.core.api.Assertions.assertThat; @Slf4j public class CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest { - @Autowired + @MockitoSpyBean CassandraBaseTimeseriesDao tsDao; @MockBean(answer = Answers.RETURNS_MOCKS) @@ -131,4 +141,53 @@ public class CassandraBaseTimeseriesDaoPartitioningMonthsAlwaysExistsTest { ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-01T00:00:00Z").getTime())); } + @Test + public void testEstimatePartitionCount() throws ParseException { + assertThat(tsDao.estimatePartitionCount(0, Long.MAX_VALUE)).as("centuries").isEqualTo(3_507_324_297L); + assertThat(tsDao.estimatePartitionCount(0, 0)).as("single").isEqualTo(1L); + long startTs = tsDao.toPartitionTs( + ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-12T00:00:00Z").getTime()); + long endTs = tsDao.toPartitionTs( + ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-31T23:59:59Z").getTime()); + assertThat(tsDao.estimatePartitionCount(startTs, endTs)).as("13 month + 2 spare periods").isEqualTo(13 + 2); + assertThat(tsDao.estimatePartitionCount(endTs, startTs)).as("wrong period estimated as 1").isEqualTo(1L); + } + + @Test + public void testGetPartitionsFutureModeratePartitionsCount() throws ParseException { + TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + TsKvQuery query = mock(TsKvQuery.class); + long startTs = tsDao.toPartitionTs( + ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-12T00:00:00Z").getTime()); + long endTs = tsDao.toPartitionTs( + ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-31T23:59:59Z").getTime()); + + willReturn(mock(ListenableFuture.class)).given(tsDao).getPartitionsFromDB(tenantId, query, tenantId, startTs, endTs); + + tsDao.getPartitionsFuture(tenantId, query, tenantId, startTs, endTs); + + verify(tsDao).estimatePartitionCount(startTs, endTs); + verify(tsDao).calculatePartitions(eq(startTs), eq(endTs), anyInt()); + verify(tsDao, never()).getPartitionsFromDB(tenantId, query, tenantId, startTs, endTs); + } + + @Test + public void testGetPartitionsFutureHugePartitionsCountPreventOOMFallbackToDB() throws ParseException { + + TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + TsKvQuery query = mock(TsKvQuery.class); + long startTs = tsDao.toPartitionTs( + ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2000-12-12T00:00:00Z").getTime()); + long endTs = tsDao.toPartitionTs( + ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("3000-01-31T23:59:59Z").getTime()); + + willReturn(mock(ListenableFuture.class)).given(tsDao).getPartitionsFromDB(tenantId, query, tenantId, startTs, endTs); + + tsDao.getPartitionsFuture(tenantId, query, tenantId, startTs, endTs); + + verify(tsDao).estimatePartitionCount(startTs, endTs); + verify(tsDao, never()).calculatePartitions(eq(startTs), eq(endTs), anyInt()); + verify(tsDao).getPartitionsFromDB(tenantId, query, tenantId, startTs, endTs); + } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningYearsAlwaysExistsTest.java b/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningYearsAlwaysExistsTest.java index 0425b23a1e..aa3e48c73f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningYearsAlwaysExistsTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesDaoPartitioningYearsAlwaysExistsTest.java @@ -112,4 +112,16 @@ public class CassandraBaseTimeseriesDaoPartitioningYearsAlwaysExistsTest { ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2025-01-01T00:00:00Z").getTime())); } + @Test + public void testEstimatePartitionCount() throws ParseException { + assertThat(tsDao.estimatePartitionCount(0, Long.MAX_VALUE)).as("centuries").isEqualTo(292_277_026L); + assertThat(tsDao.estimatePartitionCount(0, 0)).as("single").isEqualTo(1L); + long startTs = tsDao.toPartitionTs( + ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2019-12-12T00:00:00Z").getTime()); + long endTs = tsDao.toPartitionTs( + ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse("2021-01-31T23:59:59Z").getTime()); + assertThat(tsDao.estimatePartitionCount(startTs, endTs)).as("2 years + 2 spare periods").isEqualTo(2 + 2); + assertThat(tsDao.estimatePartitionCount(endTs, startTs)).as("wrong period estimated as 1").isEqualTo(1L); + } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/timeseries/NoSqlTsPartitionDateTest.java b/dao/src/test/java/org/thingsboard/server/dao/timeseries/NoSqlTsPartitionDateTest.java new file mode 100644 index 0000000000..f860d6ad51 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/timeseries/NoSqlTsPartitionDateTest.java @@ -0,0 +1,41 @@ +/** + * 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.timeseries; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class NoSqlTsPartitionDateTest { + + @ParameterizedTest + @EnumSource(NoSqlTsPartitionDate.class) + void getDurationMsTest(NoSqlTsPartitionDate tsPartitionDate) throws Exception { + final Long durationMs = switch (tsPartitionDate) { + case MINUTES -> 60000L; + case HOURS -> 3600000L; + case DAYS -> 86400000L; + case MONTHS -> 2629746000L; + case YEARS -> 31556952000L; + case INDEFINITE -> Long.MAX_VALUE; + default -> null; //should be here in case a new enum value will be added in future + }; + assertThat(durationMs).isNotNull(); + assertThat(tsPartitionDate.getDurationMs()).isEqualTo(durationMs); + } + +} From 2c06aa475f3241bc474a9ce8aee46490eec02bc3 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Tue, 30 Sep 2025 16:26:46 +0300 Subject: [PATCH 185/216] geofencing cf bugfixes --- .../main/data/upgrade/basic/schema_update.sql | 2 +- ...ulatedFieldDynamicArgumentsRefreshMsg.java | 35 -------- .../CalculatedFieldEntityActor.java | 3 - ...CalculatedFieldEntityMessageProcessor.java | 50 +++++------ .../CalculatedFieldManagerActor.java | 3 - ...alculatedFieldManagerMessageProcessor.java | 62 ------------- ...ulatedFieldDynamicArgumentsRefreshMsg.java | 37 -------- ...tractCalculatedFieldProcessingService.java | 4 + .../ctx/state/BaseCalculatedFieldState.java | 4 +- .../cf/ctx/state/CalculatedFieldCtx.java | 42 +++++++-- .../cf/ctx/state/CalculatedFieldState.java | 7 -- .../ctx/state/SimpleCalculatedFieldState.java | 5 +- .../geofencing/GeofencingArgumentEntry.java | 10 ++- .../GeofencingCalculatedFieldState.java | 87 ++++++++----------- .../cf/CalculatedFieldIntegrationTest.java | 10 ++- .../server/controller/AbstractWebTest.java | 12 +-- .../GeofencingCalculatedFieldStateTest.java | 6 +- .../state/SimpleCalculatedFieldStateTest.java | 3 +- ...SupportedCalculatedFieldConfiguration.java | 3 - ...eofencingCalculatedFieldConfiguration.java | 7 +- .../DefaultTenantProfileConfiguration.java | 2 +- ...ncingCalculatedFieldConfigurationTest.java | 27 ------ .../server/common/msg/MsgType.java | 5 +- .../service/CalculatedFieldServiceTest.java | 72 ++++----------- 24 files changed, 148 insertions(+), 350 deletions(-) delete mode 100644 application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldDynamicArgumentsRefreshMsg.java delete mode 100644 application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldDynamicArgumentsRefreshMsg.java diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index 320d3e5bdd..0add4c0545 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -27,7 +27,7 @@ SET profile_data = jsonb_set( CASE WHEN (profile_data -> 'configuration') ? 'minAllowedScheduledUpdateIntervalInSecForCF' THEN NULL - ELSE to_jsonb(3600) + ELSE to_jsonb(60) END, 'maxRelationLevelPerCfArgument', CASE diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldDynamicArgumentsRefreshMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldDynamicArgumentsRefreshMsg.java deleted file mode 100644 index 301fe22dfb..0000000000 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldDynamicArgumentsRefreshMsg.java +++ /dev/null @@ -1,35 +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.actors.calculatedField; - -import lombok.Data; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.MsgType; -import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; - -@Data -public class CalculatedFieldDynamicArgumentsRefreshMsg implements ToCalculatedFieldSystemMsg { - - private final TenantId tenantId; - private final CalculatedFieldId cfId; - - @Override - public MsgType getMsgType() { - return MsgType.CF_DYNAMIC_ARGUMENTS_REFRESH_MSG; - } - -} diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java index 2a5f3c3cfd..c57984ef3d 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityActor.java @@ -75,9 +75,6 @@ public class CalculatedFieldEntityActor extends AbstractCalculatedFieldActor { case CF_LINKED_TELEMETRY_MSG: processor.process((EntityCalculatedFieldLinkedTelemetryMsg) msg); break; - case CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG: - processor.process((EntityCalculatedFieldDynamicArgumentsRefreshMsg) msg); - break; default: return false; } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java index 7513ca41e2..f8b61a082f 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldEntityMessageProcessor.java @@ -49,6 +49,7 @@ import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingArgumentEntry; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; import java.util.ArrayList; import java.util.Collection; @@ -227,18 +228,6 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM } } - public void process(EntityCalculatedFieldDynamicArgumentsRefreshMsg msg) throws CalculatedFieldException { - log.debug("[{}][{}] Processing CF dynamic arguments refresh msg.", entityId, msg.getCfId()); - CalculatedFieldState currentState = states.get(msg.getCfId()); - if (currentState == null) { - log.debug("[{}][{}] Failed to find CF state for entity.", entityId, msg.getCfId()); - } else { - currentState.setDirty(true); - log.debug("[{}][{}] CF state marked as dirty.", entityId, msg.getCfId()); - } - msg.getCallback().onSuccess(); - } - private void processTelemetry(CalculatedFieldCtx ctx, CalculatedFieldTelemetryMsgProto proto, List cfIdList, MultipleTbCallback callback) throws CalculatedFieldException { processArgumentValuesUpdate(ctx, cfIdList, callback, mapToArguments(ctx, proto.getTsDataList()), toTbMsgId(proto), toTbMsgType(proto)); } @@ -266,12 +255,13 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM if (state == null) { state = getOrInitState(ctx); justRestored = true; - } else if (state.isDirty()) { - log.debug("[{}][{}] Going to update dirty CF state.", entityId, ctx.getCfId()); + } else if (ctx.shouldFetchDynamicArgumentsFromDb(state)) { + log.debug("[{}][{}] Going to update dynamic arguments for CF.", entityId, ctx.getCfId()); try { Map dynamicArgsFromDb = cfService.fetchDynamicArgsFromDb(ctx, entityId); dynamicArgsFromDb.forEach(newArgValues::putIfAbsent); - state.setDirty(false); + var geofencingState = (GeofencingCalculatedFieldState) state; + geofencingState.setLastDynamicArgumentsRefreshTs(System.currentTimeMillis()); } catch (Exception e) { throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).cause(e).build(); } @@ -403,7 +393,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM return mapToArguments(entityId, argNames, geofencingArgumentNames, scope, attrDataList); } - private Map mapToArguments(EntityId entityId, Map argNames, List geoArgNames, AttributeScopeProto scope, List attrDataList) { + private Map mapToArguments(EntityId entityId, Map argNames, List geofencingArgNames, AttributeScopeProto scope, List attrDataList) { Map arguments = new HashMap<>(); for (AttributeValueProto item : attrDataList) { ReferencedEntityKey key = new ReferencedEntityKey(item.getKey(), ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name())); @@ -411,7 +401,7 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM if (argName == null) { continue; } - if (geoArgNames.contains(argName)) { + if (geofencingArgNames.contains(argName)) { arguments.put(argName, new GeofencingArgumentEntry(entityId, item)); continue; } @@ -425,26 +415,32 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM if (argNames.isEmpty()) { return Collections.emptyMap(); } - return mapToArgumentsWithDefaultValue(argNames, ctx.getArguments(), scope, removedAttrKeys); + List geofencingArgumentNames = ctx.getLinkedEntityGeofencingArgumentNames(); + return mapToArgumentsWithDefaultValue(argNames, ctx.getArguments(), geofencingArgumentNames, scope, removedAttrKeys); } private Map mapToArgumentsWithDefaultValue(CalculatedFieldCtx ctx, AttributeScopeProto scope, List removedAttrKeys) { - return mapToArgumentsWithDefaultValue(ctx.getMainEntityArguments(), ctx.getArguments(), scope, removedAttrKeys); + return mapToArgumentsWithDefaultValue(ctx.getMainEntityArguments(), ctx.getArguments(), ctx.getMainEntityGeofencingArgumentNames(), scope, removedAttrKeys); } - private Map mapToArgumentsWithDefaultValue(Map argNames, Map configArguments, AttributeScopeProto scope, List removedAttrKeys) { + private Map mapToArgumentsWithDefaultValue(Map argNames, Map configArguments, List geofencingArgNames, AttributeScopeProto scope, List removedAttrKeys) { Map arguments = new HashMap<>(); for (String removedKey : removedAttrKeys) { ReferencedEntityKey key = new ReferencedEntityKey(removedKey, ArgumentType.ATTRIBUTE, AttributeScope.valueOf(scope.name())); String argName = argNames.get(key); - if (argName != null) { - Argument argument = configArguments.get(argName); - String defaultValue = (argument != null) ? argument.getDefaultValue() : null; - arguments.put(argName, StringUtils.isNotEmpty(defaultValue) - ? new SingleValueArgumentEntry(System.currentTimeMillis(), new StringDataEntry(removedKey, defaultValue), null) - : new SingleValueArgumentEntry()); - + if (argName == null) { + continue; } + if (geofencingArgNames.contains(argName)) { + arguments.put(argName, new GeofencingArgumentEntry()); + continue; + } + Argument argument = configArguments.get(argName); + String defaultValue = (argument != null) ? argument.getDefaultValue() : null; + arguments.put(argName, StringUtils.isNotEmpty(defaultValue) + ? new SingleValueArgumentEntry(System.currentTimeMillis(), new StringDataEntry(removedKey, defaultValue), null) + : new SingleValueArgumentEntry()); + } return arguments; } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java index ab6cb34176..7d2ae0ff44 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerActor.java @@ -79,9 +79,6 @@ public class CalculatedFieldManagerActor extends AbstractCalculatedFieldActor { case CF_LINKED_TELEMETRY_MSG: processor.onLinkedTelemetryMsg((CalculatedFieldLinkedTelemetryMsg) msg); break; - case CF_DYNAMIC_ARGUMENTS_REFRESH_MSG: - processor.onDynamicArgumentsRefreshMsg((CalculatedFieldDynamicArgumentsRefreshMsg) msg); - break; default: return false; } diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index 75ca0b4c9b..7a76cb9821 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -27,7 +27,6 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.ProfileEntityIdInfo; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.CalculatedFieldLink; -import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.CalculatedFieldId; import org.thingsboard.server.common.data.id.DeviceId; @@ -57,10 +56,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import static org.thingsboard.server.utils.CalculatedFieldUtils.fromProto; @@ -74,7 +70,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware private final Map calculatedFields = new HashMap<>(); private final Map> entityIdCalculatedFields = new HashMap<>(); private final Map> entityIdCalculatedFieldLinks = new HashMap<>(); - private final Map> cfDynamicArgumentsRefreshTasks = new ConcurrentHashMap<>(); private final CalculatedFieldProcessingService cfExecService; private final CalculatedFieldStateService cfStateService; @@ -113,8 +108,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware calculatedFields.clear(); entityIdCalculatedFields.clear(); entityIdCalculatedFieldLinks.clear(); - cfDynamicArgumentsRefreshTasks.values().forEach(future -> future.cancel(true)); - cfDynamicArgumentsRefreshTasks.clear(); ctx.stop(ctx.getSelf()); } @@ -274,7 +267,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx); addLinks(cf); - scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx); applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> initCfForEntity(id, cfCtx, false, cb)); } } @@ -304,12 +296,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware calculatedFields.put(newCf.getId(), newCfCtx); List oldCfList = entityIdCalculatedFields.get(newCf.getEntityId()); - boolean hasSchedulingConfigChanges = newCfCtx.hasSchedulingConfigChanges(oldCfCtx); - if (hasSchedulingConfigChanges) { - cancelCfDynamicArgumentsRefreshTaskIfExists(cfId, false); - scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(newCfCtx); - } - List newCfList = new CopyOnWriteArrayList<>(); boolean found = false; for (CalculatedFieldCtx oldCtx : oldCfList) { @@ -350,19 +336,9 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware } entityIdCalculatedFields.get(cfCtx.getEntityId()).remove(cfCtx); deleteLinks(cfCtx); - cancelCfDynamicArgumentsRefreshTaskIfExists(cfId, true); applyToTargetCfEntityActors(cfCtx, callback, (id, cb) -> deleteCfForEntity(id, cfId, cb)); } - private void cancelCfDynamicArgumentsRefreshTaskIfExists(CalculatedFieldId cfId, boolean cfDeleted) { - var existingTask = cfDynamicArgumentsRefreshTasks.remove(cfId); - if (existingTask != null) { - existingTask.cancel(false); - String reason = cfDeleted ? "deletion" : "update"; - log.debug("[{}][{}] Cancelled dynamic arguments refresh task due to CF {}!", tenantId, cfId, reason); - } - } - public void onTelemetryMsg(CalculatedFieldTelemetryMsg msg) { EntityId entityId = msg.getEntityId(); log.debug("Received telemetry msg from entity [{}]", entityId); @@ -442,43 +418,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware return result; } - private void scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(CalculatedFieldCtx cfCtx) { - CalculatedField cf = cfCtx.getCalculatedField(); - if (!(cf.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledCfConfig)) { - return; - } - if (!scheduledCfConfig.isScheduledUpdateEnabled()) { - return; - } - if (cfDynamicArgumentsRefreshTasks.containsKey(cf.getId())) { - log.debug("[{}][{}] Dynamic arguments refresh task for CF already exists!", tenantId, cf.getId()); - return; - } - long refreshDynamicSourceInterval = TimeUnit.SECONDS.toMillis(scheduledCfConfig.getScheduledUpdateInterval()); - var scheduledMsg = new CalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfCtx.getCfId()); - - ScheduledFuture scheduledFuture = systemContext - .schedulePeriodicMsgWithDelay(ctx, scheduledMsg, refreshDynamicSourceInterval, refreshDynamicSourceInterval); - cfDynamicArgumentsRefreshTasks.put(cf.getId(), scheduledFuture); - log.debug("[{}][{}] Scheduled dynamic arguments refresh task for CF!", tenantId, cf.getId()); - } - - public void onDynamicArgumentsRefreshMsg(CalculatedFieldDynamicArgumentsRefreshMsg msg) { - log.debug("[{}] [{}] Processing CF dynamic arguments refresh task.", tenantId, msg.getCfId()); - CalculatedFieldCtx cfCtx = calculatedFields.get(msg.getCfId()); - if (cfCtx == null) { - log.debug("[{}][{}] Failed to find CF context, going to stop dynamic arguments refresh task for CF.", tenantId, msg.getCfId()); - cancelCfDynamicArgumentsRefreshTaskIfExists(msg.getCfId(), true); - return; - } - applyToTargetCfEntityActors(cfCtx, msg.getCallback(), (id, cb) -> refreshDynamicArgumentsForEntity(id, msg.getCfId(), cb)); - } - - private void refreshDynamicArgumentsForEntity(EntityId entityId, CalculatedFieldId cfId, TbCallback callback) { - log.debug("Pushing CF dynamic arguments refresh msg to specific actor [{}]", entityId); - getOrCreateActor(entityId).tell(new EntityCalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfId, callback)); - } - private void linkedTelemetryMsgForEntity(EntityId entityId, EntityCalculatedFieldLinkedTelemetryMsg msg) { log.debug("Pushing linked telemetry msg to specific actor [{}]", entityId); getOrCreateActor(entityId).tell(msg); @@ -565,7 +504,6 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware // We use copy on write lists to safely pass the reference to another actor for the iteration. // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx); - scheduleDynamicArgumentsRefreshTaskForCfIfNeeded(cfCtx); } private void initCalculatedFieldLink(CalculatedFieldLink link) { diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldDynamicArgumentsRefreshMsg.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldDynamicArgumentsRefreshMsg.java deleted file mode 100644 index fdf864611f..0000000000 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/EntityCalculatedFieldDynamicArgumentsRefreshMsg.java +++ /dev/null @@ -1,37 +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.actors.calculatedField; - -import lombok.Data; -import org.thingsboard.server.common.data.id.CalculatedFieldId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.msg.MsgType; -import org.thingsboard.server.common.msg.ToCalculatedFieldSystemMsg; -import org.thingsboard.server.common.msg.queue.TbCallback; - -@Data -public class EntityCalculatedFieldDynamicArgumentsRefreshMsg implements ToCalculatedFieldSystemMsg { - - private final TenantId tenantId; - private final CalculatedFieldId cfId; - private final TbCallback callback; - - @Override - public MsgType getMsgType() { - return MsgType.CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG; - } - -} diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java index 45305ca9e3..8709e0cb68 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java @@ -45,6 +45,7 @@ import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; import java.util.HashMap; import java.util.List; @@ -102,6 +103,9 @@ public abstract class AbstractCalculatedFieldProcessingService { return Futures.whenAllComplete(argFutures.values()).call(() -> { var result = createStateByType(ctx); result.updateState(ctx, resolveArgumentFutures(argFutures)); + if (ctx.hasRelationQueryDynamicArguments() && result instanceof GeofencingCalculatedFieldState geofencingCalculatedFieldState) { + geofencingCalculatedFieldState.setLastDynamicArgumentsRefreshTs(System.currentTimeMillis()); + } return result; }, MoreExecutors.directExecutor()); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java index 9a1d06cf24..6d877331bd 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/BaseCalculatedFieldState.java @@ -62,7 +62,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { boolean entryUpdated; if (existingEntry == null || newEntry.isForceResetPrevious()) { - validateNewEntry(newEntry); + validateNewEntry(key, newEntry); arguments.put(key, newEntry); entryUpdated = true; } else { @@ -93,7 +93,7 @@ public abstract class BaseCalculatedFieldState implements CalculatedFieldState { } } - protected void validateNewEntry(ArgumentEntry newEntry) {} + protected void validateNewEntry(String key, ArgumentEntry newEntry) {} private void updateLastUpdateTimestamp(ArgumentEntry entry) { long newTs = this.latestTimestamp; diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index c2cc083853..13012a028a 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -43,11 +43,13 @@ import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.gen.transport.TransportProtos.CalculatedFieldTelemetryMsgProto; import org.thingsboard.server.service.cf.ctx.CalculatedFieldEntityCtxId; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions; @@ -78,9 +80,12 @@ public class CalculatedFieldCtx { private long maxStateSize; private long maxSingleValueArgumentSize; + private boolean relationQueryDynamicArguments; private List mainEntityGeofencingArgumentNames; private List linkedEntityGeofencingArgumentNames; + private long scheduledUpdateIntervalMillis; + public CalculatedFieldCtx(CalculatedField calculatedField, TbelInvokeService tbelInvokeService, ApiLimitService apiLimitService, RelationService relationService) { this.calculatedField = calculatedField; @@ -101,6 +106,7 @@ public class CalculatedFieldCtx { var refId = entry.getValue().getRefEntityId(); var refKey = entry.getValue().getRefEntityKey(); if (refId == null && entry.getValue().hasDynamicSource()) { + relationQueryDynamicArguments = true; continue; } if (refId == null || refId.equals(calculatedField.getEntityId())) { @@ -126,6 +132,9 @@ public class CalculatedFieldCtx { }); } } + if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration scheduledConfig) { + this.scheduledUpdateIntervalMillis = scheduledConfig.isScheduledUpdateEnabled() ? TimeUnit.SECONDS.toMillis(scheduledConfig.getScheduledUpdateInterval()) : -1L; + } this.tbelInvokeService = tbelInvokeService; this.relationService = relationService; @@ -329,25 +338,42 @@ public class CalculatedFieldCtx { public boolean hasOtherSignificantChanges(CalculatedFieldCtx other) { boolean expressionChanged = calculatedField.getConfiguration() instanceof ExpressionBasedCalculatedFieldConfiguration && !expression.equals(other.expression); boolean outputChanged = !output.equals(other.output); - return expressionChanged || outputChanged; + boolean scheduledUpdatesConfigChanged = scheduledUpdateIntervalMillis != other.scheduledUpdateIntervalMillis; + return expressionChanged || outputChanged || scheduledUpdatesConfigChanged; } public boolean hasStateChanges(CalculatedFieldCtx other) { boolean typeChanged = !cfType.equals(other.cfType); boolean argumentsChanged = !arguments.equals(other.arguments); - return typeChanged || argumentsChanged; + boolean geoZoneGroupsConfigChanged = hasGeofencingZoneGroupConfigurationChanges(other); + return typeChanged || argumentsChanged || geoZoneGroupsConfigChanged; } - public boolean hasSchedulingConfigChanges(CalculatedFieldCtx other) { - if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration thisConfig - && other.calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration otherConfig) { - boolean refreshTriggerChanged = thisConfig.isScheduledUpdateEnabled() != otherConfig.isScheduledUpdateEnabled(); - boolean refreshIntervalChanged = thisConfig.getScheduledUpdateInterval() != otherConfig.getScheduledUpdateInterval(); - return refreshTriggerChanged || refreshIntervalChanged; + private boolean hasGeofencingZoneGroupConfigurationChanges(CalculatedFieldCtx other) { + if (calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration thisConfig + && other.calculatedField.getConfiguration() instanceof GeofencingCalculatedFieldConfiguration otherConfig) { + return !thisConfig.getZoneGroups().equals(otherConfig.getZoneGroups()); } return false; } + public boolean hasRelationQueryDynamicArguments() { + return relationQueryDynamicArguments && scheduledUpdateIntervalMillis != -1; + } + + public boolean shouldFetchDynamicArgumentsFromDb(CalculatedFieldState state) { + if (!hasRelationQueryDynamicArguments()) { + return false; + } + if (!(state instanceof GeofencingCalculatedFieldState geofencingState)) { + return false; + } + if (geofencingState.getLastDynamicArgumentsRefreshTs() == -1L) { + return true; + } + return geofencingState.getLastDynamicArgumentsRefreshTs() < System.currentTimeMillis() - scheduledUpdateIntervalMillis; + } + public String getSizeExceedsLimitMessage() { return "Failed to init CF state. State size exceeds limit of " + (maxStateSize / 1024) + "Kb!"; } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java index 5f8e7538c4..3e0964bfd2 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldState.java @@ -50,13 +50,6 @@ public interface CalculatedFieldState { long getLatestTimestamp(); - default void setDirty(boolean dirty) { - } - - default boolean isDirty() { - return false; - } - void setRequiredArguments(List requiredArguments); boolean updateState(CalculatedFieldCtx ctx, Map argumentValues); diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java index 80b650fc7c..5a437b52f3 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldState.java @@ -48,9 +48,10 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState { } @Override - protected void validateNewEntry(ArgumentEntry newEntry) { + protected void validateNewEntry(String key, ArgumentEntry newEntry) { if (newEntry instanceof TsRollingArgumentEntry) { - throw new IllegalArgumentException("Rolling argument entry is not supported for simple calculated fields."); + throw new IllegalArgumentException("Unsupported argument type detected for argument: " + key + ". " + + "Rolling argument entry is not supported for simple calculated fields."); } } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java index f526cc00ab..53e5c19e72 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingArgumentEntry.java @@ -41,7 +41,7 @@ public class GeofencingArgumentEntry implements ArgumentEntry { } public GeofencingArgumentEntry(EntityId entityId, TransportProtos.AttributeValueProto entry) { - this.zoneStates = toZones(Map.of(entityId, ProtoUtils.fromProto(entry))); + this(Map.of(entityId, ProtoUtils.fromProto(entry))); } public GeofencingArgumentEntry(Map entityIdkvEntryMap) { @@ -63,6 +63,10 @@ public class GeofencingArgumentEntry implements ArgumentEntry { if (!(entry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) { throw new IllegalArgumentException("Unsupported argument entry type for geofencing argument entry: " + entry.getType()); } + if (geofencingArgumentEntry.isEmpty()) { + zoneStates.clear(); + return true; + } boolean updated = false; for (var zoneEntry : geofencingArgumentEntry.getZoneStates().entrySet()) { if (updateZone(zoneEntry)) { @@ -97,6 +101,10 @@ public class GeofencingArgumentEntry implements ArgumentEntry { zoneStates.put(zoneId, newZoneState); return true; } + if (newZoneState.getPerimeterDefinition() == null) { + zoneStates.remove(zoneId); + return true; + } return existingZoneState.update(newZoneState); } diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java index 506ddcff78..398de0c20b 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/geofencing/GeofencingCalculatedFieldState.java @@ -15,16 +15,19 @@ */ package org.thingsboard.server.service.cf.ctx.state.geofencing; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; 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.EqualsAndHashCode; +import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.geo.Coordinates; import org.thingsboard.server.common.data.cf.CalculatedFieldType; +import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingTransitionEvent; @@ -39,7 +42,6 @@ import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldCtx; import org.thingsboard.server.service.cf.ctx.state.SingleValueArgumentEntry; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -51,18 +53,14 @@ import static org.thingsboard.server.common.data.cf.configuration.geofencing.Geo @Data @Slf4j +@NoArgsConstructor @EqualsAndHashCode(callSuper = true) public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { - private boolean dirty; + private long lastDynamicArgumentsRefreshTs = -1; - public GeofencingCalculatedFieldState() { - super(new ArrayList<>(), new HashMap<>(), false, -1); - this.dirty = false; - } - - public GeofencingCalculatedFieldState(List argNames) { - super(argNames); + public GeofencingCalculatedFieldState(List requiredArguments) { + super(requiredArguments); } @Override @@ -71,49 +69,21 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { } @Override - public boolean updateState(CalculatedFieldCtx ctx, Map argumentValues) { - if (arguments == null) { - arguments = new HashMap<>(); - } - - boolean stateUpdated = false; - - for (var entry : argumentValues.entrySet()) { - String key = entry.getKey(); - ArgumentEntry newEntry = entry.getValue(); - - checkArgumentSize(key, newEntry, ctx); - - ArgumentEntry existingEntry = arguments.get(key); - boolean entryUpdated; - - if (existingEntry == null || newEntry.isForceResetPrevious()) { - entryUpdated = switch (key) { - case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> { - if (!(newEntry instanceof SingleValueArgumentEntry singleValueArgumentEntry)) { - throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " + - "Only SINGLE_VALUE type is allowed."); - } - arguments.put(key, singleValueArgumentEntry); - yield true; - } - default -> { - if (!(newEntry instanceof GeofencingArgumentEntry geofencingArgumentEntry)) { - throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " + - "Only GEOFENCING type is allowed."); - } - arguments.put(key, geofencingArgumentEntry); - yield true; - } - }; - } else { - entryUpdated = existingEntry.updateEntry(newEntry); + protected void validateNewEntry(String key, ArgumentEntry newEntry) { + switch (key) { + case ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY -> { + if (!(newEntry instanceof SingleValueArgumentEntry)) { + throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " + + "Only SINGLE_VALUE type is allowed."); + } } - if (entryUpdated) { - stateUpdated = true; + default -> { + if (!(newEntry instanceof GeofencingArgumentEntry)) { + throw new IllegalArgumentException("Unsupported argument entry type for " + key + " argument: " + newEntry.getType() + ". " + + "Only GEOFENCING type is allowed."); + } } } - return stateUpdated; } @Override @@ -125,7 +95,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { var geofencingCfg = (GeofencingCalculatedFieldConfiguration) ctx.getCalculatedField().getConfiguration(); Map zoneGroups = geofencingCfg.getZoneGroups(); - ObjectNode resultNode = JacksonUtil.newObjectNode(); + ObjectNode valuesNode = JacksonUtil.newObjectNode(); List> relationFutures = new ArrayList<>(); getGeofencingArguments().forEach((argumentKey, argumentEntry) -> { @@ -154,10 +124,11 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { relationFutures.add(f); } }); - updateResultNode(argumentKey, zoneResults, zoneGroupCfg.getReportStrategy(), resultNode); + updateValuesNode(argumentKey, zoneResults, zoneGroupCfg.getReportStrategy(), valuesNode); }); - var result = new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), resultNode); + OutputType outputType = ctx.getOutput().getType(); + var result = new CalculatedFieldResult(ctx.getOutput().getType(), ctx.getOutput().getScope(), toResultNode(outputType, valuesNode)); if (relationFutures.isEmpty()) { return Futures.immediateFuture(result); } @@ -171,7 +142,7 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { .collect(Collectors.toMap(Map.Entry::getKey, entry -> (GeofencingArgumentEntry) entry.getValue())); } - private void updateResultNode(String argumentKey, List zoneResults, GeofencingReportStrategy geofencingReportStrategy, ObjectNode resultNode) { + private void updateValuesNode(String argumentKey, List zoneResults, GeofencingReportStrategy geofencingReportStrategy, ObjectNode resultNode) { GeofencingEvalResult aggregationResult = aggregateZoneGroup(zoneResults); final String eventKey = argumentKey + "Event"; final String statusKey = argumentKey + "Status"; @@ -185,6 +156,16 @@ public class GeofencingCalculatedFieldState extends BaseCalculatedFieldState { } } + private JsonNode toResultNode(OutputType outputType, ObjectNode valuesNode) { + if (OutputType.ATTRIBUTES.equals(outputType) || latestTimestamp == -1) { + return valuesNode; + } + ObjectNode resultNode = JacksonUtil.newObjectNode(); + resultNode.put("ts", latestTimestamp); + resultNode.set("values", valuesNode); + return resultNode; + } + private GeofencingEvalResult aggregateZoneGroup(List zoneResults) { boolean nowInside = zoneResults.stream().anyMatch(r -> INSIDE.equals(r.status())); boolean prevInside = zoneResults.stream() diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index b6a1faa1ed..ffd876575c 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -831,10 +831,11 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes TenantProfile foundTenantProfile = doGet("/api/tenantProfile/" + tenantProfileEntityInfo.getId().getId().toString(), TenantProfile.class); assertThat(foundTenantProfile).isNotNull(); assertThat(foundTenantProfile.getDefaultProfileConfiguration()).isNotNull(); - foundTenantProfile.getDefaultProfileConfiguration().setMinAllowedScheduledUpdateIntervalInSecForCF(TIMEOUT / 10); + int minAllowedScheduledUpdateIntervalInSecForCF = TIMEOUT / 10; + foundTenantProfile.getDefaultProfileConfiguration().setMinAllowedScheduledUpdateIntervalInSecForCF(minAllowedScheduledUpdateIntervalInSecForCF); TenantProfile savedTenantProfile = doPost("/api/tenantProfile", foundTenantProfile, TenantProfile.class); assertThat(savedTenantProfile).isNotNull(); - assertThat(savedTenantProfile.getDefaultProfileConfiguration().getMinAllowedScheduledUpdateIntervalInSecForCF()).isEqualTo(TIMEOUT / 10); + assertThat(savedTenantProfile.getDefaultProfileConfiguration().getMinAllowedScheduledUpdateIntervalInSecForCF()).isEqualTo(minAllowedScheduledUpdateIntervalInSecForCF); loginTenantAdmin(); // --- Arrange entities --- @@ -884,7 +885,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes cfg.setOutput(out); // Enable scheduled refresh with a 6-second interval - cfg.setScheduledUpdateInterval(6); + cfg.setScheduledUpdateInterval(minAllowedScheduledUpdateIntervalInSecForCF); + cfg.setScheduledUpdateEnabled(true); cf.setConfiguration(cfg); CalculatedField savedCalculatedField = doPost("/api/calculatedField", cf, CalculatedField.class); @@ -935,7 +937,7 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes relAllowedB.setType("AllowedZone"); doPost("/api/relation", relAllowedB).andExpect(status().isOk()); - awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsDirty(device.getId(), savedCalculatedField.getId()); + awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsReadyToRefreshDynamicArguments(device.getId(), savedCalculatedField.getId(), minAllowedScheduledUpdateIntervalInSecForCF); // --- Same coordinates as before, but now we expect ENTERED since a new zone is registered --- doPost("/api/plugins/telemetry/DEVICE/" + device.getUuidId() + "/timeseries/unusedScope", diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index fd01581e36..d7c9ad4590 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -155,6 +155,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.queue.memory.InMemoryStorage; import org.thingsboard.server.service.cf.CfRocksDb; import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState; +import org.thingsboard.server.service.cf.ctx.state.geofencing.GeofencingCalculatedFieldState; import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest; @@ -1104,14 +1105,15 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { }); } - protected void awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsDirty(EntityId entityId, CalculatedFieldId cfId) { + protected void awaitForCalculatedFieldEntityMessageProcessorToRegisterCfStateAsReadyToRefreshDynamicArguments(EntityId entityId, CalculatedFieldId cfId, int scheduledUpdateInterval) { CalculatedFieldEntityMessageProcessor processor = getCalculatedFieldEntityMessageProcessor(entityId); Map statesMap = (Map) ReflectionTestUtils.getField(processor, "states"); - Awaitility.await("CF state for entity actor marked as dirty").atMost(5, TimeUnit.SECONDS).until(() -> { + Awaitility.await("CF state for entity actor ready to refresh dynamic arguments").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> { CalculatedFieldState calculatedFieldState = statesMap.get(cfId); - boolean stateDirty = calculatedFieldState != null && calculatedFieldState.isDirty(); - log.warn("entityId {}, cfId {}, state dirty == {}", entityId, cfId, stateDirty); - return stateDirty; + boolean isReady = calculatedFieldState != null && ((GeofencingCalculatedFieldState) calculatedFieldState).getLastDynamicArgumentsRefreshTs() + < System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(scheduledUpdateInterval); + log.warn("entityId {}, cfId {}, state ready to refresh == {}", entityId, cfId, isReady); + return isReady; }); } diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index 691a1f7ec4..a86af77555 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -257,7 +257,7 @@ public class GeofencingCalculatedFieldStateTest { assertThat(result2).isNotNull(); assertThat(result2.getType()).isEqualTo(output.getType()); assertThat(result2.getScope()).isEqualTo(output.getScope()); - assertThat(result2.getResult()).isEqualTo( + assertThat(result2.getResult().get("values")).isEqualTo( JacksonUtil.newObjectNode() .put("allowedZonesEvent", "LEFT") .put("allowedZonesStatus", "OUTSIDE") @@ -329,7 +329,7 @@ public class GeofencingCalculatedFieldStateTest { assertThat(result2).isNotNull(); assertThat(result2.getType()).isEqualTo(output.getType()); assertThat(result2.getScope()).isEqualTo(output.getScope()); - assertThat(result2.getResult()).isEqualTo( + assertThat(result2.getResult().get("values")).isEqualTo( JacksonUtil.newObjectNode() .put("allowedZonesEvent", "LEFT") .put("restrictedZonesEvent", "ENTERED") @@ -401,7 +401,7 @@ public class GeofencingCalculatedFieldStateTest { assertThat(result2).isNotNull(); assertThat(result2.getType()).isEqualTo(output.getType()); assertThat(result2.getScope()).isEqualTo(output.getScope()); - assertThat(result2.getResult()).isEqualTo( + assertThat(result2.getResult().get("values")).isEqualTo( JacksonUtil.newObjectNode() .put("allowedZonesStatus", "OUTSIDE") .put("restrictedZonesStatus", "INSIDE") diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java index 8c631ecf6f..3aef8896de 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/SimpleCalculatedFieldStateTest.java @@ -123,7 +123,8 @@ public class SimpleCalculatedFieldStateTest { Map newArgs = Map.of("key3", new TsRollingArgumentEntry(10, 30000L)); assertThatThrownBy(() -> state.updateState(ctx, newArgs)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Rolling argument entry is not supported for simple calculated fields."); + .hasMessage("Unsupported argument type detected for argument: key3. " + + "Rolling argument entry is not supported for simple calculated fields."); } @Test diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java index 7902a9cf5b..d0c5786f62 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/ScheduledUpdateSupportedCalculatedFieldConfiguration.java @@ -15,11 +15,8 @@ */ package org.thingsboard.server.common.data.cf.configuration; -import com.fasterxml.jackson.annotation.JsonIgnore; - public interface ScheduledUpdateSupportedCalculatedFieldConfiguration extends CalculatedFieldConfiguration { - @JsonIgnore boolean isScheduledUpdateEnabled(); int getScheduledUpdateInterval(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java index dc331f5876..b331abc50b 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfiguration.java @@ -34,6 +34,8 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal private EntityCoordinates entityCoordinates; private Map zoneGroups; + + private boolean scheduledUpdateEnabled; private int scheduledUpdateInterval; private Output output; @@ -61,11 +63,6 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal return output; } - @Override - public boolean isScheduledUpdateEnabled() { - return scheduledUpdateInterval > 0 && zoneGroups.values().stream().anyMatch(ZoneGroupConfiguration::hasDynamicSource); - } - @Override public void validate() { if (entityCoordinates == null) { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java index 4c8b9e06bd..c6bd9a7f38 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java @@ -173,7 +173,7 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura @Schema(example = "10") private long maxArgumentsPerCF = 10; @Schema(example = "3600") - private int minAllowedScheduledUpdateIntervalInSecForCF = 3600; + private int minAllowedScheduledUpdateIntervalInSecForCF = 60; @Schema(example = "10") private int maxRelationLevelPerCfArgument = 10; @Builder.Default diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java index 91a47aac57..c5fb1f8953 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/GeofencingCalculatedFieldConfigurationTest.java @@ -101,33 +101,6 @@ public class GeofencingCalculatedFieldConfigurationTest { verify(zoneGroupConfigurationB).validate(zoneGroupBName); } - @Test - void scheduledUpdateDisabledWhenIntervalIsZero() { - var cfg = new GeofencingCalculatedFieldConfiguration(); - cfg.setScheduledUpdateInterval(0); - assertThat(cfg.isScheduledUpdateEnabled()).isFalse(); - } - - @Test - void scheduledUpdateDisabledWhenIntervalIsGreaterThanZeroButNoZonesWithDynamicArguments() { - var cfg = new GeofencingCalculatedFieldConfiguration(); - var zoneGroupConfigurationMock = mock(ZoneGroupConfiguration.class); - when(zoneGroupConfigurationMock.hasDynamicSource()).thenReturn(false); - cfg.setZoneGroups(Map.of("someGroupName", zoneGroupConfigurationMock)); - cfg.setScheduledUpdateInterval(60); - assertThat(cfg.isScheduledUpdateEnabled()).isFalse(); - } - - @Test - void scheduledUpdateEnabledWhenIntervalIsGreaterThanZeroAndDynamicArgumentsPresent() { - var cfg = new GeofencingCalculatedFieldConfiguration(); - var zoneGroupConfigurationMock = mock(ZoneGroupConfiguration.class); - when(zoneGroupConfigurationMock.hasDynamicSource()).thenReturn(true); - cfg.setZoneGroups(Map.of("someGroupName", zoneGroupConfigurationMock)); - cfg.setScheduledUpdateInterval(60); - assertThat(cfg.isScheduledUpdateEnabled()).isTrue(); - } - @Test void testGetArgumentsOverride() { var cfg = new GeofencingCalculatedFieldConfiguration(); diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java index 48b07af29b..20043582d7 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/MsgType.java @@ -147,10 +147,7 @@ public enum MsgType { /* CF Manager Actor -> CF Entity actor */ CF_ENTITY_TELEMETRY_MSG, CF_ENTITY_INIT_CF_MSG, - CF_ENTITY_DELETE_MSG, - - CF_DYNAMIC_ARGUMENTS_REFRESH_MSG, - CF_ENTITY_DYNAMIC_ARGUMENTS_REFRESH_MSG; + CF_ENTITY_DELETE_MSG; @Getter private final boolean ignoreOnStart; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index 49bdcca0fb..7f563ea436 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java @@ -99,53 +99,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { } @Test - public void testSaveGeofencingCalculatedField_shouldNotChangeScheduledInterval() { - // Arrange a device - Device device = createTestDevice(); - - // Build a valid Geofencing configuration - GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); - - // Coordinates: TS_LATEST, no dynamic source - EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); - cfg.setEntityCoordinates(entityCoordinates); - - // Zone-group argument (ATTRIBUTE) — no DYNAMIC configuration, so no scheduling even if the scheduled interval is set - ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); - zoneGroupConfiguration.setRefEntityId(device.getId()); - cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration)); - - // Set a scheduled interval to some value - cfg.setScheduledUpdateInterval(600); - - // Create & save Calculated Field - CalculatedField cf = new CalculatedField(); - cf.setTenantId(tenantId); - cf.setEntityId(device.getId()); - cf.setType(CalculatedFieldType.GEOFENCING); - cf.setName("GF clamp test"); - cf.setConfigurationVersion(0); - cf.setConfiguration(cfg); - - CalculatedField saved = calculatedFieldService.save(cf); - - assertThat(saved).isNotNull(); - assertThat(saved.getConfiguration()).isInstanceOf(GeofencingCalculatedFieldConfiguration.class); - - var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration(); - - // Assert: the interval is saved, but scheduling is not enabled - int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateInterval(); - boolean scheduledUpdateEnabled = geofencingCalculatedFieldConfiguration.isScheduledUpdateEnabled(); - - assertThat(savedInterval).isEqualTo(600); - assertThat(scheduledUpdateEnabled).isFalse(); - - calculatedFieldService.deleteCalculatedField(tenantId, saved.getId()); - } - - @Test - public void testSaveGeofencingCalculatedField_shouldThrowWhenScheduledIntervalIsLessThanMinAllowedIntervalInTenantProfile() { + public void testSaveGeofencingCalculatedField_shouldThrowWhenScheduledIntervalLessThanMinAllowedIntervalInTenantProfile() { // Arrange a device Device device = createTestDevice(); @@ -165,15 +119,22 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration)); + // Get tenant profile min. + int min = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMinAllowedScheduledUpdateIntervalInSecForCF(); + int valueFromConfig = min - 10; + // Enable scheduling with an interval below tenant min - cfg.setScheduledUpdateInterval(600); + cfg.setScheduledUpdateEnabled(true); + cfg.setScheduledUpdateInterval(valueFromConfig); // Create & save Calculated Field CalculatedField cf = new CalculatedField(); cf.setTenantId(tenantId); cf.setEntityId(device.getId()); cf.setType(CalculatedFieldType.GEOFENCING); - cf.setName("GF clamp test"); + cf.setName("GF min allowed scheduled update interval test"); cf.setConfigurationVersion(0); cf.setConfiguration(cfg); @@ -185,7 +146,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { } @Test - public void testSaveGeofencingCalculatedField_shouldThrowWhenRelationLevelIsGreaterThanMaxAllowedRelationLevelInTenantProfile() { + public void testSaveGeofencingCalculatedField_shouldThrowWhenRelationLevelGreaterThanMaxAllowedRelationLevelInTenantProfile() { // Arrange a device Device device = createTestDevice(); @@ -210,7 +171,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { cf.setTenantId(tenantId); cf.setEntityId(device.getId()); cf.setType(CalculatedFieldType.GEOFENCING); - cf.setName("GF clamp test"); + cf.setName("GF max relation level test"); cf.setConfigurationVersion(0); cf.setConfiguration(cfg); @@ -221,7 +182,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { } @Test - public void testSaveGeofencingCalculatedField_shouldUseScheduledIntervalFromConfig() { + public void testSaveGeofencingCalculatedField_shouldSaveWithoutDataValidationExceptionOnScheduledUpdateInterval() { // Arrange a device Device device = createTestDevice(); @@ -245,10 +206,10 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { int min = tbTenantProfileCache.get(tenantId) .getDefaultProfileConfiguration() .getMinAllowedScheduledUpdateIntervalInSecForCF(); - + int valueFromConfig = min + 100; // Enable scheduling with an interval greater than tenant min - int valueFromConfig = min + 100; + cfg.setScheduledUpdateEnabled(true); cfg.setScheduledUpdateInterval(valueFromConfig); // Create & save Calculated Field @@ -256,7 +217,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { cf.setTenantId(tenantId); cf.setEntityId(device.getId()); cf.setType(CalculatedFieldType.GEOFENCING); - cf.setName("GF no clamp test"); + cf.setName("GF no validation error test"); cf.setConfigurationVersion(0); cf.setConfiguration(cfg); @@ -267,7 +228,6 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration(); - // Assert: the interval is clamped up to tenant profile min (or stays >= original if already >= min) int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateInterval(); assertThat(savedInterval).isEqualTo(valueFromConfig); From c1596b591d8078d1cc7bffdb79e697a7d6bfc524 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 1 Oct 2025 14:41:54 +0300 Subject: [PATCH 186/216] UI: Refactoring show total in chart tooltip --- ...e-series-chart-basic-config.component.html | 4 +- ...ime-series-chart-basic-config.component.ts | 5 +- .../chart/time-series-chart-tooltip.models.ts | 172 +++++++++--------- .../lib/chart/time-series-chart.models.ts | 2 +- .../widget/lib/chart/time-series-chart.ts | 3 +- ...eries-chart-widget-settings.component.html | 4 +- ...-series-chart-widget-settings.component.ts | 5 +- 7 files changed, 101 insertions(+), 94 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html index c237c056d9..0c5eb426b3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.html @@ -318,8 +318,8 @@ {{ 'tooltip.hide-zero-tooltip-values' | translate }} -
- +
+ {{ 'tooltip.show-stack-total' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts index 4223ce139b..60135a3438 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/time-series-chart-basic-config.component.ts @@ -363,10 +363,11 @@ export class TimeSeriesChartBasicConfigComponent extends BasicWidgetConfigCompon this.timeSeriesChartWidgetConfigForm.get('tooltipValueColor').enable(); this.timeSeriesChartWidgetConfigForm.get('tooltipShowDate').enable({emitEvent: false}); this.timeSeriesChartWidgetConfigForm.get('tooltipHideZeroValues').enable({emitEvent: false}); - if (stack) + if (stack) { this.timeSeriesChartWidgetConfigForm.get('tooltipStackedShowTotal').enable(); - else + } else { this.timeSeriesChartWidgetConfigForm.get('tooltipStackedShowTotal').disable(); + } this.timeSeriesChartWidgetConfigForm.get('tooltipBackgroundColor').enable(); this.timeSeriesChartWidgetConfigForm.get('tooltipBackgroundBlur').enable(); if (tooltipShowDate) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-tooltip.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-tooltip.models.ts index 77d32149a2..e7f3de8f31 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-tooltip.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart-tooltip.models.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { isFunction } from '@core/utils'; +import { isDefined, isFunction, isNotEmptyStr } 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'; @@ -22,6 +22,7 @@ import { Renderer2, SecurityContext } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; import { CallbackDataParams } from 'echarts/types/dist/shared'; import { Interval } from '@shared/models/time/time.models'; +import { TranslateService } from '@ngx-translate/core'; export type TimeSeriesChartTooltipValueFormatFunction = (value: any, latestData: FormattedData, units?: string, decimals?: number) => string; @@ -89,7 +90,8 @@ export class TimeSeriesChartTooltip { private sanitizer: DomSanitizer, private settings: TimeSeriesChartTooltipWidgetSettings, private tooltipDateFormat: DateFormatProcessor, - private valueFormatFunction: TimeSeriesChartTooltipValueFormatFunction) { + private valueFormatFunction: TimeSeriesChartTooltipValueFormatFunction, + private translate: TranslateService) { } @@ -131,23 +133,65 @@ export class TimeSeriesChartTooltip { if (this.settings.tooltipShowDate) { this.renderer.appendChild(tooltipItemsElement, this.constructTooltipDateElement(items[0].param, interval)); } - let total = 0, isStacked = false; + let total = 0; + let isStacked = false; + const totalUnits = new Set(); + let totalDecimal = 0; for (const item of items) { - if (!this.settings.tooltipHideZeroValues || (item.param.value[1] && item.param.value[1] !== 'false')) { + if (this.shouldShowItem(item)) { this.renderer.appendChild(tooltipItemsElement, this.constructTooltipSeriesElement(item)); - if (item.dataItem?.barRenderContext?.barStackIndex !== undefined && !isNaN(Number(item.param.value[1]))) { + if (item.dataItem?.option?.stack !== undefined && !isNaN(Number(item.param.value[1]))) { isStacked = true; total += Number(item.param.value[1]); + if (isNotEmptyStr(item.dataItem.units)) { + totalUnits.add(item.dataItem.units); + } + if (isDefined(item.dataItem.decimals)) { + totalDecimal = Math.max(item.dataItem.decimals, totalDecimal); + } } } } - if (isStacked && this.settings.tooltipStackedShowTotal) - this.renderer.appendChild(tooltipItemsElement, this.constructTooltipTotalStackedElement(total)); + if (isStacked && this.settings.tooltipStackedShowTotal) { + const unit = totalUnits.size === 1 ? Array.from(totalUnits.values())[0] : ""; + const totalValue = this.valueFormatFunction(total, {} as FormattedData, unit, totalDecimal); + this.renderer.appendChild(tooltipItemsElement, this.constructTooltipTotalStackedElement(totalValue)); + } + } + } + + private shouldShowItem(item: TooltipItem): boolean { + if (!this.settings.tooltipHideZeroValues) return true; + const value = item.param?.value?.[1]; + return value && value !== 'false'; + } + + private createElement(tag = 'div', styles?: Record): HTMLElement { + const node = this.renderer.createElement(tag); + if (styles) { + for (const [k, v] of Object.entries(styles)) { + this.renderer.setStyle(node, k, v); + } + } + return node; + } + + private applyFont(el: HTMLElement, font: {family: string; size: number; sizeUnit: string; style: string; weight: string; lineHeight: string}, color: string, overrides?: Partial) { + this.renderer.setStyle(el, 'font-family', font.family); + this.renderer.setStyle(el, 'font-size', `${font.size}${font.sizeUnit}`); + this.renderer.setStyle(el, 'font-style', font.style); + this.renderer.setStyle(el, 'font-weight', font.weight); + this.renderer.setStyle(el, 'line-height', font.lineHeight); + this.renderer.setStyle(el, 'color', color); + if (overrides) { + for (const [k, v] of Object.entries(overrides)) { + if (v != null) this.renderer.setStyle(el, k, v as string); + } } } private constructTooltipDateElement(param: CallbackDataParams, interval?: Interval): HTMLElement { - const dateElement: HTMLElement = this.renderer.createElement('div'); + const dateElement = this.createElement(); let dateText: string; const startTs = param.value[2]; const endTs = param.value[3]; @@ -164,43 +208,25 @@ export class TimeSeriesChartTooltip { dateText = this.tooltipDateFormat.update(ts, interval); } this.renderer.appendChild(dateElement, this.renderer.createText(dateText)); - this.renderer.setStyle(dateElement, 'font-family', this.settings.tooltipDateFont.family); - this.renderer.setStyle(dateElement, 'font-size', this.settings.tooltipDateFont.size + this.settings.tooltipDateFont.sizeUnit); - this.renderer.setStyle(dateElement, 'font-style', this.settings.tooltipDateFont.style); - this.renderer.setStyle(dateElement, 'font-weight', this.settings.tooltipDateFont.weight); - this.renderer.setStyle(dateElement, 'line-height', this.settings.tooltipDateFont.lineHeight); - this.renderer.setStyle(dateElement, 'color', this.settings.tooltipDateColor); + this.applyFont(dateElement, this.settings.tooltipDateFont, this.settings.tooltipDateColor); return dateElement; } private constructTooltipSeriesElement(item: TooltipItem): HTMLElement { - const labelValueElement: HTMLElement = this.renderer.createElement('div'); - this.renderer.setStyle(labelValueElement, 'display', 'flex'); - this.renderer.setStyle(labelValueElement, 'flex-direction', 'row'); - this.renderer.setStyle(labelValueElement, 'align-items', 'center'); - this.renderer.setStyle(labelValueElement, 'align-self', 'stretch'); - this.renderer.setStyle(labelValueElement, 'gap', '12px'); - const labelElement: HTMLElement = this.renderer.createElement('div'); - this.renderer.setStyle(labelElement, 'display', 'flex'); - this.renderer.setStyle(labelElement, 'align-items', 'center'); - this.renderer.setStyle(labelElement, 'gap', '8px'); - this.renderer.appendChild(labelValueElement, labelElement); - const circleElement: HTMLElement = this.renderer.createElement('div'); - this.renderer.setStyle(circleElement, 'width', '8px'); - this.renderer.setStyle(circleElement, 'height', '8px'); - this.renderer.setStyle(circleElement, 'border-radius', '50%'); - this.renderer.setStyle(circleElement, 'background', item.param.color); - this.renderer.appendChild(labelElement, circleElement); - const labelTextElement: HTMLElement = this.renderer.createElement('div'); - this.renderer.setProperty(labelTextElement, 'innerHTML', this.sanitizer.sanitize(SecurityContext.HTML, item.param.seriesName)); - this.renderer.setStyle(labelTextElement, 'font-family', this.settings.tooltipLabelFont.family); - this.renderer.setStyle(labelTextElement, 'font-size', this.settings.tooltipLabelFont.size + this.settings.tooltipLabelFont.sizeUnit); - this.renderer.setStyle(labelTextElement, 'font-style', this.settings.tooltipLabelFont.style); - this.renderer.setStyle(labelTextElement, 'font-weight', this.settings.tooltipLabelFont.weight); - this.renderer.setStyle(labelTextElement, 'line-height', this.settings.tooltipLabelFont.lineHeight); - this.renderer.setStyle(labelTextElement, 'color', this.settings.tooltipLabelColor); - this.renderer.appendChild(labelElement, labelTextElement); - const valueElement: HTMLElement = this.renderer.createElement('div'); + const row = this.createElement('div', {display: 'flex', 'flex-direction': 'row', 'align-items': 'center', 'align-self': 'stretch', gap: '12px'}); + + const label = this.createElement('div', { display: 'flex', 'align-items': 'center', gap: '8px' }); + this.renderer.appendChild(row, label); + + const dot = this.createElement('div', { width: '8px', height: '8px', 'border-radius': '50%', background: item.param.color as string }); + this.renderer.appendChild(label, dot); + + const labelText = this.createElement('div'); + this.renderer.setProperty(labelText, 'innerHTML', this.sanitizer.sanitize(SecurityContext.HTML, item.param.seriesName)); + this.applyFont(labelText, this.settings.tooltipLabelFont, this.settings.tooltipLabelColor); + this.renderer.appendChild(label, labelText); + + const valueElement: HTMLElement = this.createElement('div', { flex: '1', 'text-align': 'end' }); let formatFunction = this.valueFormatFunction; let latestData: FormattedData; let units = ''; @@ -218,51 +244,29 @@ export class TimeSeriesChartTooltip { } const value = formatFunction(item.param.value[1], latestData, units, decimals); this.renderer.setProperty(valueElement, 'innerHTML', this.sanitizer.sanitize(SecurityContext.HTML, value)); - this.renderer.setStyle(valueElement, 'flex', '1'); - this.renderer.setStyle(valueElement, 'text-align', 'end'); - this.renderer.setStyle(valueElement, 'font-family', this.settings.tooltipValueFont.family); - this.renderer.setStyle(valueElement, 'font-size', this.settings.tooltipValueFont.size + this.settings.tooltipValueFont.sizeUnit); - this.renderer.setStyle(valueElement, 'font-style', this.settings.tooltipValueFont.style); - this.renderer.setStyle(valueElement, 'font-weight', this.settings.tooltipValueFont.weight); - this.renderer.setStyle(valueElement, 'line-height', this.settings.tooltipValueFont.lineHeight); - this.renderer.setStyle(valueElement, 'color', this.settings.tooltipValueColor); - this.renderer.appendChild(labelValueElement, valueElement); - return labelValueElement; + this.applyFont(valueElement, this.settings.tooltipValueFont, this.settings.tooltipValueColor); + this.renderer.appendChild(row, valueElement); + + return row; } - private constructTooltipTotalStackedElement(total: number): HTMLElement { - const labelValueElement: HTMLElement = this.renderer.createElement('div'); - this.renderer.setStyle(labelValueElement, 'display', 'flex'); - this.renderer.setStyle(labelValueElement, 'flex-direction', 'row'); - this.renderer.setStyle(labelValueElement, 'align-items', 'center'); - this.renderer.setStyle(labelValueElement, 'align-self', 'stretch'); - this.renderer.setStyle(labelValueElement, 'gap', '12px'); - const labelElement: HTMLElement = this.renderer.createElement('div'); - this.renderer.setStyle(labelElement, 'display', 'flex'); - this.renderer.setStyle(labelElement, 'align-items', 'center'); - this.renderer.setStyle(labelElement, 'gap', '8px'); - this.renderer.appendChild(labelValueElement, labelElement); - const labelTextElement: HTMLElement = this.renderer.createElement('div'); - this.renderer.setProperty(labelTextElement, 'innerHTML', this.sanitizer.sanitize(SecurityContext.HTML, 'Total')); - this.renderer.setStyle(labelTextElement, 'font-family', this.settings.tooltipLabelFont.family); - this.renderer.setStyle(labelTextElement, 'font-size', this.settings.tooltipLabelFont.size + this.settings.tooltipLabelFont.sizeUnit); - this.renderer.setStyle(labelTextElement, 'font-style', this.settings.tooltipLabelFont.style); - this.renderer.setStyle(labelTextElement, 'font-weight', 'bold'); - this.renderer.setStyle(labelTextElement, 'line-height', this.settings.tooltipLabelFont.lineHeight); - this.renderer.setStyle(labelTextElement, 'color', this.settings.tooltipLabelColor); - this.renderer.appendChild(labelElement, labelTextElement); - const valueElement: HTMLElement = this.renderer.createElement('div'); - this.renderer.setProperty(valueElement, 'innerHTML', this.sanitizer.sanitize(SecurityContext.HTML, total.toString())); - this.renderer.setStyle(valueElement, 'flex', '1'); - this.renderer.setStyle(valueElement, 'text-align', 'end'); - this.renderer.setStyle(valueElement, 'font-family', this.settings.tooltipValueFont.family); - this.renderer.setStyle(valueElement, 'font-size', this.settings.tooltipValueFont.size + this.settings.tooltipValueFont.sizeUnit); - this.renderer.setStyle(valueElement, 'font-style', this.settings.tooltipValueFont.style); - this.renderer.setStyle(valueElement, 'font-weight', 'bold'); - this.renderer.setStyle(valueElement, 'line-height', this.settings.tooltipValueFont.lineHeight); - this.renderer.setStyle(valueElement, 'color', this.settings.tooltipValueColor); - this.renderer.appendChild(labelValueElement, valueElement); - return labelValueElement; + private constructTooltipTotalStackedElement(total: string): HTMLElement { + const row = this.createElement('div', {display: 'flex', 'flex-direction': 'row', 'align-items': 'center', 'align-self': 'stretch', gap: '12px'}); + + const label = this.createElement('div', { display: 'flex', 'align-items': 'center', gap: '8px' }); + this.renderer.appendChild(row, label); + + const labelText = this.createElement('div'); + this.renderer.setProperty(labelText, 'innerHTML', this.sanitizer.sanitize(SecurityContext.HTML, this.translate.instant('legend.Total'))); + this.applyFont(labelText, this.settings.tooltipLabelFont, this.settings.tooltipLabelColor, { fontWeight: 'bold' }); + this.renderer.appendChild(label, labelText); + + const valueEl = this.createElement('div', { flex: '1', 'text-align': 'end' }); + this.renderer.setProperty(valueEl, 'innerHTML', this.sanitizer.sanitize(SecurityContext.HTML, total)); + this.applyFont(valueEl, this.settings.tooltipValueFont, this.settings.tooltipValueColor, { fontWeight: 'bold' }); + this.renderer.appendChild(row, valueEl); + + return row; } private static mapTooltipParams(params: CallbackDataParams[] | CallbackDataParams, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts index d18f985c1d..089c655224 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.models.ts @@ -143,7 +143,7 @@ export interface TimeSeriesChartDataItem { xAxisIndex: number; yAxisId: TimeSeriesChartYAxisId; yAxisIndex: number; - option?: LineSeriesOption | CustomSeriesOption; + option?: LineSeriesOption; barRenderContext?: BarRenderContext; unitConvertor?: TbUnitConverter; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts index 7ac6e3ec85..d447b51b37 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/chart/time-series-chart.ts @@ -212,7 +212,8 @@ export class TbTimeSeriesChart { this.ctx.sanitizer, this.settings, this.tooltipDateFormat, - tooltipValueFormatFunction + tooltipValueFormatFunction, + this.ctx.translate ); this.onResize(); if (this.autoResize) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html index 9914a6124d..9a80362c8d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.html @@ -242,8 +242,8 @@ {{ 'tooltip.hide-zero-tooltip-values' | translate }}
-
- +
+ {{ 'tooltip.show-stack-total' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts index a753b12acd..0c2f00d09b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/time-series-chart-widget-settings.component.ts @@ -227,10 +227,11 @@ export class TimeSeriesChartWidgetSettingsComponent extends WidgetSettingsCompon this.timeSeriesChartWidgetSettingsForm.get('tooltipValueFormatter').enable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipShowDate').enable({emitEvent: false}); this.timeSeriesChartWidgetSettingsForm.get('tooltipHideZeroValues').enable(); - if (stack) + if (stack) { this.timeSeriesChartWidgetSettingsForm.get('tooltipStackedShowTotal').enable(); - else + } else { this.timeSeriesChartWidgetSettingsForm.get('tooltipStackedShowTotal').disable(); + } this.timeSeriesChartWidgetSettingsForm.get('tooltipBackgroundColor').enable(); this.timeSeriesChartWidgetSettingsForm.get('tooltipBackgroundBlur').enable(); if (tooltipShowDate) { From 909497703a71449d5d7a388d96da45bc745d13bb Mon Sep 17 00:00:00 2001 From: dshvaika Date: Wed, 1 Oct 2025 15:35:55 +0300 Subject: [PATCH 187/216] new relation path query --- ...tractCalculatedFieldProcessingService.java | 6 ++ .../server/dao/relation/RelationService.java | 3 + .../CFArgumentDynamicSourceType.java | 2 +- .../CfArgumentDynamicSourceConfiguration.java | 3 +- ...onPathQueryDynamicSourceConfiguration.java | 73 ++++++++++++++ .../cf/configuration/RelationQueryBased.java | 29 ++++++ ...lationQueryDynamicSourceConfiguration.java | 9 +- .../relation/EntityRelationPathQuery.java | 24 +++++ .../data/relation/RelationPathLevel.java | 30 ++++++ .../dao/relation/BaseRelationService.java | 21 ++++ .../server/dao/relation/RelationDao.java | 3 + .../CalculatedFieldDataValidator.java | 6 +- .../dao/sql/relation/JpaRelationDao.java | 99 +++++++++++++++++++ 13 files changed, 295 insertions(+), 13 deletions(-) create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryBased.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationPathQuery.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationPathLevel.java diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java index 8709e0cb68..bd3546082a 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java @@ -26,6 +26,7 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -178,6 +179,11 @@ public abstract class AbstractCalculatedFieldProcessingService { yield Futures.transform(relationService.findByQuery(tenantId, configuration.toEntityRelationsQuery(entityId)), configuration::resolveEntityIds, calculatedFieldCallbackExecutor); } + case RELATION_PATH_QUERY -> { + var configuration = (RelationPathQueryDynamicSourceConfiguration) refDynamicSourceConfiguration; + yield Futures.transform(relationService.findByRelationPathQueryAsync(tenantId, configuration.toRelationPathQuery(entityId)), + configuration::resolveEntityIds, calculatedFieldCallbackExecutor); + } }; } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java index 1db5739e94..a0bc9a72e6 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/relation/RelationService.java @@ -20,6 +20,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationInfo; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -83,6 +84,8 @@ public interface RelationService { List findRuleNodeToRuleChainRelations(TenantId tenantId, RuleChainType ruleChainType, int limit); + ListenableFuture> findByRelationPathQueryAsync(TenantId tenantId, EntityRelationPathQuery relationPathQuery); + // TODO: This method may be useful for some validations in the future // ListenableFuture checkRecursiveRelation(EntityId from, EntityId to); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java index bd2e9b0c00..35c6cdf562 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java @@ -17,6 +17,6 @@ package org.thingsboard.server.common.data.cf.configuration; public enum CFArgumentDynamicSourceType { - RELATION_QUERY + RELATION_QUERY, RELATION_PATH_QUERY } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java index f36071615e..397b1d016e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java @@ -26,7 +26,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; property = "type" ) @JsonSubTypes({ - @JsonSubTypes.Type(value = RelationQueryDynamicSourceConfiguration.class, name = "RELATION_QUERY") + @JsonSubTypes.Type(value = RelationQueryDynamicSourceConfiguration.class, name = "RELATION_QUERY"), + @JsonSubTypes.Type(value = RelationPathQueryDynamicSourceConfiguration.class, name = "RELATION_PATH_QUERY") }) @JsonIgnoreProperties(ignoreUnknown = true) public interface CfArgumentDynamicSourceConfiguration { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java new file mode 100644 index 0000000000..c7889be19d --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java @@ -0,0 +1,73 @@ +/** + * 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.common.data.cf.configuration; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; +import org.thingsboard.server.common.data.util.CollectionsUtil; + +import java.util.List; +import java.util.NoSuchElementException; + +@Data +public class RelationPathQueryDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration, RelationQueryBased { + + private List levels; + + @Override + public CFArgumentDynamicSourceType getType() { + return CFArgumentDynamicSourceType.RELATION_PATH_QUERY; + } + + @Override + public void validate() { + if (CollectionsUtil.isEmpty(levels)) { + throw new IllegalArgumentException("At least one relation level must be specified!"); + } + levels.forEach(RelationPathLevel::validate); + } + + public List resolveEntityIds(List relations) { + EntitySearchDirection lastLevelDirection = getLastLevel().direction(); + return switch (lastLevelDirection) { + case FROM -> relations.stream().map(EntityRelation::getTo).toList(); + case TO -> relations.stream().map(EntityRelation::getFrom).toList(); + }; + } + + @Override + @JsonIgnore + public int getMaxLevel() { + return levels != null ? levels.size() : 0; + } + + public EntityRelationPathQuery toRelationPathQuery(EntityId entityId) { + return new EntityRelationPathQuery(entityId, levels); + } + + private RelationPathLevel getLastLevel() { + if (CollectionsUtil.isEmpty(levels)) { + throw new NoSuchElementException(); + } + return levels.get(levels.size() - 1); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryBased.java new file mode 100644 index 0000000000..e3248b264f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryBased.java @@ -0,0 +1,29 @@ +/** + * 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.common.data.cf.configuration; + +public interface RelationQueryBased { + + int getMaxLevel(); + + default void validateMaxRelationLevel(String argumentName, int maxAllowedRelationLevel) { + if (getMaxLevel() > maxAllowedRelationLevel) { + throw new IllegalArgumentException("Max relation level is greater than configured " + + "maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + argumentName); + } + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java index 4e9b4252c9..120d04d40d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java @@ -29,7 +29,7 @@ import java.util.Collections; import java.util.List; @Data -public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration { +public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration, RelationQueryBased { private int maxLevel; private boolean fetchLastLevelOnly; @@ -59,13 +59,6 @@ public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynami return maxLevel == 1; } - public void validateMaxRelationLevel(String argumentName, int maxAllowedRelationLevel) { - if (maxLevel > maxAllowedRelationLevel) { - throw new IllegalArgumentException("Max relation level is greater than configured " + - "maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + argumentName); - } - } - public EntityRelationsQuery toEntityRelationsQuery(EntityId rootEntityId) { if (isSimpleRelation()) { throw new IllegalArgumentException("Entity relations query can't be created for a simple relation!"); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationPathQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationPathQuery.java new file mode 100644 index 0000000000..a81e3e0c86 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/EntityRelationPathQuery.java @@ -0,0 +1,24 @@ +/** + * 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.common.data.relation; + +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.List; + +public record EntityRelationPathQuery(EntityId rootEntityId, List levels) { + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationPathLevel.java b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationPathLevel.java new file mode 100644 index 0000000000..c28135204f --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/relation/RelationPathLevel.java @@ -0,0 +1,30 @@ +/** + * 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.common.data.relation; + +import org.thingsboard.server.common.data.StringUtils; + +public record RelationPathLevel(EntitySearchDirection direction, String relationType) { + + public void validate() { + if (direction == null) { + throw new IllegalArgumentException("Direction must be specified!"); + } + if (StringUtils.isBlank(relationType)) { + throw new IllegalArgumentException("Relation type must be specified!"); + } + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index 87f7e44a1f..0ec1257bcc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -32,17 +32,21 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.CollectionUtils; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.cache.TbTransactionalCache; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UUIDBased; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntityRelationInfo; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -495,6 +499,23 @@ public class BaseRelationService implements RelationService { return relationDao.findRuleNodeToRuleChainRelations(ruleChainType, limit); } + @Override + public ListenableFuture> findByRelationPathQueryAsync(TenantId tenantId, EntityRelationPathQuery relationPathQuery) { + log.trace("Executing findByRelationPathQuery, tenantId [{}], relationPathQuery {}", tenantId, relationPathQuery); + validateId(tenantId, id -> "Invalid tenant id: " + id); + validate(relationPathQuery); + return executor.submit(() -> relationDao.findByRelationPathQuery(tenantId, relationPathQuery)); + } + + private void validate(EntityRelationPathQuery relationPathQuery) { + validateId((UUIDBased) relationPathQuery.rootEntityId(), id -> "Invalid root entity id: " + id); + List levels = relationPathQuery.levels(); + if (CollectionUtils.isEmpty(levels)) { + throw new DataValidationException("Relation path levels should be specified!"); + } + levels.forEach(RelationPathLevel::validate); + } + protected void validate(EntityRelation relation) { if (relation == null) { throw new DataValidationException("Relation type should be specified!"); diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java index c061faed5e..ad53164ad7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/RelationDao.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChainType; @@ -71,4 +72,6 @@ public interface RelationDao { List findRuleNodeToRuleChainRelations(RuleChainType ruleChainType, int limit); + List findByRelationPathQuery(TenantId tenantId, EntityRelationPathQuery relationPathQuery); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java index 05b782c26c..85c30ca74e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java @@ -19,7 +19,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.RelationQueryBased; import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; @@ -98,10 +98,10 @@ public class CalculatedFieldDataValidator extends DataValidator if (!(calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argumentsBasedCfg)) { return; } - Map relationQueryBasedArguments = argumentsBasedCfg.getArguments().entrySet() + Map relationQueryBasedArguments = argumentsBasedCfg.getArguments().entrySet() .stream() .filter(entry -> entry.getValue().hasDynamicSource()) - .collect(Collectors.toMap(Map.Entry::getKey, entry -> (RelationQueryDynamicSourceConfiguration) entry.getValue().getRefDynamicSourceConfiguration())); + .collect(Collectors.toMap(Map.Entry::getKey, entry -> (RelationQueryBased) entry.getValue().getRefDynamicSourceConfiguration())); if (relationQueryBasedArguments.isEmpty()) { return; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java index 7417418f54..782c9b9d53 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java @@ -25,6 +25,9 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.dao.DaoUtil; @@ -43,6 +46,7 @@ import java.util.stream.Collectors; import static org.thingsboard.server.dao.model.ModelConstants.RELATION_FROM_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.RELATION_FROM_TYPE_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TABLE_NAME; import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TO_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TO_TYPE_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.RELATION_TYPE_GROUP_PROPERTY; @@ -293,4 +297,99 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple public List findRuleNodeToRuleChainRelations(RuleChainType ruleChainType, int limit) { return DaoUtil.convertDataList(relationRepository.findRuleNodeToRuleChainRelations(ruleChainType, PageRequest.of(0, limit))); } + + @Override + public List findByRelationPathQuery(TenantId tenantId, EntityRelationPathQuery query) { + List levels = query.levels(); + if (levels == null || levels.isEmpty()) { + return Collections.emptyList(); + } + String sql = buildRelationPathSql(query); + Object[] params = buildRelationPathParams(query); + + log.info("[{}] relation path query: {}", tenantId, sql); + + return jdbcTemplate.queryForList(sql, params).stream() + .map(row -> { + var entityRelation = new EntityRelation(); + var fromId = (UUID) row.get(RELATION_FROM_ID_PROPERTY); + var fromType = (String) row.get(RELATION_FROM_TYPE_PROPERTY); + var toId = (UUID) row.get(RELATION_TO_ID_PROPERTY); + var toType = (String) row.get(RELATION_TO_TYPE_PROPERTY); + var grp = (String) row.get(RELATION_TYPE_GROUP_PROPERTY); + var type = (String) row.get(RELATION_TYPE_PROPERTY); + var version = (Long) row.get(VERSION_COLUMN); + + entityRelation.setFrom(EntityIdFactory.getByTypeAndUuid(fromType, fromId)); + entityRelation.setTo(EntityIdFactory.getByTypeAndUuid(toType, toId)); + entityRelation.setType(type); + entityRelation.setTypeGroup(RelationTypeGroup.valueOf(grp)); + entityRelation.setVersion(version); + return entityRelation; + }) + .collect(Collectors.toList()); + } + + private Object[] buildRelationPathParams(EntityRelationPathQuery query) { + final List params = new ArrayList<>(); + // seed + params.add(query.rootEntityId().getId()); + params.add(query.rootEntityId().getEntityType().name()); + + // levels + for (var lvl : query.levels()) { + params.add(lvl.relationType()); + } + return params.toArray(); + } + + private static String buildRelationPathSql(EntityRelationPathQuery query) { + List levels = query.levels(); + StringBuilder sb = new StringBuilder(); + + sb.append("WITH seed AS (\n") + .append(" SELECT ?::uuid AS id, ?::varchar AS type\n") + .append(")"); + + String prev = "seed"; + for (int i = 0; i < levels.size() - 1; i++) { + RelationPathLevel lvl = levels.get(i); + boolean down = lvl.direction() == EntitySearchDirection.FROM; + + String cur = "lvl" + (i + 1); + String joinCond = down + ? "r.from_id = p.id AND r.from_type = p.type" + : "r.to_id = p.id AND r.to_type = p.type"; + String selectNext = down + ? "r.to_id AS id, r.to_type AS type" + : "r.from_id AS id, r.from_type AS type"; + + sb.append(",\n").append(cur).append(" AS (\n") + .append(" SELECT ").append(selectNext).append("\n") + .append(" FROM ").append(RELATION_TABLE_NAME).append(" r\n") + .append(" JOIN ").append(prev).append(" p ON ").append(joinCond).append("\n") + .append(" WHERE r.relation_type_group = '").append(RelationTypeGroup.COMMON).append("'\n") + .append(" AND r.relation_type = ?\n") + .append(")"); + prev = cur; + } + + RelationPathLevel last = levels.get(levels.size() - 1); + boolean lastDown = last.direction() == EntitySearchDirection.FROM; + String prevForLast = (levels.size() == 1) ? "seed" : prev; + String lastJoin = lastDown + ? "r.from_id = p.id AND r.from_type = p.type" + : "r.to_id = p.id AND r.to_type = p.type"; + + sb.append("\n") + .append("SELECT r.from_id, r.from_type, r.to_id, r.to_type,\n") + .append(" r.relation_type_group, r.relation_type, r.version\n") + .append("FROM ").append(RELATION_TABLE_NAME).append(" r\n") + .append("JOIN ").append(prevForLast).append(" p ON ").append(lastJoin).append("\n") + .append("WHERE r.relation_type_group = '").append(RelationTypeGroup.COMMON).append("'\n") + .append(" AND r.relation_type = ?"); + + return sb.toString(); + } + } From 8e8b5507ee99f57dad022e1e977388ab05a01b26 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 1 Oct 2025 16:42:31 +0300 Subject: [PATCH 188/216] UI: Doughnut widget disabled setting legendShowTotal in total Layout --- .../config/basic/chart/doughnut-basic-config.component.ts | 2 ++ .../lib/settings/chart/doughnut-widget-settings.component.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.ts b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.ts index 0321efac83..067fae3b4e 100644 --- a/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/config/basic/chart/doughnut-basic-config.component.ts @@ -86,9 +86,11 @@ export class DoughnutBasicConfigComponent extends LatestChartBasicConfigComponen if (totalEnabled) { latestChartWidgetConfigForm.get('totalValueFont').enable(); latestChartWidgetConfigForm.get('totalValueColor').enable(); + latestChartWidgetConfigForm.get('legendShowTotal').disable(); } else { latestChartWidgetConfigForm.get('totalValueFont').disable(); latestChartWidgetConfigForm.get('totalValueColor').disable(); + latestChartWidgetConfigForm.get('legendShowTotal').enable(); } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.ts index 8db598b919..f1854a0d00 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/chart/doughnut-widget-settings.component.ts @@ -69,9 +69,11 @@ export class DoughnutWidgetSettingsComponent extends LatestChartWidgetSettingsCo if (totalEnabled) { latestChartWidgetSettingsForm.get('totalValueFont').enable(); latestChartWidgetSettingsForm.get('totalValueColor').enable(); + latestChartWidgetSettingsForm.get('legendShowTotal').disable(); } else { latestChartWidgetSettingsForm.get('totalValueFont').disable(); latestChartWidgetSettingsForm.get('totalValueColor').disable(); + latestChartWidgetSettingsForm.get('legendShowTotal').enable(); } } } From 0a2b1168fabeb03f14f199c0a424c771ea9927f2 Mon Sep 17 00:00:00 2001 From: IrynaMatveieva Date: Thu, 2 Oct 2025 13:18:31 +0300 Subject: [PATCH 189/216] updated ctx value in cache when expression failed after cf update --- ...alculatedFieldManagerMessageProcessor.java | 47 ++++++++-------- .../cf/ctx/state/CalculatedFieldCtx.java | 2 + .../cf/CalculatedFieldIntegrationTest.java | 53 +++++++++++++++++++ 3 files changed, 79 insertions(+), 23 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java index c00995d3d4..590e097928 100644 --- a/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/calculatedField/CalculatedFieldManagerMessageProcessor.java @@ -287,29 +287,29 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware newCfCtx.init(); } catch (Exception e) { throw CalculatedFieldException.builder().ctx(newCfCtx).eventEntity(newCfCtx.getEntityId()).cause(e).errorMessage("Failed to initialize CF context").build(); - } - calculatedFields.put(newCf.getId(), newCfCtx); - List oldCfList = entityIdCalculatedFields.get(newCf.getEntityId()); - List newCfList = new CopyOnWriteArrayList<>(); - boolean found = false; - for (CalculatedFieldCtx oldCtx : oldCfList) { - if (oldCtx.getCfId().equals(newCf.getId())) { + } finally { + calculatedFields.put(newCf.getId(), newCfCtx); + List oldCfList = entityIdCalculatedFields.get(newCf.getEntityId()); + List newCfList = new CopyOnWriteArrayList<>(); + boolean found = false; + for (CalculatedFieldCtx oldCtx : oldCfList) { + if (oldCtx.getCfId().equals(newCf.getId())) { + newCfList.add(newCfCtx); + found = true; + } else { + newCfList.add(oldCtx); + } + } + if (!found) { newCfList.add(newCfCtx); - found = true; - } else { - newCfList.add(oldCtx); } + // We use copy on write lists to safely pass the reference to another actor for the iteration. + // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) + entityIdCalculatedFields.put(newCf.getEntityId(), newCfList); + deleteLinks(oldCfCtx); + addLinks(newCf); } - if (!found) { - newCfList.add(newCfCtx); - } - entityIdCalculatedFields.put(newCf.getEntityId(), newCfList); - - deleteLinks(oldCfCtx); - addLinks(newCf); - // We use copy on write lists to safely pass the reference to another actor for the iteration. - // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) var stateChanges = newCfCtx.hasStateChanges(oldCfCtx); if (stateChanges || newCfCtx.hasOtherSignificantChanges(oldCfCtx)) { initCf(newCfCtx, callback, stateChanges); @@ -550,11 +550,12 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware cfCtx.init(); } catch (Exception e) { throw CalculatedFieldException.builder().ctx(cfCtx).eventEntity(cf.getEntityId()).cause(e).errorMessage("Failed to initialize CF context").build(); + } finally { + calculatedFields.put(cf.getId(), cfCtx); + // We use copy on write lists to safely pass the reference to another actor for the iteration. + // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) + entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx); } - calculatedFields.put(cf.getId(), cfCtx); - // We use copy on write lists to safely pass the reference to another actor for the iteration. - // Alternative approach would be to use any list but avoid modifications to the list (change the complete map value instead) - entityIdCalculatedFields.computeIfAbsent(cf.getEntityId(), id -> new CopyOnWriteArrayList<>()).add(cfCtx); } private void initCalculatedFieldLink(CalculatedFieldLink link) { diff --git a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java index 2e3321eece..c9eaaef19a 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/ctx/state/CalculatedFieldCtx.java @@ -110,6 +110,7 @@ public class CalculatedFieldCtx { this.calculatedFieldScriptEngine = initEngine(tenantId, expression, tbelInvokeService); initialized = true; } catch (Exception e) { + initialized = false; throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax.", e); } } else { @@ -123,6 +124,7 @@ public class CalculatedFieldCtx { ); initialized = true; } else { + initialized = false; throw new RuntimeException("Failed to init calculated field ctx. Invalid expression syntax."); } } diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index c8b8b0244b..da4b5758cc 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -606,6 +606,59 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes }); } + @Test + public void testSimpleCalculatedFieldWhenCtxBecameUninitialized() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + + CalculatedField calculatedField = new CalculatedField(); + calculatedField.setEntityId(testDevice.getId()); + calculatedField.setType(CalculatedFieldType.SIMPLE); + calculatedField.setName("M + 1"); + calculatedField.setDebugSettings(DebugSettings.all()); + + SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); + + Argument argument = new Argument(); + ReferencedEntityKey refEntityKey = new ReferencedEntityKey("m", ArgumentType.TS_LATEST, null); + argument.setRefEntityKey(refEntityKey); + config.setArguments(Map.of("m", argument)); + config.setExpression("m + 1"); + + Output output = new Output(); + output.setName("m1"); + output.setType(OutputType.TIME_SERIES); + output.setDecimalsByDefault(0); + config.setOutput(output); + + calculatedField.setConfiguration(config); + + calculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"m\":1}")); + + await().alias("create CF -> ctx is initialized -> perform calculation").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode m1 = getLatestTelemetry(testDevice.getId(), "m1"); + assertThat(m1).isNotNull(); + assertThat(m1.get("m1").get(0).get("value").asText()).isEqualTo("2"); + }); + + config.setExpression("m m"); + calculatedField.setConfiguration(config); + calculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + + doPost("/api/plugins/telemetry/DEVICE/" + testDevice.getUuidId() + "/timeseries/" + DataConstants.SERVER_SCOPE, JacksonUtil.toJsonNode("{\"m\":2}")); + + await().alias("update CF -> ctx is not initialized -> no calculation performed").atMost(TIMEOUT, TimeUnit.SECONDS) + .pollInterval(POLL_INTERVAL, TimeUnit.SECONDS) + .untilAsserted(() -> { + ObjectNode m1 = getLatestTelemetry(testDevice.getId(), "m1"); + assertThat(m1).isNotNull(); + assertThat(m1.get("m1").get(0).get("value").asText()).isEqualTo("2"); + }); + } + private ObjectNode getLatestTelemetry(EntityId entityId, String... keys) throws Exception { return doGetAsync("/api/plugins/telemetry/" + entityId.getEntityType() + "/" + entityId.getId() + "/values/timeseries?keys=" + String.join(",", keys), ObjectNode.class); } From 2cb05c9d2b975930db26371f81dcefeb36824c40 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Thu, 2 Oct 2025 13:35:51 +0300 Subject: [PATCH 190/216] Added tests & temporary removed RelationQueryDynamicSourceConfiguration from interface --- .../CfArgumentDynamicSourceConfiguration.java | 1 - ...thQueryDynamicSourceConfigurationTest.java | 126 ++++++++++++++++++ ...onQueryDynamicSourceConfigurationTest.java | 14 +- .../dao/service/RelationServiceTest.java | 53 +++++++- 4 files changed, 183 insertions(+), 11 deletions(-) create mode 100644 common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfigurationTest.java diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java index 397b1d016e..ff746161e9 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CfArgumentDynamicSourceConfiguration.java @@ -26,7 +26,6 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; property = "type" ) @JsonSubTypes({ - @JsonSubTypes.Type(value = RelationQueryDynamicSourceConfiguration.class, name = "RELATION_QUERY"), @JsonSubTypes.Type(value = RelationPathQueryDynamicSourceConfiguration.class, name = "RELATION_PATH_QUERY") }) @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfigurationTest.java new file mode 100644 index 0000000000..1a6ea5de3b --- /dev/null +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfigurationTest.java @@ -0,0 +1,126 @@ +/** + * 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.common.data.cf.configuration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.mockito.junit.jupiter.MockitoExtension; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class RelationPathQueryDynamicSourceConfigurationTest { + + @Test + void typeShouldBeRelationQuery() { + var cfg = new RelationPathQueryDynamicSourceConfiguration(); + assertThat(cfg.getType()).isEqualTo(CFArgumentDynamicSourceType.RELATION_PATH_QUERY); + } + + @ParameterizedTest + @NullAndEmptySource + void validateShouldThrowWhenLevelsIsNull(List levels) { + var cfg = new RelationPathQueryDynamicSourceConfiguration(); + cfg.setLevels(levels); + + assertThatThrownBy(cfg::validate) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("At least one relation level must be specified!"); + } + + @Test + void validateShouldCallValidateForPathLevels() { + List levels = new ArrayList<>(); + + RelationPathLevel lvl1 = mock(RelationPathLevel.class); + RelationPathLevel lvl2 = mock(RelationPathLevel.class); + levels.add(lvl1); + levels.add(lvl2); + + var cfg = new RelationPathQueryDynamicSourceConfiguration(); + cfg.setLevels(levels); + + assertThatCode(cfg::validate).doesNotThrowAnyException(); + + verify(lvl1).validate(); + verify(lvl2).validate(); + } + + @Test + void resolveEntityIds_whenDirectionFROM_thenReturnsToIds() { + List levels = new ArrayList<>(); + + RelationPathLevel lvl1 = mock(RelationPathLevel.class); + RelationPathLevel lvl2 = mock(RelationPathLevel.class); + levels.add(lvl1); + levels.add(lvl2); + + when(lvl2.direction()).thenReturn(EntitySearchDirection.FROM); + + EntityRelation rel1 = mock(EntityRelation.class); + EntityRelation rel2 = mock(EntityRelation.class); + + when(rel1.getTo()).thenReturn(mock(EntityId.class)); + when(rel2.getTo()).thenReturn(mock(EntityId.class)); + + var cfg = new RelationPathQueryDynamicSourceConfiguration(); + cfg.setLevels(levels); + + var out = cfg.resolveEntityIds(List.of(rel1, rel2)); + + assertThat(out).containsExactly(rel1.getTo(), rel2.getTo()); + } + + @Test + void resolveEntityIds_whenDirectionTO_thenReturnsFromIds() { + List levels = new ArrayList<>(); + + RelationPathLevel lvl1 = mock(RelationPathLevel.class); + RelationPathLevel lvl2 = mock(RelationPathLevel.class); + levels.add(lvl1); + levels.add(lvl2); + + when(lvl2.direction()).thenReturn(EntitySearchDirection.TO); + + EntityRelation rel1 = mock(EntityRelation.class); + EntityRelation rel2 = mock(EntityRelation.class); + + when(rel1.getFrom()).thenReturn(mock(EntityId.class)); + when(rel2.getFrom()).thenReturn(mock(EntityId.class)); + + var cfg = new RelationPathQueryDynamicSourceConfiguration(); + cfg.setLevels(levels); + + var out = cfg.resolveEntityIds(List.of(rel1, rel2)); + + assertThat(out).containsExactly(rel1.getFrom(), rel2.getFrom()); + } + +} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java index 86fa52ba66..afd78e47f9 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java @@ -123,9 +123,8 @@ public class RelationQueryDynamicSourceConfigurationTest { .hasMessage("Relation query dynamic source configuration relation type must be specified!"); } - @ParameterizedTest - @NullAndEmptySource - void isSimpleRelationTrueWhenLevelIsOneAndEntityTypesEmptyOrNull(List entityTypes) { + @Test + void isSimpleRelationTrueWhenLevelIsOneAndEntityTypesEmptyOrNull() { var cfg = new RelationQueryDynamicSourceConfiguration(); cfg.setMaxLevel(1); assertThat(cfg.isSimpleRelation()).isTrue(); @@ -138,9 +137,8 @@ public class RelationQueryDynamicSourceConfigurationTest { assertThat(cfg.isSimpleRelation()).isFalse(); } - @ParameterizedTest - @NullAndEmptySource - void toEntityRelationsQueryShouldThrowForSimpleRelation(List entityTypes) { + @Test + void toEntityRelationsQueryShouldThrowForSimpleRelation() { var cfg = new RelationQueryDynamicSourceConfiguration(); cfg.setMaxLevel(1); cfg.setFetchLastLevelOnly(false); @@ -177,7 +175,7 @@ public class RelationQueryDynamicSourceConfigurationTest { } @Test - void resolveEntityIdsFromDirectionFROMReturnsToIds() { + void resolveEntityIds_whenDirectionFROM_thenReturnsToIds() { when(rel1.getTo()).thenReturn(mock(EntityId.class)); when(rel2.getTo()).thenReturn(mock(EntityId.class)); @@ -190,7 +188,7 @@ public class RelationQueryDynamicSourceConfigurationTest { } @Test - void resolveEntityIdsFromDirectionTOReturnsFromIds() { + void resolveEntityIds_whenDirectionTO_thenReturnsFromIds() { when(rel1.getFrom()).thenReturn(mock(EntityId.class)); when(rel2.getFrom()).thenReturn(mock(EntityId.class)); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java index 063e35ae80..bb7bfad677 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/RelationServiceTest.java @@ -28,9 +28,11 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.relation.EntityRelation; +import org.thingsboard.server.common.data.relation.EntityRelationPathQuery; import org.thingsboard.server.common.data.relation.EntityRelationsQuery; import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.relation.RelationTypeGroup; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.dao.exception.DataValidationException; @@ -42,6 +44,8 @@ import java.util.LinkedList; import java.util.List; import java.util.concurrent.ExecutionException; +import static org.assertj.core.api.Assertions.assertThat; + @DaoSqlTest public class RelationServiceTest extends AbstractServiceTest { @@ -348,14 +352,14 @@ public class RelationServiceTest extends AbstractServiceTest { query.setFilters(Collections.singletonList(new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.singletonList(EntityType.ASSET)))); List relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get(); Assert.assertEquals(expected.size(), relations.size()); - for(EntityRelation r : expected){ + for (EntityRelation r : expected) { Assert.assertTrue(relations.contains(r)); } //Test from cache relations = relationService.findByQuery(SYSTEM_TENANT_ID, query).get(); Assert.assertEquals(expected.size(), relations.size()); - for(EntityRelation r : expected){ + for (EntityRelation r : expected) { Assert.assertTrue(relations.contains(r)); } } @@ -623,6 +627,51 @@ public class RelationServiceTest extends AbstractServiceTest { Assert.assertTrue(relations.contains(relationF)); } + @Test + public void testFindByPathQuery() throws Exception { + /* + A + └──[firstLevel, TO]→ B + └──[secondLevel, TO]→ C + ├──[thirdLevel, FROM]→ D + ├──[thirdLevel, FROM]→ E + └──[thirdLevel, FROM]→ F + */ + // rootEntity + AssetId assetA = new AssetId(Uuids.timeBased()); + // firstLevelEntity + AssetId assetB = new AssetId(Uuids.timeBased()); + // secondLevelEntity + AssetId assetC = new AssetId(Uuids.timeBased()); + // thirdLevelEntities + AssetId assetD = new AssetId(Uuids.timeBased()); + AssetId assetE = new AssetId(Uuids.timeBased()); + AssetId assetF = new AssetId(Uuids.timeBased()); + + EntityRelation firstLevelRelation = new EntityRelation(assetB, assetA, "firstLevel"); + EntityRelation secondLevelRelation = new EntityRelation(assetC, assetB, "secondLevel"); + EntityRelation thirdLevelRelation1 = new EntityRelation(assetC, assetD, "thirdLevel"); + EntityRelation thirdLevelRelation2 = new EntityRelation(assetC, assetE, "thirdLevel"); + EntityRelation thirdLevelRelation3 = new EntityRelation(assetC, assetF, "thirdLevel"); + + firstLevelRelation = saveRelation(firstLevelRelation); + secondLevelRelation = saveRelation(secondLevelRelation); + thirdLevelRelation1 = saveRelation(thirdLevelRelation1); + thirdLevelRelation2 = saveRelation(thirdLevelRelation2); + thirdLevelRelation3 = saveRelation(thirdLevelRelation3); + + List expectedRelations = List.of(thirdLevelRelation1, thirdLevelRelation2, thirdLevelRelation3); + + EntityRelationPathQuery relationPathQuery = new EntityRelationPathQuery(assetA, List.of( + new RelationPathLevel(EntitySearchDirection.TO, "firstLevel"), + new RelationPathLevel(EntitySearchDirection.TO, "secondLevel"), + new RelationPathLevel(EntitySearchDirection.FROM, "thirdLevel") + )); + List entityRelations = relationService.findByRelationPathQueryAsync(tenantId, relationPathQuery).get(); + + assertThat(expectedRelations).containsExactlyInAnyOrderElementsOf(entityRelations); + } + @Test public void testFindByQueryLargeHierarchyFetchAllWithUnlimLvl() throws Exception { AssetId rootAsset = new AssetId(Uuids.timeBased()); From 14ee34889609201de5703dc091cd758fce3097e4 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Thu, 2 Oct 2025 15:10:40 +0300 Subject: [PATCH 191/216] Remove deprecated Sonatype repo --- pom.xml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 189e81411f..fd49af548f 100755 --- a/pom.xml +++ b/pom.xml @@ -1929,9 +1929,6 @@ Typesafe Repository https://repo.typesafe.com/typesafe/releases/ - - sonatype - https://oss.sonatype.org/content/groups/public - + From 00700afd83637c752f3fe340bebb723dc5c87845 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 2 Oct 2025 15:29:38 +0300 Subject: [PATCH 192/216] fixed pubAck publications --- .../AbstractGatewaySessionHandler.java | 250 ++++++++++++------ .../session/SparkplugNodeSessionHandler.java | 27 +- 2 files changed, 194 insertions(+), 83 deletions(-) diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java index af8ed2aa6d..168fef6c2e 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java @@ -80,6 +80,8 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; @@ -405,18 +407,34 @@ public abstract class AbstractGatewaySessionHandler deviceEntry : json.getAsJsonObject().entrySet()) { - if (!deviceEntry.getValue().isJsonArray()) { - log.warn("{}[{}]", CAN_T_PARSE_VALUE, json); - continue; - } + + List> deviceEntries = json.getAsJsonObject().entrySet().stream() + .filter(entry -> { + final boolean isArray = entry.getValue().isJsonArray(); + if (!isArray) { + log.warn("{} device='{}' value={}", CAN_T_PARSE_VALUE, entry.getKey(), entry.getValue()); + } + return isArray; + }) + .toList(); + + if (deviceEntries.isEmpty()) { + log.debug("[{}][{}][{}] Devices telemetry message is empty", gateway.getTenantId(), gateway.getDeviceId(), sessionId); + throw new IllegalArgumentException("[" + sessionId + "] Devices telemetry message is empty for [" + gateway.getDeviceId() + "]"); + } + + AtomicInteger remaining = new AtomicInteger(deviceEntries.size()); + AtomicBoolean ackSent = new AtomicBoolean(false); + + for (Map.Entry deviceEntry : deviceEntries) { String deviceName = deviceEntry.getKey(); - process(deviceName, deviceCtx -> processPostTelemetryMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId), - t -> processFailure(msgId, deviceName, TELEMETRY, t)); + process(deviceName, deviceCtx -> processPostTelemetryMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId, + remaining, ackSent), + t -> processFailure(msgId, deviceName, TELEMETRY, ackSent, t)); } } - private void processPostTelemetryMsg(T deviceCtx, JsonElement msg, String deviceName, int msgId) { + private void processPostTelemetryMsg(T deviceCtx, JsonElement msg, String deviceName, int msgId, AtomicInteger remaining, AtomicBoolean ackSent) { try { long systemTs = System.currentTimeMillis(); TbPair> gatewayPayloadPair = JsonConverter.convertToGatewayTelemetry(msg.getAsJsonArray(), systemTs); @@ -425,10 +443,10 @@ public abstract class AbstractGatewaySessionHandler { String deviceName = checkDeviceName(telemetryMsg.getDeviceName()); - process(deviceName, deviceCtx -> processPostTelemetryMsg(deviceCtx, telemetryMsg.getMsg(), deviceName, msgId), - t -> processFailure(msgId, deviceName, TELEMETRY, t)); + process(deviceName, deviceCtx -> processPostTelemetryMsg(deviceCtx, telemetryMsg.getMsg(), deviceName, msgId, + remaining, ackSent), + t -> processFailure(msgId, deviceName, TELEMETRY, ackSent, t)); }); } catch (RuntimeException | InvalidProtocolBufferException e) { throw new AdaptorException(e); } } - protected void processPostTelemetryMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostTelemetryMsg msg, String deviceName, int msgId) { + protected void processPostTelemetryMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostTelemetryMsg msg, String deviceName, int msgId, + AtomicInteger remaining, AtomicBoolean ackSent) { try { TransportProtos.PostTelemetryMsg postTelemetryMsg = ProtoConverter.validatePostTelemetryMsg(msg.toByteArray()); - transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(channel, deviceName, msgId, postTelemetryMsg)); + transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getAggregatePubAckCallback(channel, msgId, deviceName, postTelemetryMsg, remaining, ackSent)); } catch (Throwable e) { log.warn("[{}][{}][{}] Failed to convert telemetry: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, msg, e); - ackOrClose(msgId); + ackOrClose(msgId, ackSent); } } @@ -475,26 +498,42 @@ public abstract class AbstractGatewaySessionHandler deviceEntry : json.getAsJsonObject().entrySet()) { - if (!deviceEntry.getValue().isJsonObject()) { - log.warn("{}[{}]", CAN_T_PARSE_VALUE, json); - continue; - } + List> deviceEntries = json.getAsJsonObject().entrySet().stream() + .filter(entry -> { + boolean isJsonObject = entry.getValue().isJsonObject(); + if (!isJsonObject) { + log.warn("{} device='{}' value={}", CAN_T_PARSE_VALUE, entry.getKey(), entry.getValue()); + } + return isJsonObject; + }) + .toList(); + + if (deviceEntries.isEmpty()) { + log.debug("[{}][{}][{}] Devices claim message is empty", gateway.getTenantId(), gateway.getDeviceId(), sessionId); + throw new IllegalArgumentException("[" + sessionId + "] Devices claim message is empty for [" + gateway.getDeviceId() + "]"); + } + + AtomicInteger remaining = new AtomicInteger(deviceEntries.size()); + AtomicBoolean ackSent = new AtomicBoolean(false); + + for (Map.Entry deviceEntry : deviceEntries) { String deviceName = deviceEntry.getKey(); - process(deviceName, deviceCtx -> processClaimDeviceMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId), - t -> processFailure(msgId, deviceName, CLAIMING, t)); + process(deviceName, deviceCtx -> processClaimDeviceMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId, + remaining, ackSent), + t -> processFailure(msgId, deviceName, CLAIMING, ackSent, t)); } } - private void processClaimDeviceMsg(MqttDeviceAwareSessionContext deviceCtx, JsonElement claimRequest, String deviceName, int msgId) { + private void processClaimDeviceMsg(MqttDeviceAwareSessionContext deviceCtx, JsonElement claimRequest, String deviceName, int msgId, + AtomicInteger remaining, AtomicBoolean ackSent) { try { DeviceId deviceId = deviceCtx.getDeviceId(); TransportProtos.ClaimDeviceMsg claimDeviceMsg = JsonConverter.convertToClaimDeviceProto(deviceId, claimRequest); - transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(channel, deviceName, msgId, claimDeviceMsg)); + transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getAggregatePubAckCallback(channel, msgId, deviceName, claimDeviceMsg, remaining, ackSent)); } catch (Throwable e) { log.warn("[{}][{}][{}] Failed to convert claim message: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, claimRequest, e); - ackOrClose(msgId); + ackOrClose(msgId, ackSent); } } @@ -507,49 +546,70 @@ public abstract class AbstractGatewaySessionHandler { String deviceName = checkDeviceName(claimDeviceMsg.getDeviceName()); - process(deviceName, deviceCtx -> processClaimDeviceMsg(deviceCtx, claimDeviceMsg.getClaimRequest(), deviceName, msgId), - t -> processFailure(msgId, deviceName, CLAIMING, t)); + process(deviceName, deviceCtx -> processClaimDeviceMsg(deviceCtx, claimDeviceMsg.getClaimRequest(), deviceName, msgId, + remaining, ackSent), + t -> processFailure(msgId, deviceName, CLAIMING, ackSent, t)); }); } catch (RuntimeException | InvalidProtocolBufferException e) { throw new AdaptorException(e); } } - private void processClaimDeviceMsg(MqttDeviceAwareSessionContext deviceCtx, TransportApiProtos.ClaimDevice claimRequest, String deviceName, int msgId) { + private void processClaimDeviceMsg(MqttDeviceAwareSessionContext deviceCtx, TransportApiProtos.ClaimDevice claimRequest, String deviceName, int msgId, + AtomicInteger remaining, AtomicBoolean ackSent) { try { DeviceId deviceId = deviceCtx.getDeviceId(); TransportProtos.ClaimDeviceMsg claimDeviceMsg = ProtoConverter.convertToClaimDeviceProto(deviceId, claimRequest.toByteArray()); - transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(channel, deviceName, msgId, claimDeviceMsg)); + transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getAggregatePubAckCallback(channel, msgId, deviceName, claimDeviceMsg, remaining, ackSent)); } catch (Throwable e) { log.warn("[{}][{}][{}] Failed to convert claim message: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, claimRequest, e); - ackOrClose(msgId); + ackOrClose(msgId, ackSent); } } private void onDeviceAttributesJson(int msgId, ByteBuf payload) throws AdaptorException { JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); validateJsonObject(json); - for (Map.Entry deviceEntry : json.getAsJsonObject().entrySet()) { - if (!deviceEntry.getValue().isJsonObject()) { - log.warn("{}[{}]", CAN_T_PARSE_VALUE, json); - continue; - } + List> deviceEntries = json.getAsJsonObject().entrySet().stream() + .filter(entry -> { + boolean isJsonObject = entry.getValue().isJsonObject(); + if (!isJsonObject) { + log.warn("{} device='{}' value={}", CAN_T_PARSE_VALUE, entry.getKey(), entry.getValue()); + } + return isJsonObject; + }) + .toList(); + + if (deviceEntries.isEmpty()) { + log.debug("[{}][{}][{}] Devices attribute message is empty", gateway.getTenantId(), gateway.getDeviceId(), sessionId); + throw new IllegalArgumentException("[" + sessionId + "] Devices attribute message is empty for [" + gateway.getDeviceId() + "]"); + } + + AtomicInteger remaining = new AtomicInteger(deviceEntries.size()); + AtomicBoolean ackSent = new AtomicBoolean(false); + + for (Map.Entry deviceEntry : deviceEntries) { String deviceName = deviceEntry.getKey(); - process(deviceName, deviceCtx -> processPostAttributesMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId), - t -> processFailure(msgId, deviceName, ATTRIBUTE, t)); + process(deviceName, deviceCtx -> processPostAttributesMsg(deviceCtx, deviceEntry.getValue(), deviceName, msgId, + remaining, ackSent), + t -> processFailure(msgId, deviceName, ATTRIBUTE, ackSent, t)); } } - private void processPostAttributesMsg(MqttDeviceAwareSessionContext deviceCtx, JsonElement msg, String deviceName, int msgId) { + private void processPostAttributesMsg(MqttDeviceAwareSessionContext deviceCtx, JsonElement msg, String deviceName, int msgId, + AtomicInteger remaining, AtomicBoolean ackSent) { try { TransportProtos.PostAttributeMsg postAttributeMsg = JsonConverter.convertToAttributesProto(msg.getAsJsonObject()); - transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg)); + transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getAggregatePubAckCallback(channel, msgId, deviceName, postAttributeMsg, remaining, ackSent)); } catch (Throwable e) { log.warn("[{}][{}][{}] Failed to process device attributes command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, msg, e); - ackOrClose(msgId); + ackOrClose(msgId, ackSent); } } @@ -562,23 +622,28 @@ public abstract class AbstractGatewaySessionHandler { String deviceName = checkDeviceName(attributesMsg.getDeviceName()); - process(deviceName, deviceCtx -> processPostAttributesMsg(deviceCtx, attributesMsg.getMsg(), deviceName, msgId), - t -> processFailure(msgId, deviceName, ATTRIBUTE, t)); + process(deviceName, deviceCtx -> processPostAttributesMsg(deviceCtx, attributesMsg.getMsg(), deviceName, msgId, + remaining, ackSent), + t -> processFailure(msgId, deviceName, ATTRIBUTE, ackSent, t)); }); } catch (RuntimeException | InvalidProtocolBufferException e) { throw new AdaptorException(e); } } - protected void processPostAttributesMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostAttributeMsg kvListProto, String deviceName, int msgId) { + protected void processPostAttributesMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostAttributeMsg kvListProto, String deviceName, int msgId, + AtomicInteger remaining, AtomicBoolean ackSent) { try { TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto); - transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg)); + transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getAggregatePubAckCallback(channel, msgId, deviceName, postAttributeMsg, remaining, ackSent)); } catch (Throwable e) { log.warn("[{}][{}][{}] Failed to process device attributes command: [{}]", gateway.getTenantId(), gateway.getDeviceId(), deviceName, kvListProto, e); - ackOrClose(msgId); + ackOrClose(msgId, ackSent); } } @@ -647,26 +712,34 @@ public abstract class AbstractGatewaySessionHandler processRpcResponseMsg(deviceCtx, requestId, data, deviceName, msgId), - t -> processFailure(msgId, deviceName, RPC_RESPONSE, t)); + AtomicInteger remaining = new AtomicInteger(1); + AtomicBoolean ackSent = new AtomicBoolean(false); + process(deviceName, deviceCtx -> processRpcResponseMsg(deviceCtx, requestId, data, deviceName, msgId, remaining, ackSent), + t -> processFailure(msgId, deviceName, RPC_RESPONSE, ackSent, t)); } - private void processRpcResponseMsg(MqttDeviceAwareSessionContext deviceCtx, Integer requestId, String data, String deviceName, int msgId) { + private void processRpcResponseMsg(MqttDeviceAwareSessionContext deviceCtx, Integer requestId, String data, String deviceName, + int msgId, AtomicInteger remaining, AtomicBoolean ackSent) { TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() .setRequestId(requestId).setPayload(data).build(); - transportService.process(deviceCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(channel, deviceName, msgId, rpcResponseMsg)); + transportService.process(deviceCtx.getSessionInfo(), rpcResponseMsg, + getAggregatePubAckCallback(channel, msgId, deviceName, rpcResponseMsg, remaining, ackSent)); } private void processGetAttributeRequestMessage(MqttPublishMessage mqttMsg, String deviceName, TransportProtos.GetAttributeRequestMsg requestMsg) { int msgId = getMsgId(mqttMsg); - process(deviceName, deviceCtx -> processGetAttributeRequestMessage(deviceCtx, requestMsg, deviceName, msgId), - t -> { - processFailure(msgId, deviceName, ATTRIBUTES_REQUEST, t, MqttReasonCodes.PubAck.IMPLEMENTATION_SPECIFIC_ERROR); - }); + AtomicInteger remaining = new AtomicInteger(1); + AtomicBoolean ackSent = new AtomicBoolean(false); + process(deviceName, deviceCtx -> { + processGetAttributeRequestMessage(deviceCtx, requestMsg, deviceName, msgId, remaining, ackSent); + }, + t -> processFailure(msgId, deviceName, ATTRIBUTES_REQUEST, ackSent, MqttReasonCodes.PubAck.IMPLEMENTATION_SPECIFIC_ERROR, t)); } - private void processGetAttributeRequestMessage(T deviceCtx, TransportProtos.GetAttributeRequestMsg requestMsg, String deviceName, int msgId) { - transportService.process(deviceCtx.getSessionInfo(), requestMsg, getPubAckCallback(channel, deviceName, msgId, requestMsg)); + private void processGetAttributeRequestMessage(T deviceCtx, TransportProtos.GetAttributeRequestMsg requestMsg, + String deviceName, int msgId, AtomicInteger remaining, AtomicBoolean ackSent) { + transportService.process(deviceCtx.getSessionInfo(), requestMsg, + getAggregatePubAckCallback(channel, msgId, deviceName, requestMsg, remaining, ackSent)); } private TransportProtos.GetAttributeRequestMsg toGetAttributeRequestMsg(int requestId, boolean clientScope, Set keys) { @@ -717,9 +790,11 @@ public abstract class AbstractGatewaySessionHandler pubAckCallback = getAggregatePubAckCallback(channel, -1, deviceName, postTelemetryMsg, + new AtomicInteger(1), new AtomicBoolean(false)); + transportService.process(sessionInfo, postTelemetryMsg, pubAckCallback); } - public ConcurrentMap getDevices () { + public ConcurrentMap getDevices() { return this.devices; } - private TransportServiceCallback getPubAckCallback(final ChannelHandlerContext ctx, final String deviceName, final int msgId, final T msg) { + protected TransportServiceCallback getAggregatePubAckCallback( + final ChannelHandlerContext ctx, + final int msgId, + final String deviceName, + final T msg, + final AtomicInteger remaining, + final AtomicBoolean ackSent) { + return new TransportServiceCallback() { @Override public void onSuccess(Void dummy) { log.trace("[{}][{}][{}][{}] Published msg: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, deviceName, msg); - if (msgId > 0) { - ctx.writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(deviceSessionCtx, msgId, MqttReasonCodes.PubAck.SUCCESS.byteValue())); - } else { - log.trace("[{}][{}][{}] Wrong msg id: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, msg); - ctx.writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(deviceSessionCtx, msgId, MqttReasonCodes.PubAck.UNSPECIFIED_ERROR.byteValue())); - closeDeviceSession(deviceName, MqttReasonCodes.Disconnect.MALFORMED_PACKET); + if (remaining.decrementAndGet() == 0 && ackSent.compareAndSet(false, true)) { + if (msgId > 0) { + ctx.writeAndFlush(MqttTransportHandler.createMqttPubAckMsg( + deviceSessionCtx, msgId, MqttReasonCodes.PubAck.SUCCESS.byteValue())); + } else { + log.trace("[{}][{}][{}] Wrong msg id: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, msgId); + ctx.writeAndFlush(MqttTransportHandler.createMqttPubAckMsg( + deviceSessionCtx, msgId, MqttReasonCodes.PubAck.UNSPECIFIED_ERROR.byteValue())); + closeDeviceSession(deviceName, MqttReasonCodes.Disconnect.MALFORMED_PACKET); + } } } @Override public void onError(Throwable e) { log.trace("[{}][{}][{}] Failed to publish msg: [{}] for device: [{}]", gateway.getTenantId(), gateway.getDeviceId(), sessionId, msg, deviceName, e); - if (e instanceof TbRateLimitsException) { - closeDeviceSession(deviceName, MqttReasonCodes.Disconnect.MESSAGE_RATE_TOO_HIGH); - } else { - closeDeviceSession(deviceName, MqttReasonCodes.Disconnect.UNSPECIFIED_ERROR); + if (ackSent.compareAndSet(false, true)) { + if (e instanceof TbRateLimitsException) { + ctx.writeAndFlush(MqttTransportHandler.createMqttPubAckMsg( + deviceSessionCtx, msgId, MqttReasonCodes.PubAck.QUOTA_EXCEEDED.byteValue())); + closeDeviceSession(deviceName, MqttReasonCodes.Disconnect.MESSAGE_RATE_TOO_HIGH); + } else { + ctx.writeAndFlush(MqttTransportHandler.createMqttPubAckMsg( + deviceSessionCtx, msgId, MqttReasonCodes.PubAck.UNSPECIFIED_ERROR.byteValue())); + closeDeviceSession(deviceName, MqttReasonCodes.Disconnect.UNSPECIFIED_ERROR); + } + ctx.close(); } - ctx.close(); + remaining.set(0); } }; } @@ -789,18 +884,19 @@ public abstract class AbstractGatewaySessionHandler contextListenableFuture, int msgId, List postTelemetryMsgList, String deviceName) { + if (CollectionUtils.isEmpty(postTelemetryMsgList)) { + log.debug("[{}] Device telemetry list is empty for: [{}]", sessionId, gateway.getDeviceId()); + } + + AtomicInteger remaining = new AtomicInteger(postTelemetryMsgList.size()); + AtomicBoolean ackSent = new AtomicBoolean(false); + process(contextListenableFuture, deviceCtx -> { for (TransportProtos.PostTelemetryMsg telemetryMsg : postTelemetryMsgList) { try { - processPostTelemetryMsg(deviceCtx, telemetryMsg, deviceName, msgId); + processPostTelemetryMsg(deviceCtx, telemetryMsg, deviceName, msgId, remaining, ackSent); } catch (Throwable e) { log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, telemetryMsg, e); - ackOrClose(msgId); + ackOrClose(msgId, ackSent); } } }, - t -> log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t)); + t -> processFailure(msgId, deviceName, "Failed to process device telemetry command", ackSent, t)); } private void onDeviceAttributesProto(ListenableFuture contextListenableFuture, int msgId, List attributesMsgList, String deviceName) throws AdaptorException { try { if (CollectionUtils.isEmpty(attributesMsgList)) { - log.debug("[{}] Devices attributes keys list is empty for: [{}]", sessionId, gateway.getDeviceId()); + log.debug("[{}] Device attribute list is empty for: [{}]", sessionId, gateway.getDeviceId()); } + + AtomicInteger remaining = new AtomicInteger(attributesMsgList.size()); + AtomicBoolean ackSent = new AtomicBoolean(false); + process(contextListenableFuture, deviceCtx -> { for (TransportApiProtos.AttributesMsg attributesMsg : attributesMsgList) { TransportProtos.PostAttributeMsg kvListProto = attributesMsg.getMsg(); try { TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto); - processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); + processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId, remaining, ackSent); } catch (Throwable e) { log.warn("[{}][{}] Failed to process device attributes command: {}", gateway.getDeviceId(), deviceName, kvListProto, e); + ackOrClose(msgId, ackSent); } } }, - t -> log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t)); + t -> processFailure(msgId, deviceName, "Failed to process device attributes command", ackSent, t)); } catch (RuntimeException e) { throw new AdaptorException(e); } From fcba7004f94ab92521786bdcf3eeb61bd932ce79 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Thu, 2 Oct 2025 16:10:09 +0300 Subject: [PATCH 193/216] fixed tests to use new dynamic source configuration --- .../cf/CalculatedFieldIntegrationTest.java | 25 +++++-------- .../GeofencingCalculatedFieldStateTest.java | 17 +++------ .../data/cf/configuration/ArgumentTest.java | 2 +- .../ZoneGroupConfigurationTest.java | 4 +- .../dao/sql/relation/JpaRelationDao.java | 2 +- .../service/CalculatedFieldServiceTest.java | 37 +++++++++++-------- .../server/msa/cf/CalculatedFieldTest.java | 18 ++++----- 7 files changed, 48 insertions(+), 57 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java index ffd876575c..efcc38304a 100644 --- a/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/cf/CalculatedFieldIntegrationTest.java @@ -36,7 +36,7 @@ import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfig import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; -import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; @@ -47,10 +47,12 @@ import org.thingsboard.server.common.data.id.AssetProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.controller.CalculatedFieldControllerTest; import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -760,19 +762,13 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes // Zone groups: ATTRIBUTE on specific assets (one zone per group) ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); - var allowedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); - allowedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); - allowedZoneDynamicSourceConfiguration.setRelationType("AllowedZone"); - allowedZoneDynamicSourceConfiguration.setMaxLevel(1); - allowedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true); + var allowedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + allowedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "AllowedZone"))); allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration); ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); - var restrictedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); - restrictedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); - restrictedZoneDynamicSourceConfiguration.setRelationType("RestrictedZone"); - restrictedZoneDynamicSourceConfiguration.setMaxLevel(1); - restrictedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true); + var restrictedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + restrictedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "RestrictedZone"))); restrictedZonesGroup.setRefDynamicSourceConfiguration(restrictedZoneDynamicSourceConfiguration); cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup, "restrictedZones", restrictedZonesGroup)); @@ -870,11 +866,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes cfg.setEntityCoordinates(new EntityCoordinates(ENTITY_ID_LATITUDE_ARGUMENT_KEY, ENTITY_ID_LONGITUDE_ARGUMENT_KEY)); var allowedZonesGroup = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); - var allowedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); - allowedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); - allowedZoneDynamicSourceConfiguration.setRelationType("AllowedZone"); - allowedZoneDynamicSourceConfiguration.setMaxLevel(1); - allowedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true); + var allowedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + allowedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "AllowedZone"))); allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration); cfg.setZoneGroups(Map.of("allowedZones", allowedZonesGroup)); diff --git a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java index a86af77555..1f1ee32df2 100644 --- a/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cf/ctx/state/GeofencingCalculatedFieldStateTest.java @@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; -import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy; @@ -41,6 +41,7 @@ import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import org.thingsboard.server.service.cf.CalculatedFieldResult; @@ -453,21 +454,15 @@ public class GeofencingCalculatedFieldStateTest { config.setEntityCoordinates(entityCoordinates); ZoneGroupConfiguration allowedZonesGroup = new ZoneGroupConfiguration("zone", reportStrategy, true); - var allowedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); - allowedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.TO); - allowedZoneDynamicSourceConfiguration.setRelationType("AllowedZone"); - allowedZoneDynamicSourceConfiguration.setMaxLevel(1); - allowedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true); + var allowedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + allowedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.TO, "AllowedZone"))); allowedZonesGroup.setRefDynamicSourceConfiguration(allowedZoneDynamicSourceConfiguration); allowedZonesGroup.setRelationType("CurrentZone"); allowedZonesGroup.setDirection(EntitySearchDirection.TO); ZoneGroupConfiguration restrictedZonesGroup = new ZoneGroupConfiguration("zone", reportStrategy, true); - var restrictedZoneDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); - restrictedZoneDynamicSourceConfiguration.setDirection(EntitySearchDirection.TO); - restrictedZoneDynamicSourceConfiguration.setRelationType("RestrictedZone"); - restrictedZoneDynamicSourceConfiguration.setMaxLevel(1); - restrictedZoneDynamicSourceConfiguration.setFetchLastLevelOnly(true); + var restrictedZoneDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + restrictedZoneDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.TO, "RestrictedZone"))); restrictedZonesGroup.setRefDynamicSourceConfiguration(restrictedZoneDynamicSourceConfiguration); restrictedZonesGroup.setRelationType("CurrentZone"); restrictedZonesGroup.setDirection(EntitySearchDirection.TO); diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java index fd59317649..d1108e9eed 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/ArgumentTest.java @@ -31,7 +31,7 @@ public class ArgumentTest { @Test void validateShouldReturnTrueIfDynamicSourceConfigurationIsNotNull() { var argument = new Argument(); - argument.setRefDynamicSourceConfiguration(new RelationQueryDynamicSourceConfiguration()); + argument.setRefDynamicSourceConfiguration(new RelationPathQueryDynamicSourceConfiguration()); assertThat(argument.hasDynamicSource()).isTrue(); } diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java index 4eb822d93c..beb4639a31 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/geofencing/ZoneGroupConfigurationTest.java @@ -23,7 +23,7 @@ import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; -import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; @@ -100,7 +100,7 @@ public class ZoneGroupConfigurationTest { @Test void whenHasDynamicSourceCalled_shouldReturnTrueIfDynamicSourceConfigurationIsNotNull() { var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); - zoneGroupConfiguration.setRefDynamicSourceConfiguration(new RelationQueryDynamicSourceConfiguration()); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(new RelationPathQueryDynamicSourceConfiguration()); assertThat(zoneGroupConfiguration.hasDynamicSource()).isTrue(); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java index 782c9b9d53..b2871313ed 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/relation/JpaRelationDao.java @@ -307,7 +307,7 @@ public class JpaRelationDao extends JpaAbstractDaoListeningExecutorService imple String sql = buildRelationPathSql(query); Object[] params = buildRelationPathParams(query); - log.info("[{}] relation path query: {}", tenantId, sql); + log.trace("[{}] relation path query: {}", tenantId, sql); return jdbcTemplate.queryForList(sql, params).stream() .map(row -> { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java index 7f563ea436..4b830835fa 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/CalculatedFieldServiceTest.java @@ -31,7 +31,7 @@ import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfig import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; -import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; @@ -39,15 +39,19 @@ import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupC import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +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; +import static org.mockito.Mockito.mock; import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; @DaoSqlTest @@ -112,10 +116,8 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { // Zone-group argument (ATTRIBUTE) — make it DYNAMIC so scheduling is enabled ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration("allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); - var dynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); - dynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); - dynamicSourceConfiguration.setMaxLevel(1); - dynamicSourceConfiguration.setRelationType(EntityRelation.CONTAINS_TYPE); + var dynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + dynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE))); zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration)); @@ -150,19 +152,26 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { // Arrange a device Device device = createTestDevice(); - // Build a valid Geofencing configuration GeofencingCalculatedFieldConfiguration cfg = new GeofencingCalculatedFieldConfiguration(); // Coordinates: TS_LATEST, no dynamic source EntityCoordinates entityCoordinates = new EntityCoordinates("latitude", "longitude"); cfg.setEntityCoordinates(entityCoordinates); - // Zone-group argument (ATTRIBUTE) — make it DYNAMIC so scheduling is enabled + int maxRelationLevel = tbTenantProfileCache.get(tenantId) + .getDefaultProfileConfiguration() + .getMaxRelationLevelPerCfArgument(); + + // Zone-group argument (ATTRIBUTE) ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration( "allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); - var dynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); - dynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); - dynamicSourceConfiguration.setMaxLevel(Integer.MAX_VALUE); - dynamicSourceConfiguration.setRelationType(EntityRelation.CONTAINS_TYPE); + var dynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + + List levels = new ArrayList<>(); + for (int i = 0; i < maxRelationLevel + 1; i++) { + levels.add(mock(RelationPathLevel.class)); + } + + dynamicSourceConfiguration.setLevels(levels); zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration)); @@ -195,10 +204,8 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest { // Zone-group argument (ATTRIBUTE) — make it DYNAMIC so scheduling is enabled ZoneGroupConfiguration zoneGroupConfiguration = new ZoneGroupConfiguration( "allowed", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); - var dynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); - dynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); - dynamicSourceConfiguration.setMaxLevel(1); - dynamicSourceConfiguration.setRelationType(EntityRelation.CONTAINS_TYPE); + var dynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + dynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, EntityRelation.CONTAINS_TYPE))); zoneGroupConfiguration.setRefDynamicSourceConfiguration(dynamicSourceConfiguration); cfg.setZoneGroups(Map.of("allowed", zoneGroupConfiguration)); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java index 7f2dfba937..5e8d367538 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/cf/CalculatedFieldTest.java @@ -33,7 +33,7 @@ import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; -import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; @@ -50,10 +50,12 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.msa.AbstractContainerTest; import org.thingsboard.server.msa.ui.utils.EntityPrototypes; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -366,19 +368,13 @@ public class CalculatedFieldTest extends AbstractContainerTest { // Dynamic groups via relations ZoneGroupConfiguration allowedZoneGroupConfiguration = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); - var allowedDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); - allowedDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); - allowedDynamicSourceConfiguration.setMaxLevel(1); - allowedDynamicSourceConfiguration.setFetchLastLevelOnly(true); - allowedDynamicSourceConfiguration.setRelationType("AllowedZone"); + var allowedDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + allowedDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "AllowedZone"))); allowedZoneGroupConfiguration.setRefDynamicSourceConfiguration(allowedDynamicSourceConfiguration); ZoneGroupConfiguration restrictedZoneGroupConfiguration = new ZoneGroupConfiguration("zone", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); - var restrictedDynamicSourceConfiguration = new RelationQueryDynamicSourceConfiguration(); - restrictedDynamicSourceConfiguration.setDirection(EntitySearchDirection.FROM); - restrictedDynamicSourceConfiguration.setMaxLevel(1); - restrictedDynamicSourceConfiguration.setFetchLastLevelOnly(true); - restrictedDynamicSourceConfiguration.setRelationType("RestrictedZone"); + var restrictedDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + restrictedDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.FROM, "RestrictedZone"))); restrictedZoneGroupConfiguration.setRefDynamicSourceConfiguration(restrictedDynamicSourceConfiguration); cfg.setZoneGroups(Map.of("allowedZones", allowedZoneGroupConfiguration, "restrictedZones", restrictedZoneGroupConfiguration)); From eea9e6bf6e43e38c5cd65ef5ca83db4ffb456ab9 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Fri, 3 Oct 2025 11:43:24 +0300 Subject: [PATCH 194/216] Removed RELATION_QUERY type --- ...tractCalculatedFieldProcessingService.java | 16 -- .../CFArgumentDynamicSourceType.java | 2 +- ...onPathQueryDynamicSourceConfiguration.java | 14 +- .../cf/configuration/RelationQueryBased.java | 29 --- ...lationQueryDynamicSourceConfiguration.java | 79 ------- ...onQueryDynamicSourceConfigurationTest.java | 214 ------------------ .../dao/relation/BaseRelationService.java | 7 + .../CalculatedFieldDataValidator.java | 6 +- 8 files changed, 17 insertions(+), 350 deletions(-) delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryBased.java delete mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java delete mode 100644 common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java index bd3546082a..f1b39d87d8 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java @@ -27,7 +27,6 @@ import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.cf.configuration.Argument; import org.thingsboard.server.common.data.cf.configuration.ArgumentType; import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; -import org.thingsboard.server.common.data.cf.configuration.RelationQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.kv.Aggregation; @@ -164,21 +163,6 @@ public abstract class AbstractCalculatedFieldProcessingService { } var refDynamicSourceConfiguration = value.getRefDynamicSourceConfiguration(); return switch (refDynamicSourceConfiguration.getType()) { - case RELATION_QUERY -> { - var configuration = (RelationQueryDynamicSourceConfiguration) refDynamicSourceConfiguration; - if (configuration.isSimpleRelation()) { - yield switch (configuration.getDirection()) { - case FROM -> - Futures.transform(relationService.findByFromAndTypeAsync(tenantId, entityId, configuration.getRelationType(), RelationTypeGroup.COMMON), - configuration::resolveEntityIds, calculatedFieldCallbackExecutor); - case TO -> - Futures.transform(relationService.findByToAndTypeAsync(tenantId, entityId, configuration.getRelationType(), RelationTypeGroup.COMMON), - configuration::resolveEntityIds, calculatedFieldCallbackExecutor); - }; - } - yield Futures.transform(relationService.findByQuery(tenantId, configuration.toEntityRelationsQuery(entityId)), - configuration::resolveEntityIds, calculatedFieldCallbackExecutor); - } case RELATION_PATH_QUERY -> { var configuration = (RelationPathQueryDynamicSourceConfiguration) refDynamicSourceConfiguration; yield Futures.transform(relationService.findByRelationPathQueryAsync(tenantId, configuration.toRelationPathQuery(entityId)), diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java index 35c6cdf562..dc52287f3e 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/CFArgumentDynamicSourceType.java @@ -17,6 +17,6 @@ package org.thingsboard.server.common.data.cf.configuration; public enum CFArgumentDynamicSourceType { - RELATION_QUERY, RELATION_PATH_QUERY + RELATION_PATH_QUERY } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java index c7889be19d..dc92ff3685 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationPathQueryDynamicSourceConfiguration.java @@ -28,7 +28,7 @@ import java.util.List; import java.util.NoSuchElementException; @Data -public class RelationPathQueryDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration, RelationQueryBased { +public class RelationPathQueryDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration { private List levels; @@ -53,10 +53,11 @@ public class RelationPathQueryDynamicSourceConfiguration implements CfArgumentDy }; } - @Override - @JsonIgnore - public int getMaxLevel() { - return levels != null ? levels.size() : 0; + public void validateMaxRelationLevel(String argumentName, int maxAllowedRelationLevel) { + if (levels.size() > maxAllowedRelationLevel) { + throw new IllegalArgumentException("Max relation level is greater than configured " + + "maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + argumentName); + } } public EntityRelationPathQuery toRelationPathQuery(EntityId entityId) { @@ -64,9 +65,6 @@ public class RelationPathQueryDynamicSourceConfiguration implements CfArgumentDy } private RelationPathLevel getLastLevel() { - if (CollectionsUtil.isEmpty(levels)) { - throw new NoSuchElementException(); - } return levels.get(levels.size() - 1); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryBased.java deleted file mode 100644 index e3248b264f..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryBased.java +++ /dev/null @@ -1,29 +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.common.data.cf.configuration; - -public interface RelationQueryBased { - - int getMaxLevel(); - - default void validateMaxRelationLevel(String argumentName, int maxAllowedRelationLevel) { - if (getMaxLevel() > maxAllowedRelationLevel) { - throw new IllegalArgumentException("Max relation level is greater than configured " + - "maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + argumentName); - } - } - -} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java deleted file mode 100644 index 120d04d40d..0000000000 --- a/common/data/src/main/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfiguration.java +++ /dev/null @@ -1,79 +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.common.data.cf.configuration; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import lombok.Data; -import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.common.data.relation.EntityRelationsQuery; -import org.thingsboard.server.common.data.relation.EntitySearchDirection; -import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; -import org.thingsboard.server.common.data.relation.RelationsSearchParameters; - -import java.util.Collections; -import java.util.List; - -@Data -public class RelationQueryDynamicSourceConfiguration implements CfArgumentDynamicSourceConfiguration, RelationQueryBased { - - private int maxLevel; - private boolean fetchLastLevelOnly; - private EntitySearchDirection direction; - private String relationType; - - @Override - public CFArgumentDynamicSourceType getType() { - return CFArgumentDynamicSourceType.RELATION_QUERY; - } - - @Override - public void validate() { - if (maxLevel < 1) { - throw new IllegalArgumentException("Relation query dynamic source configuration max relation level can't be less than 1!"); - } - if (direction == null) { - throw new IllegalArgumentException("Relation query dynamic source configuration direction must be specified!"); - } - if (StringUtils.isBlank(relationType)) { - throw new IllegalArgumentException("Relation query dynamic source configuration relation type must be specified!"); - } - } - - @JsonIgnore - public boolean isSimpleRelation() { - return maxLevel == 1; - } - - public EntityRelationsQuery toEntityRelationsQuery(EntityId rootEntityId) { - if (isSimpleRelation()) { - throw new IllegalArgumentException("Entity relations query can't be created for a simple relation!"); - } - var entityRelationsQuery = new EntityRelationsQuery(); - entityRelationsQuery.setParameters(new RelationsSearchParameters(rootEntityId, direction, maxLevel, fetchLastLevelOnly)); - entityRelationsQuery.setFilters(Collections.singletonList(new RelationEntityTypeFilter(relationType, Collections.emptyList()))); - return entityRelationsQuery; - } - - public List resolveEntityIds(List relations) { - return switch (direction) { - case FROM -> relations.stream().map(EntityRelation::getTo).toList(); - case TO -> relations.stream().map(EntityRelation::getFrom).toList(); - }; - } - -} diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java deleted file mode 100644 index afd78e47f9..0000000000 --- a/common/data/src/test/java/org/thingsboard/server/common/data/cf/configuration/RelationQueryDynamicSourceConfigurationTest.java +++ /dev/null @@ -1,214 +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.common.data.cf.configuration; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.id.EntityId; -import org.thingsboard.server.common.data.relation.EntityRelation; -import org.thingsboard.server.common.data.relation.EntitySearchDirection; -import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; -import org.thingsboard.server.common.data.relation.RelationsSearchParameters; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -public class RelationQueryDynamicSourceConfigurationTest { - - @Mock - EntityId rootEntityId; - - @Mock - EntityRelation rel1; - @Mock - EntityRelation rel2; - - @Test - void typeShouldBeRelationQuery() { - var cfg = new RelationQueryDynamicSourceConfiguration(); - assertThat(cfg.getType()).isEqualTo(CFArgumentDynamicSourceType.RELATION_QUERY); - } - - @Test - void validateShouldThrowWhenMaxLevelLessThanOne() { - var cfg = new RelationQueryDynamicSourceConfiguration(); - cfg.setMaxLevel(0); - cfg.setDirection(EntitySearchDirection.FROM); - cfg.setRelationType(EntityRelation.CONTAINS_TYPE); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Relation query dynamic source configuration max relation level can't be less than 1!"); - } - - @Test - void validateShouldThrowWhenMaxLevelGreaterThanMaxAllowedLevelFromTenantProfile() { - int maxAllowedRelationLevel = 2; - int argumentMaxRelationLevel = 3; - - var cfg = new RelationQueryDynamicSourceConfiguration(); - cfg.setMaxLevel(argumentMaxRelationLevel); - cfg.setDirection(EntitySearchDirection.FROM); - cfg.setRelationType(EntityRelation.CONTAINS_TYPE); - - String testRelationArgument = "testRelationArgument"; - assertThatThrownBy(() -> cfg.validateMaxRelationLevel(testRelationArgument, maxAllowedRelationLevel)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Max relation level is greater than configured " + - "maximum allowed relation level in tenant profile: " + maxAllowedRelationLevel + " for argument: " + testRelationArgument); - } - - @Test - void validateShouldPassValidationWhenMaxLevelLessThanMaxAllowedLevelFromTenantProfile() { - int maxAllowedRelationLevel = 5; - int argumentMaxRelationLevel = 2; - - var cfg = new RelationQueryDynamicSourceConfiguration(); - cfg.setMaxLevel(argumentMaxRelationLevel); - cfg.setDirection(EntitySearchDirection.FROM); - cfg.setRelationType(EntityRelation.CONTAINS_TYPE); - - String testRelationArgument = "testRelationArgument"; - assertThatCode(() -> cfg.validateMaxRelationLevel(testRelationArgument, maxAllowedRelationLevel)).doesNotThrowAnyException(); - } - - @Test - void validateShouldThrowWhenDirectionIsNull() { - var cfg = new RelationQueryDynamicSourceConfiguration(); - cfg.setMaxLevel(1); - cfg.setDirection(null); - cfg.setRelationType(EntityRelation.CONTAINS_TYPE); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Relation query dynamic source configuration direction must be specified!"); - } - - @ParameterizedTest - @ValueSource(strings = {" "}) - @NullAndEmptySource - void validateShouldThrowWhenRelationTypeIsNull(String relationType) { - var cfg = new RelationQueryDynamicSourceConfiguration(); - cfg.setMaxLevel(1); - cfg.setDirection(EntitySearchDirection.TO); - cfg.setRelationType(relationType); - - assertThatThrownBy(cfg::validate) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Relation query dynamic source configuration relation type must be specified!"); - } - - @Test - void isSimpleRelationTrueWhenLevelIsOneAndEntityTypesEmptyOrNull() { - var cfg = new RelationQueryDynamicSourceConfiguration(); - cfg.setMaxLevel(1); - assertThat(cfg.isSimpleRelation()).isTrue(); - } - - @Test - void isSimpleRelationFalseWhenMaxLevelNotOne() { - var cfg = new RelationQueryDynamicSourceConfiguration(); - cfg.setMaxLevel(2); - assertThat(cfg.isSimpleRelation()).isFalse(); - } - - @Test - void toEntityRelationsQueryShouldThrowForSimpleRelation() { - var cfg = new RelationQueryDynamicSourceConfiguration(); - cfg.setMaxLevel(1); - cfg.setFetchLastLevelOnly(false); - cfg.setDirection(EntitySearchDirection.FROM); - cfg.setRelationType(EntityRelation.CONTAINS_TYPE); - - assertThatThrownBy(() -> cfg.toEntityRelationsQuery(rootEntityId)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Entity relations query can't be created for a simple relation!"); - } - - @Test - void toEntityRelationsQueryShouldBuildQueryForNonSimpleRelation() { - var cfg = new RelationQueryDynamicSourceConfiguration(); - cfg.setMaxLevel(2); - cfg.setFetchLastLevelOnly(true); - cfg.setDirection(EntitySearchDirection.TO); - cfg.setRelationType(EntityRelation.MANAGES_TYPE); - - var query = cfg.toEntityRelationsQuery(rootEntityId); - - assertThat(query).isNotNull(); - RelationsSearchParameters params = query.getParameters(); - assertThat(params).isNotNull(); - assertThat(params.getRootId()).isEqualTo(rootEntityId.getId()); - assertThat(params.getDirection()).isEqualTo(EntitySearchDirection.TO); - assertThat(params.getMaxLevel()).isEqualTo(2); - assertThat(params.isFetchLastLevelOnly()).isTrue(); - - assertThat(query.getFilters()).hasSize(1); - assertThat(query.getFilters().get(0)).isInstanceOf(RelationEntityTypeFilter.class); - RelationEntityTypeFilter filter = query.getFilters().get(0); - assertThat(filter.getRelationType()).isEqualTo(EntityRelation.MANAGES_TYPE); - } - - @Test - void resolveEntityIds_whenDirectionFROM_thenReturnsToIds() { - when(rel1.getTo()).thenReturn(mock(EntityId.class)); - when(rel2.getTo()).thenReturn(mock(EntityId.class)); - - var cfg = new RelationQueryDynamicSourceConfiguration(); - cfg.setDirection(EntitySearchDirection.FROM); - - var out = cfg.resolveEntityIds(List.of(rel1, rel2)); - - assertThat(out).containsExactly(rel1.getTo(), rel2.getTo()); - } - - @Test - void resolveEntityIds_whenDirectionTO_thenReturnsFromIds() { - when(rel1.getFrom()).thenReturn(mock(EntityId.class)); - when(rel2.getFrom()).thenReturn(mock(EntityId.class)); - - var cfg = new RelationQueryDynamicSourceConfiguration(); - cfg.setDirection(EntitySearchDirection.TO); - - var out = cfg.resolveEntityIds(List.of(rel1, rel2)); - - assertThat(out).containsExactly(rel1.getFrom(), rel2.getFrom()); - } - - @Test - void validateShouldPassForValidConfig() { - var cfg = new RelationQueryDynamicSourceConfiguration(); - cfg.setMaxLevel(2); - cfg.setFetchLastLevelOnly(false); - cfg.setDirection(EntitySearchDirection.FROM); - cfg.setRelationType(EntityRelation.CONTAINS_TYPE); - - assertThatCode(cfg::validate).doesNotThrowAnyException(); - } - -} diff --git a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java index 0ec1257bcc..18d1806fe4 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java @@ -504,6 +504,13 @@ public class BaseRelationService implements RelationService { log.trace("Executing findByRelationPathQuery, tenantId [{}], relationPathQuery {}", tenantId, relationPathQuery); validateId(tenantId, id -> "Invalid tenant id: " + id); validate(relationPathQuery); + if (relationPathQuery.levels().size() == 1) { + RelationPathLevel relationPathLevel = relationPathQuery.levels().get(0); + return switch (relationPathLevel.direction()) { + case FROM -> findByFromAndTypeAsync(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON); + case TO -> findByToAndTypeAsync(tenantId, relationPathQuery.rootEntityId(), relationPathLevel.relationType(), RelationTypeGroup.COMMON); + }; + } return executor.submit(() -> relationDao.findByRelationPathQuery(tenantId, relationPathQuery)); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java index 85c30ca74e..67cf32191b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/CalculatedFieldDataValidator.java @@ -19,7 +19,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.cf.CalculatedField; import org.thingsboard.server.common.data.cf.configuration.ArgumentsBasedCalculatedFieldConfiguration; -import org.thingsboard.server.common.data.cf.configuration.RelationQueryBased; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; @@ -98,10 +98,10 @@ public class CalculatedFieldDataValidator extends DataValidator if (!(calculatedField.getConfiguration() instanceof ArgumentsBasedCalculatedFieldConfiguration argumentsBasedCfg)) { return; } - Map relationQueryBasedArguments = argumentsBasedCfg.getArguments().entrySet() + Map relationQueryBasedArguments = argumentsBasedCfg.getArguments().entrySet() .stream() .filter(entry -> entry.getValue().hasDynamicSource()) - .collect(Collectors.toMap(Map.Entry::getKey, entry -> (RelationQueryBased) entry.getValue().getRefDynamicSourceConfiguration())); + .collect(Collectors.toMap(Map.Entry::getKey, entry -> (RelationPathQueryDynamicSourceConfiguration) entry.getValue().getRefDynamicSourceConfiguration())); if (relationQueryBasedArguments.isEmpty()) { return; } From 95583a0703bef23ed36b0da532301931326c2953 Mon Sep 17 00:00:00 2001 From: dshvaika Date: Fri, 3 Oct 2025 12:18:19 +0300 Subject: [PATCH 195/216] Added minor test to CalculatedFieldController and todo for geo CF state --- ...tractCalculatedFieldProcessingService.java | 1 + .../CalculatedFieldControllerTest.java | 66 ++++++++++++++++++- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java index f1b39d87d8..fa10a49503 100644 --- a/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java +++ b/application/src/main/java/org/thingsboard/server/service/cf/AbstractCalculatedFieldProcessingService.java @@ -103,6 +103,7 @@ public abstract class AbstractCalculatedFieldProcessingService { return Futures.whenAllComplete(argFutures.values()).call(() -> { var result = createStateByType(ctx); result.updateState(ctx, resolveArgumentFutures(argFutures)); + // TODO: move to state.init() method after merge with alarm rules 2.0 if (ctx.hasRelationQueryDynamicArguments() && result instanceof GeofencingCalculatedFieldState geofencingCalculatedFieldState) { geofencingCalculatedFieldState.setLastDynamicArgumentsRefreshTs(System.currentTimeMillis()); } diff --git a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java index af43b34558..27622b347a 100644 --- a/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/CalculatedFieldControllerTest.java @@ -29,15 +29,23 @@ import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfig import org.thingsboard.server.common.data.cf.configuration.Output; import org.thingsboard.server.common.data.cf.configuration.OutputType; import org.thingsboard.server.common.data.cf.configuration.ReferencedEntityKey; +import org.thingsboard.server.common.data.cf.configuration.RelationPathQueryDynamicSourceConfiguration; import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates; +import org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingCalculatedFieldConfiguration; +import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupConfiguration; import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationPathLevel; import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.dao.service.DaoSqlTest; +import java.util.List; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.common.data.cf.configuration.geofencing.GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS; @DaoSqlTest public class CalculatedFieldControllerTest extends AbstractControllerTest { @@ -84,7 +92,35 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId()); assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); - assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getCalculatedFieldConfig()); + assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getSimpleCalculatedFieldConfig()); + assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); + + savedCalculatedField.setName("Test CF"); + + CalculatedField updatedCalculatedField = doPost("/api/calculatedField", savedCalculatedField, CalculatedField.class); + + assertThat(updatedCalculatedField.getName()).isEqualTo(savedCalculatedField.getName()); + assertThat(updatedCalculatedField.getVersion()).isEqualTo(savedCalculatedField.getVersion() + 1); + + doDelete("/api/calculatedField/" + savedCalculatedField.getId().getId().toString()) + .andExpect(status().isOk()); + } + + @Test + public void testSaveGeofencingCalculatedField() throws Exception { + Device testDevice = createDevice("Test device", "1234567890"); + CalculatedField calculatedField = getCalculatedField(testDevice.getId(), getGeofencingCalculatedFieldConfig()); + + CalculatedField savedCalculatedField = doPost("/api/calculatedField", calculatedField, CalculatedField.class); + + assertThat(savedCalculatedField).isNotNull(); + assertThat(savedCalculatedField.getId()).isNotNull(); + assertThat(savedCalculatedField.getCreatedTime()).isGreaterThan(0); + assertThat(savedCalculatedField.getTenantId()).isEqualTo(savedTenant.getId()); + assertThat(savedCalculatedField.getEntityId()).isEqualTo(calculatedField.getEntityId()); + assertThat(savedCalculatedField.getType()).isEqualTo(calculatedField.getType()); + assertThat(savedCalculatedField.getName()).isEqualTo(calculatedField.getName()); + assertThat(savedCalculatedField.getConfiguration()).isEqualTo(getGeofencingCalculatedFieldConfig()); assertThat(savedCalculatedField.getVersion()).isEqualTo(1L); savedCalculatedField.setName("Test CF"); @@ -128,17 +164,41 @@ public class CalculatedFieldControllerTest extends AbstractControllerTest { } private CalculatedField getCalculatedField(DeviceId deviceId) { + return getCalculatedField(deviceId, getSimpleCalculatedFieldConfig()); + } + + private CalculatedField getCalculatedField(DeviceId deviceId, CalculatedFieldConfiguration configuration) { CalculatedField calculatedField = new CalculatedField(); calculatedField.setEntityId(deviceId); calculatedField.setType(CalculatedFieldType.SIMPLE); calculatedField.setName("Test Calculated Field"); calculatedField.setConfigurationVersion(1); - calculatedField.setConfiguration(getCalculatedFieldConfig()); + calculatedField.setConfiguration(configuration); calculatedField.setVersion(1L); return calculatedField; } - private CalculatedFieldConfiguration getCalculatedFieldConfig() { + private CalculatedFieldConfiguration getGeofencingCalculatedFieldConfig() { + var config = new GeofencingCalculatedFieldConfiguration(); + + var refDynamicSourceConfiguration = new RelationPathQueryDynamicSourceConfiguration(); + refDynamicSourceConfiguration.setLevels(List.of(new RelationPathLevel(EntitySearchDirection.TO, "FromSafeArea"))); + + var zoneGroupConfiguration = new ZoneGroupConfiguration("perimeter", REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false); + zoneGroupConfiguration.setRefDynamicSourceConfiguration(refDynamicSourceConfiguration); + + Output output = new Output(); + output.setType(OutputType.TIME_SERIES); + + config.setEntityCoordinates(new EntityCoordinates("latitide", "longitude")); + config.setZoneGroups(Map.of("safeArea", zoneGroupConfiguration)); + config.setScheduledUpdateEnabled(false); + config.setOutput(output); + + return config; + } + + private CalculatedFieldConfiguration getSimpleCalculatedFieldConfig() { SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration(); Argument argument = new Argument(); From 6b5e33992182b0825b3dd145f545e4851910b224 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Fri, 3 Oct 2025 16:21:03 +0300 Subject: [PATCH 196/216] UI: fixed and improve geofencing cf --- .../calculated-field-dialog.component.html | 8 +- .../calculated-field-dialog.component.ts | 19 +++ ...-geofencing-zone-groups-table.component.ts | 2 +- ...eofencing-zone-groups-panel.component.html | 146 +++++++++++------- ...eofencing-zone-groups-panel.component.scss | 25 +++ ...-geofencing-zone-groups-panel.component.ts | 79 ++++++++-- .../shared/models/calculated-field.models.ts | 14 +- .../assets/locale/locale.constant-en_US.json | 27 +++- 8 files changed, 239 insertions(+), 81 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html index 4b12d9f31a..e235b66fa7 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.html @@ -160,12 +160,16 @@ [tenantId]="data.tenantId" [entityName]="data.entityName"/>
-
{{ 'calculated-fields.zone-group-refresh-interval' | translate }}
+ +
+ {{ 'calculated-fields.zone-group-refresh-interval' | translate }} +
+
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts index 3037b3a4ce..8cca16d4ef 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/dialog/calculated-field-dialog.component.ts @@ -81,6 +81,7 @@ export class CalculatedFieldDialogComponent extends DialogComponent + this.checkScheduledUpdateEnabled(value) + ); + this.checkScheduledUpdateEnabled(this.configFormGroup.get('scheduledUpdateEnabled').value); + } + + private checkScheduledUpdateEnabled(value: boolean) { + if (value) { + this.configFormGroup.get('scheduledUpdateInterval').enable({emitEvent: false}); + } else { + this.configFormGroup.get('scheduledUpdateInterval').disable({emitEvent: false}); + } + } + private checkRelatedEntity(zoneGroups: CalculatedFieldGeofencing) { this.isRelatedEntity = Object.values(zoneGroups).some(zone => zone.refDynamicSourceConfiguration?.type === ArgumentEntityType.RelationQuery); } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.ts index a11ae4cea5..9d2a124d68 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.ts @@ -168,7 +168,7 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro renderer: this.renderer, componentType: CalculatedFieldGeofencingZoneGroupsPanelComponent, hostView: this.viewContainerRef, - preferredPlacement: isExists ? ['left', 'leftTop', 'leftBottom'] : ['topRight', 'right', 'rightTop'], + preferredPlacement: 'right', context: ctx, isModal: true }); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.html index a660158ab9..1c57cfbc02 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.html @@ -80,12 +80,13 @@ @if (ArgumentEntityTypeParamsMap.has(entityType)) {
{{ ArgumentEntityTypeParamsMap.get(entityType).title | translate }}
-
@@ -94,76 +95,115 @@
- {{ 'calculated-fields.relation-query' | translate }}* - -
-
{{ 'calculated-fields.direction' | translate }}
- - - @for (direction of GeofencingDirectionList; track direction) { - {{ GeofencingDirectionTranslations.get(direction) | translate }} + {{ 'calculated-fields.entity-zone-relationship' | translate }} +
+
+
calculated-fields.level
+
calculated-fields.direction-level
+
calculated-fields.relation-type
+
+
+ @if (levelsFormArray()?.controls?.length) { +
+ @for (keyControl of levelsFormArray().controls; track trackByKey;) { +
+
+
{{ $index+1 }}
+ + + @for (direction of GeofencingDirectionList; track direction) { + {{ GeofencingDirectionLevelTranslations.get(direction) | translate }} + } + + + + +
+
+ + +
+
} - - +
+ } @else { + {{ 'calculated-fields.no-level' | translate }} + } + @if (levelsFormArray().errors) { + + }
-
-
{{ 'calculated-fields.relation-type' | translate }}
- - +
+ @if (maxRelationLevelPerCfArgument && levelsFormArray().length >= maxRelationLevelPerCfArgument) { +
+ warning + {{ 'calculated-fields.max-allowed-levels-error' | translate }} +
+ } @else { + + }
-
-
{{ 'calculated-fields.relation-level' | translate }}
+ +
+ + + @if (entityFilter.singleEntity.id) { +
+
+ {{ 'calculated-fields.perimeter-attribute-key' | translate }} +
+ @if (entityType === ArgumentEntityType.RelationQuery) { - - @if (refDynamicSourceFormGroup.get('maxLevel').touched && refDynamicSourceFormGroup.get('maxLevel').hasError('required')) { - - warning - - } @else if (refDynamicSourceFormGroup.get('maxLevel').touched && refDynamicSourceFormGroup.get('maxLevel').hasError('min')) { + + @if (geofencingFormGroup.get('perimeterKeyName').touched && geofencingFormGroup.get('perimeterKeyName').hasError('required')) { warning - } @else if (refDynamicSourceFormGroup.get('maxLevel').touched && refDynamicSourceFormGroup.get('maxLevel').hasError('max')) { + } @else if (geofencingFormGroup.get('perimeterKeyName').touched && geofencingFormGroup.get('perimeterKeyName').hasError('pattern')) { warning } -
-
- - {{ 'calculated-fields.fetch-last-available-level' | translate }} - -
- -
- - -
-
- {{ 'calculated-fields.perimeter-attribute-key' | translate }} + } @else { + + }
- -
+ }
{{ 'calculated-fields.report-strategy' | translate }}
diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.scss index bedaf2eeb0..ff6140f12c 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.scss @@ -29,6 +29,26 @@ $panel-width: 520px; } } + .level-text { + width: 25px; + color: rgba(0, 0, 0, 0.54); + } + + .tb-form-table { + .tb-form-row { + gap: 12px; + } + .tb-form-table-body { + gap: unset; + } + } + .tb-form-table-header-cell { + &.tb-actions-header { + width: 80px; + min-width: 80px; + } + } + .limit-field-row { @media screen and (max-width: $panel-width) { display: flex; @@ -48,4 +68,9 @@ $panel-width: 520px; flex-direction: column; } } + tb-entity-autocomplete { + .mat-mdc-form-field-has-icon-suffix .mat-mdc-text-field-wrapper { + padding-right: 0 !important; + } + } } diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.ts index 72ea58919f..8e5921516e 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.ts @@ -16,7 +16,15 @@ import { AfterViewInit, ChangeDetectorRef, Component, Input, OnInit, output, ViewChild } from '@angular/core'; import { TbPopoverComponent } from '@shared/components/popover.component'; -import { FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; +import { + AbstractControl, + FormBuilder, + FormControl, + FormGroup, + UntypedFormArray, + ValidatorFn, + Validators +} from '@angular/forms'; import { charsWithNumRegex, oneSpaceInsideRegex } from '@shared/models/regex.constants'; import { ArgumentEntityType, @@ -25,14 +33,15 @@ import { CalculatedFieldGeofencing, CalculatedFieldGeofencingValue, CalculatedFieldType, + GeofencingDirectionLevelTranslations, GeofencingDirectionTranslations, GeofencingReportStrategy, GeofencingReportStrategyTranslations, getCalculatedFieldCurrentEntityFilter } from '@shared/models/calculated-field.models'; -import { debounceTime, delay, distinctUntilChanged, filter, map } from 'rxjs/operators'; +import { debounceTime, delay, distinctUntilChanged, map } from 'rxjs/operators'; import { EntityType } from '@shared/models/entity-type.models'; -import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; +import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models'; import { EntityId } from '@shared/models/id/entity-id'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { EntityFilter } from '@shared/models/query/query.models'; @@ -44,6 +53,7 @@ import { Store } from '@ngrx/store'; import { EntityAutocompleteComponent } from '@shared/components/entity/entity-autocomplete.component'; import { NULL_UUID } from '@shared/models/id/has-uuid'; import { EntitySearchDirection } from '@shared/models/relation.models'; +import { CdkDragDrop } from "@angular/cdk/drag-drop"; @Component({ selector: 'tb-calculated-field-geofencing-zone-groups-panel', @@ -73,12 +83,9 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit id: [''] }), refDynamicSourceConfiguration: this.fb.group({ - direction: [EntitySearchDirection.TO], - relationType: ['', [Validators.required]], - maxLevel: [1, [Validators.required, Validators.min(1), Validators.max(this.maxRelationLevelPerCfArgument)]], - fetchLastLevelOnly: [false], + levels: this.fb.array([], [this.levelsRequired()]) }), - perimeterKeyName: ['', [Validators.pattern(oneSpaceInsideRegex)]], + perimeterKeyName: ['', [Validators.required, Validators.pattern(oneSpaceInsideRegex)]], reportStrategy: [GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS], createRelationsWithMatchedZones: [false], direction: [EntitySearchDirection.TO], @@ -97,6 +104,8 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit readonly GeofencingReportStrategyTranslations = GeofencingReportStrategyTranslations; readonly GeofencingDirectionList = Object.values(EntitySearchDirection) as Array; readonly GeofencingDirectionTranslations = GeofencingDirectionTranslations; + readonly GeofencingDirectionLevelTranslations = GeofencingDirectionLevelTranslations; + readonly AttributeScope = AttributeScope; private currentEntityFilter: EntityFilter; @@ -107,7 +116,6 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit private store: Store ) { - this.observeMaxLevelChanges(); this.observeEntityFilterChanges(); this.observeEntityTypeChanges(); this.observeUpdatePosition(); @@ -131,7 +139,16 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit if (this.zone.refDynamicSourceConfiguration?.type) { this.refEntityIdFormGroup.get('entityType').setValue(this.zone.refDynamicSourceConfiguration.type, {emitEvent: false}); } - this.validateFetchLastLevelOnly(this.zone?.refDynamicSourceConfiguration?.maxLevel); + if (this.zone?.refDynamicSourceConfiguration?.levels?.length > 0) { + this.zone.refDynamicSourceConfiguration.levels.forEach(level => { + this.levelsFormArray().push(this.fb.group({ + direction: [level.direction], + relationType: [level.relationType, [Validators.required]] + })); + }) + } else { + this.addKey(); + } this.validateDirectionAndRelationType(this.zone?.createRelationsWithMatchedZones); this.validateRefDynamicSourceConfiguration(this.zone?.refEntityId?.entityType || this.zone?.refDynamicSourceConfiguration?.type); @@ -241,17 +258,19 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit private observeEntityFilterChanges(): void { merge( this.refEntityIdFormGroup.get('entityType').valueChanges, - this.refEntityIdFormGroup.get('id').valueChanges.pipe(filter(Boolean)), + this.refEntityIdFormGroup.get('id').valueChanges, ) .pipe(debounceTime(50), takeUntilDestroyed()) .subscribe(() => this.updateEntityFilter(this.entityType)); + + this.refEntityIdFormGroup.get('id').valueChanges.pipe(distinctUntilChanged(), takeUntilDestroyed()).subscribe(() => this.geofencingFormGroup.get('perimeterKeyName').reset('')); } private observeEntityTypeChanges(): void { this.refEntityIdFormGroup.get('entityType').valueChanges .pipe(distinctUntilChanged(), takeUntilDestroyed()) .subscribe(type => { - this.geofencingFormGroup.get('refEntityId').get('id').setValue(''); + this.geofencingFormGroup.get('refEntityId').get('id').setValue(null); const isEntityWithId = type !== ArgumentEntityType.Tenant && type !== ArgumentEntityType.Current && type !== ArgumentEntityType.RelationQuery; this.geofencingFormGroup.get('refEntityId') .get('id')[isEntityWithId ? 'enable' : 'disable'](); @@ -271,6 +290,12 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit }; } + private levelsRequired(): ValidatorFn { + return (control: FormControl) => { + return control.value.length ? null : { levelsRequired: true }; + }; + } + private forbiddenNameValidator(): ValidatorFn { return (control: FormControl) => { const trimmedValue = control.value.trim().toLowerCase(); @@ -282,10 +307,40 @@ export class CalculatedFieldGeofencingZoneGroupsPanelComponent implements OnInit private observeUpdatePosition(): void { merge( this.refEntityIdFormGroup.get('entityType').valueChanges, + this.refEntityIdFormGroup.get('id').valueChanges, this.geofencingFormGroup.get('createRelationsWithMatchedZones').valueChanges ) .pipe(delay(50), takeUntilDestroyed()) .subscribe(() => this.popover.updatePosition()); } + levelsFormArray(): UntypedFormArray { + return this.refDynamicSourceFormGroup.get('levels') as UntypedFormArray; + } + + trackByKey(index: number, keyControl: AbstractControl): any { + return keyControl; + } + + removeKey(index: number) { + this.levelsFormArray().removeAt(index); + } + + addKey() { + this.levelsFormArray().push(this.fb.group({ + direction: [EntitySearchDirection.TO], + relationType: ['', [Validators.required]] + })); + } + + keyDrop(event: CdkDragDrop) { + const keysArray = this.levelsFormArray(); + const key = keysArray.at(event.previousIndex); + keysArray.removeAt(event.previousIndex); + keysArray.insert(event.currentIndex, key); + } + + get dragEnabled(): boolean { + return this.levelsFormArray().controls.length > 1; + } } diff --git a/ui-ngx/src/app/shared/models/calculated-field.models.ts b/ui-ngx/src/app/shared/models/calculated-field.models.ts index a86943c86e..841baea168 100644 --- a/ui-ngx/src/app/shared/models/calculated-field.models.ts +++ b/ui-ngx/src/app/shared/models/calculated-field.models.ts @@ -73,7 +73,7 @@ export enum ArgumentEntityType { Asset = 'ASSET', Customer = 'CUSTOMER', Tenant = 'TENANT', - RelationQuery = 'RELATION_QUERY', + RelationQuery = 'RELATION_PATH_QUERY', } export const ArgumentEntityTypeTranslations = new Map( @@ -108,6 +108,13 @@ export const GeofencingDirectionTranslations = new Map( + [ + [EntitySearchDirection.FROM, 'calculated-fields.direction-down'], + [EntitySearchDirection.TO, 'calculated-fields.direction-up'], + ] +) + export enum ArgumentType { Attribute = 'ATTRIBUTE', LatestTelemetry = 'TS_LATEST', @@ -167,10 +174,7 @@ export interface CalculatedFieldGeofencing { export interface RefDynamicSourceConfiguration { type?: ArgumentEntityType.RelationQuery; - direction: EntitySearchDirection; - relationType: string; - maxLevel: number; - fetchLastLevelOnly?: boolean; + levels?: Array<{direction: EntitySearchDirection; relationType: string;}>; } export interface CalculatedFieldGeofencingValue extends CalculatedFieldGeofencing { diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 1866ab7d0b..c56d08701c 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -1122,17 +1122,28 @@ "report-presence-status-only": "Presence status only", "report-transition-event-and-presence": "Presence status and transition events", "perimeter-attribute-key": "Perimeter attribute key", - "relation-query": "Relations query", - "direction": "Direction", - "direction-from": "From source entity", - "direction-to": "To source entity", + "perimeter-attribute-key-required": "Perimeter attribute key is required.", + "perimeter-attribute-key-pattern": "Perimeter attribute key is invalid.", + "entity-zone-relationship": "Path from Entity to Zones *", + "direction": "Relation direction", + "direction-from": "From entity to zone", + "direction-to": "From zone to entity", "relation-type": "Relation type", - "create-relation-with-matched-zones": "Create relations with matched zones", + "create-relation-with-matched-zones": "Create relations for source entity with matched zones", "relation-level": "Relation level", "fetch-last-available-level": "Fetch last available level only", "zone-group-refresh-interval": "Zone groups refresh interval", "copy-zone-group-name": "Copy zone group name", "open-details-page": "Open entity details page", + "level": "Level", + "direction-level": "Direction", + "direction-up": "Up", + "direction-down": "Down", + "add-level": "Add level", + "delete-level": "Delete level", + "no-level": "No level configured", + "levels-required": "At least one level must be configured.", + "max-allowed-levels-error": "Relation level exceeds the maximum allowed.", "hint": { "arguments-simple-with-rolling": "Simple type calculated field should not contain keys with time series rolling type.", "arguments-empty": "Arguments should not be empty.", @@ -1158,7 +1169,7 @@ "entity-coordinates": "Specify the time series keys that provide entity GPS coordinates (latitude and longitude).", "geofencing-zone-groups": "Define one or more geofencing zones groups to check (e.g. 'allowedZones', 'restrictedZones'). Each group must have a unique name, which is used as a prefix for calculated field output telemetry keys.", "perimeter-attribute-key": "Set the attribute key that contains the geofencing zone perimeter definition. The perimeter is always taken from server-side attributes of the zone entity.", - "report-strategy": "Presence status reports whether the entity is currently INSIDE or OUTSIDE the zone group.Transition events report when the entity ENTERED or LEFT the zone group.", + "report-strategy": "Presence status reports whether the entity is currently INSIDE or OUTSIDE the zone group. Transition events report when the entity ENTERED or LEFT the zone group.", "create-relation-with-matched-zones": "Automatically create and maintain relations between the entity and the zones it is currently inside. Relations are removed when the entity leaves a zone and created when it enters a new one.", "relation-type-required": "Relation type is required.", "relation-level-required": "Relation level is required.", @@ -1167,9 +1178,9 @@ "geofencing-empty": "At least one zone group must be configured.", "geofencing-entity-not-found": "Geofencing target entity not found.", "max-geofencing-zone": "Maximum number of geofencing zones reached.", - "zone-group-refresh-interval": "Defines how often zone groups configured via related entities are refreshed. Set to 0 to disable scheduled refresh.", + "zone-group-refresh-interval": "Defines how often zone groups configured via related entities are refreshed.", "zone-group-refresh-interval-required": "Zone groups refresh interval is required.", - "zone-group-refresh-interval-min": "Zone group refresh interval is below the minimum allowed system interval." + "zone-group-refresh-interval-min": "Zone group refresh interval should be at least {{ min }} second." } }, "ai-models": { From a813cf403e63e7e271fa3007e485ee1664dbb9cd Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Fri, 3 Oct 2025 18:02:58 +0300 Subject: [PATCH 197/216] UI: Fixed entity key autocomplete on filter change --- .../components/entity/entity-key-autocomplete.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts index a86fc70115..9bc8d4ced8 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-key-autocomplete.component.ts @@ -133,6 +133,7 @@ export class EntityKeyAutocompleteComponent implements ControlValueAccessor, Val if (filterChanged || keyScopeChanged || keyTypeChanged) { this.keyControl.setValue('', {emitEvent: false}); + this.cachedResult = null; } } From 7609722b308084149e013b1c2790c95715afdf93 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Mon, 6 Oct 2025 16:46:54 +0300 Subject: [PATCH 198/216] UI: Move drag icon button to first column of table --- ...-geofencing-zone-groups-table.component.ts | 2 +- ...eofencing-zone-groups-panel.component.html | 23 +++++++++++-------- ...eofencing-zone-groups-panel.component.scss | 18 +++++++++++---- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.ts b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.ts index 9d2a124d68..a11ae4cea5 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.ts +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/geofencing-zone-grups-table/calculated-field-geofencing-zone-groups-table.component.ts @@ -168,7 +168,7 @@ export class CalculatedFieldGeofencingZoneGroupsTableComponent implements Contro renderer: this.renderer, componentType: CalculatedFieldGeofencingZoneGroupsPanelComponent, hostView: this.viewContainerRef, - preferredPlacement: 'right', + preferredPlacement: isExists ? ['left', 'leftTop', 'leftBottom'] : ['topRight', 'right', 'rightTop'], context: ctx, isModal: true }); diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.html b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.html index 1c57cfbc02..a460deaf4e 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.html @@ -97,7 +97,8 @@ {{ 'calculated-fields.entity-zone-relationship' | translate }}
-
+
+
calculated-fields.level
calculated-fields.direction-level
calculated-fields.relation-type
@@ -110,6 +111,17 @@ (cdkDropListDropped)="keyDrop($event)"> @for (keyControl of levelsFormArray().controls; track trackByKey;) {
+
+ +
{{ $index+1 }}
@@ -137,15 +149,6 @@ matTooltipPosition="above"> delete -
} diff --git a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.scss b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.scss index ff6140f12c..ac5dc70ef9 100644 --- a/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.scss +++ b/ui-ngx/src/app/modules/home/components/calculated-fields/components/panel/calculated-field-geofencing-zone-groups-panel.component.scss @@ -30,6 +30,8 @@ $panel-width: 520px; } .level-text { + display: flex; + justify-content: center; width: 25px; color: rgba(0, 0, 0, 0.54); } @@ -38,14 +40,20 @@ $panel-width: 520px; .tb-form-row { gap: 12px; } + .tb-form-table-body { gap: unset; } - } - .tb-form-table-header-cell { - &.tb-actions-header { - width: 80px; - min-width: 80px; + + .tb-form-table-header { + padding: 0; + } + + .tb-form-table-header-cell { + &.tb-actions-header { + width: 40px; + min-width: 40px; + } } } From 14efeecdc33949472e9878733bb4522b778656d0 Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 6 Oct 2025 16:36:30 +0300 Subject: [PATCH 199/216] Update rule node doc links --- .../engine/action/TbAssignToCustomerNode.java | 5 +- .../rule/engine/action/TbClearAlarmNode.java | 6 +- .../TbCopyAttributesToEntityViewNode.java | 35 ++--- .../rule/engine/action/TbCreateAlarmNode.java | 21 ++- .../engine/action/TbCreateRelationNode.java | 5 +- .../engine/action/TbDeleteRelationNode.java | 6 +- .../rule/engine/action/TbDeviceStateNode.java | 3 +- .../rule/engine/action/TbLogNode.java | 16 +- .../rule/engine/action/TbMsgCountNode.java | 6 +- .../TbSaveToCustomCassandraTableNode.java | 7 +- .../action/TbUnassignFromCustomerNode.java | 3 +- .../thingsboard/rule/engine/ai/TbAiNode.java | 4 +- .../engine/aws/lambda/TbAwsLambdaNode.java | 4 +- .../rule/engine/aws/sns/TbSnsNode.java | 4 +- .../rule/engine/aws/sqs/TbSqsNode.java | 4 +- .../rule/engine/debug/TbMsgGeneratorNode.java | 4 +- .../deduplication/TbMsgDeduplicationNode.java | 5 +- .../rule/engine/delay/TbMsgDelayNode.java | 6 +- .../engine/edge/TbMsgPushToCloudNode.java | 8 +- .../rule/engine/edge/TbMsgPushToEdgeNode.java | 6 +- .../engine/filter/TbAssetTypeSwitchNode.java | 6 +- .../engine/filter/TbCheckAlarmStatusNode.java | 4 +- .../engine/filter/TbCheckMessageNode.java | 8 +- .../engine/filter/TbCheckRelationNode.java | 9 +- .../engine/filter/TbDeviceTypeSwitchNode.java | 6 +- .../rule/engine/filter/TbJsFilterNode.java | 12 +- .../rule/engine/filter/TbJsSwitchNode.java | 13 +- .../engine/filter/TbMsgTypeFilterNode.java | 13 +- .../engine/filter/TbMsgTypeSwitchNode.java | 14 +- .../filter/TbOriginatorTypeFilterNode.java | 10 +- .../filter/TbOriginatorTypeSwitchNode.java | 6 +- .../rule/engine/flow/TbAckNode.java | 13 +- .../rule/engine/flow/TbCheckpointNode.java | 7 +- .../engine/flow/TbRuleChainInputNode.java | 6 +- .../engine/flow/TbRuleChainOutputNode.java | 9 +- .../rule/engine/gcp/pubsub/TbPubSubNode.java | 4 +- .../engine/geo/TbGpsGeofencingActionNode.java | 14 +- .../engine/geo/TbGpsGeofencingFilterNode.java | 10 +- .../rule/engine/kafka/TbKafkaNode.java | 3 +- .../rule/engine/mail/TbMsgToEmailNode.java | 22 +-- .../rule/engine/mail/TbSendEmailNode.java | 6 +- .../rule/engine/math/TbMathNode.java | 7 +- .../engine/metadata/CalculateDeltaNode.java | 12 +- .../TbFetchDeviceCredentialsNode.java | 6 +- .../engine/metadata/TbGetAttributesNode.java | 12 +- .../metadata/TbGetCustomerAttributeNode.java | 6 +- .../metadata/TbGetCustomerDetailsNode.java | 9 +- .../engine/metadata/TbGetDeviceAttrNode.java | 9 +- .../metadata/TbGetOriginatorFieldsNode.java | 12 +- .../metadata/TbGetRelatedAttributeNode.java | 6 +- .../engine/metadata/TbGetTelemetryNode.java | 12 +- .../metadata/TbGetTenantAttributeNode.java | 6 +- .../metadata/TbGetTenantDetailsNode.java | 9 +- .../rule/engine/mqtt/TbMqttNode.java | 5 +- .../engine/mqtt/azure/TbAzureIotHubNode.java | 5 +- .../notification/TbNotificationNode.java | 5 +- .../rule/engine/notification/TbSlackNode.java | 5 +- .../engine/profile/TbDeviceProfileNode.java | 7 +- .../rule/engine/rabbitmq/TbRabbitMqNode.java | 35 ++--- .../rule/engine/rest/TbRestApiCallNode.java | 5 +- .../rest/TbSendRestApiCallReplyNode.java | 8 +- .../rule/engine/rpc/TbSendRPCReplyNode.java | 13 +- .../rule/engine/rpc/TbSendRPCRequestNode.java | 11 +- .../rule/engine/sms/TbSendSmsNode.java | 5 +- .../telemetry/TbCalculatedFieldsNode.java | 13 +- .../engine/telemetry/TbMsgAttributesNode.java | 5 +- .../telemetry/TbMsgDeleteAttributesNode.java | 9 +- .../engine/telemetry/TbMsgTimeseriesNode.java | 5 +- .../transform/TbChangeOriginatorNode.java | 3 +- .../rule/engine/transform/TbCopyKeysNode.java | 10 +- .../engine/transform/TbDeleteKeysNode.java | 12 +- .../rule/engine/transform/TbJsonPathNode.java | 21 ++- .../engine/transform/TbRenameKeysNode.java | 10 +- .../engine/transform/TbSplitArrayMsgNode.java | 13 +- .../engine/transform/TbTransformMsgNode.java | 4 +- .../geo/GpsGeofencingActionTestCase.java | 9 +- ui-ngx/src/app/shared/models/constants.ts | 145 +++++++++--------- .../src/app/shared/models/rule-node.models.ts | 7 + 78 files changed, 390 insertions(+), 439 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNode.java index 35c3ce7683..6f18e7c1dd 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAssignToCustomerNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.action; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -32,7 +31,6 @@ import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "assign to customer", @@ -42,7 +40,8 @@ import org.thingsboard.server.common.msg.TbMsg; "Rule node will create a new customer if it doesn't exist, and 'Create new customer if it doesn't exist' enabled.", configDirective = "tbActionNodeAssignToCustomerConfig", icon = "add_circle", - version = 1 + version = 1, + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/assign-to-customer/" ) public class TbAssignToCustomerNode extends TbAbstractCustomerActionNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java index 6a806e7bc1..4ef3d6d589 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.action; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -31,7 +30,6 @@ import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "clear alarm", relationTypes = {"Cleared", "False"}, @@ -44,7 +42,8 @@ import org.thingsboard.server.common.msg.TbMsg; "Message payload can be accessed via msg property. For example 'temperature = ' + msg.temperature ;. " + "Message metadata can be accessed via metadata property. For example 'name = ' + metadata.customerName;.", configDirective = "tbActionNodeClearAlarmConfig", - icon = "notifications_off" + icon = "notifications_off", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/clear-alarm/" ) public class TbClearAlarmNode extends TbAbstractAlarmNode { @@ -79,4 +78,5 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode getFutureCallback(TbContext ctx, TbMsg msg, EntityView entityView) { - return new FutureCallback() { + return new FutureCallback<>() { @Override public void onSuccess(@Nullable Void result) { transformAndTellNext(ctx, msg, entityView); } @Override - public void onFailure(Throwable t) { + public void onFailure(@NonNull Throwable t) { ctx.tellFailure(msg, t); } }; @@ -157,18 +151,11 @@ public class TbCopyAttributesToEntityViewNode implements TbNode { private boolean attributeContainsInEntityView(AttributeScope scope, String attrKey, EntityView entityView) { AttributesEntityView attributesEntityView = entityView.getKeys().getAttributes(); - List keys = null; - switch (scope) { - case CLIENT_SCOPE: - keys = attributesEntityView.getCs(); - break; - case SERVER_SCOPE: - keys = attributesEntityView.getSs(); - break; - case SHARED_SCOPE: - keys = attributesEntityView.getSh(); - break; - } + List keys = switch (scope) { + case CLIENT_SCOPE -> attributesEntityView.getCs(); + case SERVER_SCOPE -> attributesEntityView.getSs(); + case SHARED_SCOPE -> attributesEntityView.getSh(); + }; return CollectionsUtil.contains(keys, attrKey); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java index bd576be936..600c276901 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.EnumUtils; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; @@ -39,7 +38,6 @@ import org.thingsboard.server.common.msg.TbMsg; import java.io.IOException; import java.util.List; -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "create alarm", relationTypes = {"Created", "Updated", "False"}, @@ -52,7 +50,8 @@ import java.util.List; "Message payload can be accessed via msg property. For example 'temperature = ' + msg.temperature ;. " + "Message metadata can be accessed via metadata property. For example 'name = ' + metadata.customerName;.", configDirective = "tbActionNodeCreateAlarmConfig", - icon = "notifications_active" + icon = "notifications_active", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/create-alarm/" ) public class TbCreateAlarmNode extends TbAbstractAlarmNode { @@ -62,10 +61,10 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNodeSuccess - if the relation already exists or successfully created, otherwise Failure.", configDirective = "tbActionNodeCreateRelationConfig", icon = "add_circle", - version = 1 + version = 1, + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/create-relation/" ) public class TbCreateRelationNode extends TbAbstractRelationActionNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNode.java index 82c3a0fab5..6a643aee75 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeleteRelationNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.action; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -32,8 +31,6 @@ import org.thingsboard.server.common.msg.TbMsg; import static org.thingsboard.common.util.DonAsynchron.withCallback; - -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "delete relation", @@ -55,7 +52,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; "Output connections: Success - If the relation(s) successfully deleted, otherwise Failure.", configDirective = "tbActionNodeDeleteRelationConfig", icon = "remove_circle", - version = 1 + version = 1, + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/delete-relation/" ) public class TbDeleteRelationNode extends TbAbstractRelationActionNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java index c9a50cf88d..caff3fc832 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbDeviceStateNode.java @@ -58,7 +58,8 @@ import java.util.Set; "This node is particularly useful when device isn't using transports to receive data, such as when fetching data from external API or computing new data within the rule chain.", configClazz = TbDeviceStateNodeConfiguration.class, relationTypes = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE, "Rate limited"}, - configDirective = "tbActionNodeDeviceStateConfig" + configDirective = "tbActionNodeDeviceStateConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/device-state/" ) public class TbDeviceStateNode implements TbNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java index 47ecac154f..d4a598cf1b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbLogNode.java @@ -19,6 +19,7 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; @@ -44,19 +45,19 @@ import java.util.Objects; "Message payload can be accessed via msg property. For example 'temperature = ' + msg.temperature ;. " + "Message metadata can be accessed via metadata property. For example 'name = ' + metadata.customerName;.", configDirective = "tbActionNodeLogConfig", - icon = "menu" + icon = "menu", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/log/" ) public class TbLogNode implements TbNode { - private TbLogNodeConfiguration config; private ScriptEngine scriptEngine; private boolean standard; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbLogNodeConfiguration.class); - this.standard = isStandard(config); - this.scriptEngine = this.standard ? null : createScriptEngine(ctx, config); + var config = TbNodeUtils.convert(configuration, TbLogNodeConfiguration.class); + standard = isStandard(config); + scriptEngine = standard ? null : createScriptEngine(ctx, config); } ScriptEngine createScriptEngine(TbContext ctx, TbLogNodeConfiguration config) { @@ -75,7 +76,7 @@ public class TbLogNode implements TbNode { return; } - Futures.addCallback(scriptEngine.executeToStringAsync(msg), new FutureCallback() { + Futures.addCallback(scriptEngine.executeToStringAsync(msg), new FutureCallback<>() { @Override public void onSuccess(@Nullable String result) { log.info(result); @@ -83,7 +84,7 @@ public class TbLogNode implements TbNode { } @Override - public void onFailure(Throwable t) { + public void onFailure(@NonNull Throwable t) { ctx.tellFailure(msg, t); } }, MoreExecutors.directExecutor()); //usually js responses runs on js callback executor @@ -120,4 +121,5 @@ public class TbLogNode implements TbNode { scriptEngine.destroy(); } } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java index 3d93bea8a2..4488a95e31 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbMsgCountNode.java @@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.action; import com.google.gson.Gson; import com.google.gson.JsonObject; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; @@ -34,7 +33,6 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "message count", @@ -42,7 +40,8 @@ import java.util.concurrent.atomic.AtomicLong; nodeDescription = "Count incoming messages", nodeDetails = "Count incoming messages for specified interval and produces POST_TELEMETRY_REQUEST msg with messages count", icon = "functions", - configDirective = "tbActionNodeMsgCountConfig" + configDirective = "tbActionNodeMsgCountConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/message-count/" ) public class TbMsgCountNode implements TbNode { @@ -59,7 +58,6 @@ public class TbMsgCountNode implements TbNode { this.delay = TimeUnit.SECONDS.toMillis(config.getInterval()); this.telemetryPrefix = config.getTelemetryPrefix(); scheduleTickMsg(ctx, null); - } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java index e56a3459f8..329e0e154b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbSaveToCustomCassandraTableNode.java @@ -57,7 +57,8 @@ import java.util.concurrent.atomic.AtomicInteger; import static org.thingsboard.common.util.DonAsynchron.withCallback; @Slf4j -@RuleNode(type = ComponentType.ACTION, +@RuleNode( + type = ComponentType.ACTION, name = "save to custom table", configClazz = TbSaveToCustomCassandraTableNodeConfiguration.class, version = 1, @@ -71,7 +72,9 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; " otherwise, the message will be routed via success chain.", configDirective = "tbActionNodeCustomTableConfig", icon = "file_upload", - ruleChainTypes = RuleChainType.CORE) + ruleChainTypes = RuleChainType.CORE, + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/save-to-custom-table/" +) public class TbSaveToCustomCassandraTableNode implements TbNode { private static final String TABLE_PREFIX = "cs_tb_"; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNode.java index e889e3a970..0ff79dee39 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbUnassignFromCustomerNode.java @@ -44,7 +44,8 @@ import org.thingsboard.server.common.msg.TbMsg; "Other entities can be assigned only to one customer, so specified customer title in the configuration will be ignored if the originator isn't a dashboard.", configDirective = "tbActionNodeUnAssignToCustomerConfig", icon = "remove_circle", - version = 1 + version = 1, + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/unassign-from-customer/" ) public class TbUnassignFromCustomerNode extends TbAbstractCustomerActionNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index bd02089204..054b06fb54 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -92,8 +92,8 @@ import static org.thingsboard.server.dao.service.ConstraintValidator.validateFie configClazz = TbAiNodeConfiguration.class, configDirective = "tbExternalNodeAiConfig", iconUrl = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDkiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OSA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0zOC42MzExIDE3LjA3OTVDNDAuMTcwNSAxNy4wNzk2IDQxLjY1MTggMTcuNjg3MiA0Mi43NDc4IDE4Ljc3NjNDNDMuODQ0OCAxOS44NjYzIDQ0LjQ2NTkgMjEuMzUwMSA0NC40NjU5IDIyLjkwMjlWMzUuNDY1MkM0NC40NjU5IDM2LjM1MDkgNDQuMzU2NyAzNy4wNzY5IDQ0LjA5NzMgMzcuNzUxN0M0My44NDE0IDM4LjQxNjcgNDMuNDY1MSAzOC45NjE0IDQzLjA0NDggMzkuNTAyOEM0Mi40NjY3IDQwLjI0NzIgNDEuNjU2MyA0MC42ODU5IDQwLjg5MTkgNDAuOTM4OEM0MC4xMjExIDQxLjE5MzcgMzkuMzE0MyA0MS4yODg1IDM4LjYzMTEgNDEuMjg4NUgzMS4wMjU5TDIzLjM4MTIgNDUuODQ2NEMyMy4wNDMxIDQ2LjA0NzggMjIuNjI0MSA0Ni4wNTA3IDIyLjI4MzkgNDUuODUyOUMyMS45NDM3IDQ1LjY1NDcgMjEuNzMzOCA0NS4yODU5IDIxLjczMzcgNDQuODg3MlY0MS4yODg1SDE5LjY2NjNDMTguMTI2OSA0MS4yODg0IDE2LjY0NTUgNDAuNjgwOSAxNS41NDk2IDM5LjU5MThDMTQuNDUyNyAzOC41MDE5IDEzLjgzMTUgMzcuMDE3OSAxMy44MzE1IDM1LjQ2NTJWMjIuOTAyOUMxMy44MzE1IDIyLjMyMDIgMTMuOTE4NSAyMS43NDY4IDE0LjA4NTggMjEuMjAwN0wxNi4yODg5IDIxLjgxMDFMMTcuMjA5OSAyNS4yNTAyQzE3Ljk0MTYgMjcuOTg0NSAyMS43NTYyIDI3Ljk4NDQgMjIuNDg4IDI1LjI1MDJMMjMuNDA3OSAyMS44MTAxTDI2Ljc5MTcgMjAuODc0OUMyOC41NzkxIDIwLjM4MDUgMjkuMTc3IDE4LjUwMjYgMjguNTg4OCAxNy4wNzk1SDM4LjYzMTFaTTIyLjU4NDIgMzEuNTM5NUMyMS45OCAzMS41Mzk3IDIxLjQ5MDEgMzIuMDM3NiAyMS40OTAxIDMyLjY1MTlDMjEuNDkwMiAzMy4yNjYgMjEuOTgwMSAzMy43NjQgMjIuNTg0MiAzMy43NjQySDM0LjYxOTFDMzUuMjIzMyAzMy43NjQyIDM1LjcxMzEgMzMuMjY2MSAzNS43MTMyIDMyLjY1MTlDMzUuNzEzMiAzMi4wMzc1IDM1LjIyMzQgMzEuNTM5NSAzNC42MTkxIDMxLjUzOTVIMjIuNTg0MlpNMjQuNzcyMyAyNC44NjU3QzI0LjE2ODIgMjQuODY1OCAyMy42NzgzIDI1LjM2MzggMjMuNjc4MyAyNS45NzhDMjMuNjc4NCAyNi41OTIyIDI0LjE2ODMgMjcuMDkwMiAyNC43NzIzIDI3LjA5MDNIMzcuOTAxNEMzOC41MDU1IDI3LjA5MDMgMzguOTk1MyAyNi41OTIyIDM4Ljk5NTQgMjUuOTc4QzM4Ljk5NTQgMjUuMzYzNyAzOC41MDU2IDI0Ljg2NTcgMzcuOTAxNCAyNC44NjU3SDI0Ljc3MjNaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjc2Ii8+CjxwYXRoIGQ9Ik0xOC43ODkxIDExLjI5NzVDMTkuMDY5MSAxMC4xODA4IDIwLjYyOTkgMTAuMTgwOCAyMC45MDk5IDExLjI5NzVMMjEuOTE0MyAxNS4zMDM2QzIyLjAxMTYgMTUuNjkxOCAyMi4zMDY1IDE1Ljk5NzggMjIuNjg2NyAxNi4xMDNMMjYuMzYxMSAxNy4xMTg3QzI3LjQzNyAxNy40MTYyIDI3LjQzNyAxOC45Njc2IDI2LjM2MTEgMTkuMjY1MUwyMi42NzYxIDIwLjI4NEMyMi4zMDE4IDIwLjM4NzQgMjIuMDA4NyAyMC42ODQ1IDIxLjkwNjggMjEuMDY1TDIwLjkwNDYgMjQuODEyNUMyMC42MTE3IDI1LjkwNTggMTkuMDg2MSAyNS45MDU5IDE4Ljc5MzMgMjQuODEyNUwxNy43OTExIDIxLjA2NUMxNy42ODkzIDIwLjY4NDcgMTcuMzk3IDIwLjM4NzUgMTcuMDIyOSAyMC4yODRMMTMuMzM2OCAxOS4yNjUxQzEyLjI2MTQgMTguOTY3MyAxMi4yNjE1IDE3LjQxNjUgMTMuMzM2OCAxNy4xMTg3TDE3LjAxMTIgMTYuMTAzQzE3LjM5MTYgMTUuOTk3OCAxNy42ODc0IDE1LjY5MTkgMTcuNzg0NyAxNS4zMDM2TDE4Ljc4OTEgMTEuMjk3NVoiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNzYiLz4KPHBhdGggZD0iTTEwLjAzNDMgNy4wMjQyNUMxMC4zMDY4IDUuODk0NDQgMTEuODg2OCA1Ljg5NDQ0IDEyLjE1OTQgNy4wMjQyNUwxMi42OTg5IDkuMjYyOThDMTIuNzkyNyA5LjY1MTc0IDEzLjA4NTEgOS45NTg4NyAxMy40NjQgMTAuMDY3OUwxNS41NzczIDEwLjY3NTFDMTYuNjM5MyAxMC45ODAzIDE2LjYzOTMgMTIuNTEwOSAxNS41NzczIDEyLjgxNjFMMTMuNDUzMyAxMy40MjY1QzEzLjA4MDIgMTMuNTMzOCAxMi43OTA4IDEzLjgzMzkgMTIuNjkyNSAxNC4yMTUxTDEyLjE1NTEgMTYuMzA0QzExLjg3IDE3LjQxMTYgMTAuMzIzNiAxNy40MTE2IDEwLjAzODUgMTYuMzA0TDkuNTAwMDMgMTQuMjE1MUM5LjQwMTczIDEzLjgzMzkgOS4xMTIzNSAxMy41MzM3IDguNzM5MyAxMy40MjY1TDYuNjE1MjQgMTIuODE2MUM1LjU1Mzc4IDEyLjUxMDYgNS41NTM2NCAxMC45ODA0IDYuNjE1MjQgMTAuNjc1MUw4LjcyODYyIDEwLjA2NzlDOS4xMDc2IDkuOTU4OTggOS4zOTk3OCA5LjY1MTg0IDkuNDkzNjIgOS4yNjI5OEwxMC4wMzQzIDcuMDI0MjVaIiBmaWxsPSJibGFjayIgZmlsbC1vcGFjaXR5PSIwLjc2Ii8+CjxwYXRoIGQ9Ik0yNS45MDI4IDYuNzMzMTNDMjYuMTg3OCA1LjYyNTQxIDI3LjczNDMgNS42MjU0MSAyOC4wMTkzIDYuNzMzMTNMMjguMjAzMSA3LjQ0Njc5QzI4LjMwMyA3LjgzNDMxIDI4LjYwMDEgOC4xMzcwNSAyOC45ODA5IDguMjM5NzVMMjkuNTM0NCA4LjM4OTY1QzMwLjYxOTIgOC42ODIxMiAzMC42MTkzIDEwLjI0NjkgMjkuNTM0NCAxMC41MzkzTDI4Ljk2OTIgMTAuNjkxNEMyOC41OTQ0IDEwLjc5MjUgMjguMjk5OSAxMS4wODgzIDI4LjE5NTYgMTEuNDY4TDI4LjAxNTEgMTIuMTI4NUMyNy43MTc0IDEzLjIxMjggMjYuMjA0NyAxMy4yMTI4IDI1LjkwNyAxMi4xMjg1TDI1LjcyNTQgMTEuNDY4QzI1LjYyMTEgMTEuMDg4MiAyNS4zMjY4IDEwLjc5MjQgMjQuOTUxOCAxMC42OTE0TDI0LjM4NzcgMTAuNTM5M0MyMy4zMDI2IDEwLjI0NyAyMy4zMDI2IDguNjgxOTggMjQuMzg3NyA4LjM4OTY1TDI0Ljk0MDEgOC4yMzk3NUMyNS4zMjExIDguMTM3MDkgMjUuNjE5MSA3LjgzNDQ2IDI1LjcxOSA3LjQ0Njc5TDI1LjkwMjggNi43MzMxM1oiIGZpbGw9ImJsYWNrIiBmaWxsLW9wYWNpdHk9IjAuNzYiLz4KPC9zdmc+Cg==", - docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/external-nodes/#ai-request-node", - ruleChainTypes = RuleChainType.CORE + ruleChainTypes = RuleChainType.CORE, + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/ai-request/" ) public final class TbAiNode extends TbAbstractExternalNode implements TbNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java index 82aa667319..c4e448e2ee 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/lambda/TbAwsLambdaNode.java @@ -54,7 +54,8 @@ import static org.thingsboard.server.dao.service.ConstraintValidator.validateFie "The node uses a pre-configured client and specified function to run.

" + "Output connections: Success, Failure.", configDirective = "tbExternalNodeLambdaConfig", - iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+" + iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/aws-lambda/" ) public class TbAwsLambdaNode extends TbAbstractExternalNode { @@ -156,4 +157,5 @@ public class TbAwsLambdaNode extends TbAbstractExternalNode { } } } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java index a13478c717..1877aff733 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sns/TbSnsNode.java @@ -47,7 +47,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; "(messageId, requestId) in the Message Metadata from the AWS SNS. " + "For example requestId field can be accessed with metadata.requestId.", configDirective = "tbExternalNodeSnsConfig", - iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+" + iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/aws-sns/" ) public class TbSnsNode extends TbAbstractExternalNode { @@ -125,4 +126,5 @@ public class TbSnsNode extends TbAbstractExternalNode { } } } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java index 5d538df932..2183f20c1c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/aws/sqs/TbSqsNode.java @@ -53,7 +53,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; ", sequenceNumber) in the Message Metadata from the AWS SQS." + " For example requestId field can be accessed with metadata.requestId.", configDirective = "tbExternalNodeSqsConfig", - iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+" + iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4Ij48cGF0aCBkPSJNMTMuMjMgMTAuNTZWMTBjLTEuOTQgMC0zLjk5LjM5LTMuOTkgMi42NyAwIDEuMTYuNjEgMS45NSAxLjYzIDEuOTUuNzYgMCAxLjQzLS40NyAxLjg2LTEuMjIuNTItLjkzLjUtMS44LjUtMi44NG0yLjcgNi41M2MtLjE4LjE2LS40My4xNy0uNjMuMDYtLjg5LS43NC0xLjA1LTEuMDgtMS41NC0xLjc5LTEuNDcgMS41LTIuNTEgMS45NS00LjQyIDEuOTUtMi4yNSAwLTQuMDEtMS4zOS00LjAxLTQuMTcgMC0yLjE4IDEuMTctMy42NCAyLjg2LTQuMzggMS40Ni0uNjQgMy40OS0uNzYgNS4wNC0uOTNWNy41YzAtLjY2LjA1LTEuNDEtLjMzLTEuOTYtLjMyLS40OS0uOTUtLjctMS41LS43LTEuMDIgMC0xLjkzLjUzLTIuMTUgMS42MS0uMDUuMjQtLjI1LjQ4LS40Ny40OWwtMi42LS4yOGMtLjIyLS4wNS0uNDYtLjIyLS40LS41Ni42LTMuMTUgMy40NS00LjEgNi00LjEgMS4zIDAgMyAuMzUgNC4wMyAxLjMzQzE3LjExIDQuNTUgMTcgNi4xOCAxNyA3Ljk1djQuMTdjMCAxLjI1LjUgMS44MSAxIDIuNDguMTcuMjUuMjEuNTQgMCAuNzFsLTIuMDYgMS43OGgtLjAxIj48L3BhdGg+PHBhdGggZD0iTTIwLjE2IDE5LjU0QzE4IDIxLjE0IDE0LjgyIDIyIDEyLjEgMjJjLTMuODEgMC03LjI1LTEuNDEtOS44NS0zLjc2LS4yLS4xOC0uMDItLjQzLjI1LS4yOSAyLjc4IDEuNjMgNi4yNSAyLjYxIDkuODMgMi42MSAyLjQxIDAgNS4wNy0uNSA3LjUxLTEuNTMuMzctLjE2LjY2LjI0LjMyLjUxIj48L3BhdGg+PHBhdGggZD0iTTIxLjA3IDE4LjVjLS4yOC0uMzYtMS44NS0uMTctMi41Ny0uMDgtLjE5LjAyLS4yMi0uMTYtLjAzLS4zIDEuMjQtLjg4IDMuMjktLjYyIDMuNTMtLjMzLjI0LjMtLjA3IDIuMzUtMS4yNCAzLjMyLS4xOC4xNi0uMzUuMDctLjI2LS4xMS4yNi0uNjcuODUtMi4xNC41Ny0yLjV6Ij48L3BhdGg+PC9zdmc+", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/aws-sqs/" ) public class TbSqsNode extends TbAbstractExternalNode { @@ -156,4 +157,5 @@ public class TbSqsNode extends TbAbstractExternalNode { } } } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java index 90ee0c9048..34514fd644 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNode.java @@ -63,9 +63,9 @@ import static org.thingsboard.server.common.data.DataConstants.QUEUE_NAME; nodeDetails = "Generates messages with configurable period. Javascript function used for message generation.", inEnabled = false, configDirective = "tbActionNodeGeneratorConfig", - icon = "repeat" + icon = "repeat", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/generator/" ) - public class TbMsgGeneratorNode implements TbNode { private static final Set supportedEntityTypes = EnumSet.of(EntityType.DEVICE, EntityType.ASSET, EntityType.ENTITY_VIEW, diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java index ce5fba102a..3a78004820 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java @@ -46,6 +46,7 @@ import java.util.concurrent.TimeUnit; import static org.thingsboard.server.common.data.DataConstants.QUEUE_NAME; +@Slf4j @RuleNode( type = ComponentType.TRANSFORMATION, name = "deduplication", @@ -59,9 +60,9 @@ import static org.thingsboard.server.common.data.DataConstants.QUEUE_NAME; "
  • ALL - return all messages as a single JSON array message. " + "Where each element represents object with msg and metadata inner properties.
  • ", icon = "content_copy", - configDirective = "tbTransformationNodeDeduplicationConfig" + configDirective = "tbTransformationNodeDeduplicationConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/deduplication/" ) -@Slf4j public class TbMsgDeduplicationNode implements TbNode { public static final long TB_MSG_DEDUPLICATION_RETRY_DELAY = 10L; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java index 3507f7c500..4cda58e290 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/delay/TbMsgDelayNode.java @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.delay; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.math.NumberUtils; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -34,7 +33,6 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "delay (deprecated)", @@ -45,7 +43,8 @@ import java.util.concurrent.TimeUnit; "Deprecated because the acknowledged message still stays in memory (to be delayed) and this " + "does not guarantee that message will be processed even if the \"retry failures and timeouts\" processing strategy will be chosen.", icon = "pause", - configDirective = "tbActionNodeMsgDelayConfig" + configDirective = "tbActionNodeMsgDelayConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/delay/" ) public class TbMsgDelayNode implements TbNode { @@ -109,4 +108,5 @@ public class TbMsgDelayNode implements TbNode { public void destroy() { pendingMsgs.clear(); } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToCloudNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToCloudNode.java index 668a8403da..8edb3368c6 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToCloudNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/edge/TbMsgPushToCloudNode.java @@ -16,7 +16,6 @@ package org.thingsboard.rule.engine.edge; import com.fasterxml.jackson.databind.JsonNode; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.server.common.data.EntityType; @@ -28,7 +27,6 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.UUID; -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "push to cloud", @@ -48,7 +46,8 @@ import java.util.UUID; "In case successful storage cloud event to database message will be routed via Success route.", configDirective = "tbActionNodePushToCloudConfig", icon = "cloud_upload", - ruleChainTypes = RuleChainType.EDGE + ruleChainTypes = RuleChainType.EDGE, + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/push-to-cloud/" ) public class TbMsgPushToCloudNode extends AbstractTbMsgPushNode { @@ -80,7 +79,6 @@ public class TbMsgPushToCloudNode extends AbstractTbMsgPushNodeSuccess route.", configDirective = "tbActionNodePushToEdgeConfig", icon = "cloud_download", - ruleChainTypes = RuleChainType.CORE + ruleChainTypes = RuleChainType.CORE, + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/push-to-edge/" ) public class TbMsgPushToEdgeNode extends AbstractTbMsgPushNode { @@ -113,7 +115,7 @@ public class TbMsgPushToEdgeNode extends AbstractTbMsgPushNodeAsset profile name or Failure", - configDirective = "tbNodeEmptyConfig") + configDirective = "tbNodeEmptyConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/asset-profile-switch/" +) public class TbAssetTypeSwitchNode extends TbAbstractTypeSwitchNode { @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNode.java index 9fb60ca671..8f0f35ea46 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckAlarmStatusNode.java @@ -43,7 +43,9 @@ import java.util.Objects; nodeDescription = "Checks alarm status.", nodeDetails = "Checks the alarm status to match one of the specified statuses.

    " + "Output connections: True, False, Failure.", - configDirective = "tbFilterNodeCheckAlarmStatusConfig") + configDirective = "tbFilterNodeCheckAlarmStatusConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/alarm-status-filter/" +) public class TbCheckAlarmStatusNode implements TbNode { private TbCheckAlarmStatusNodeConfig config; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNode.java index d59f2bb0e4..8e23dae658 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckMessageNode.java @@ -16,7 +16,6 @@ package org.thingsboard.rule.engine.filter; import com.google.gson.Gson; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; @@ -30,7 +29,6 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.List; import java.util.Map; -@Slf4j @RuleNode( type = ComponentType.FILTER, name = "check fields presence", @@ -40,7 +38,9 @@ import java.util.Map; nodeDetails = "By default, the rule node checks that all specified fields are present. " + "Uncheck the 'Check that all selected fields are present' if the presence of at least one field is sufficient.

    " + "Output connections: True, False, Failure", - configDirective = "tbFilterNodeCheckMessageConfig") + configDirective = "tbFilterNodeCheckMessageConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/check-fields-presence/" +) public class TbCheckMessageNode implements TbNode { private static final Gson gson = new Gson(); @@ -127,4 +127,4 @@ public class TbCheckMessageNode implements TbNode { return (Map) gson.fromJson(msg.getData(), Map.class); } -} \ No newline at end of file +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java index a5f694373a..6186fe80df 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbCheckRelationNode.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; @@ -41,10 +40,6 @@ import java.util.List; import static org.thingsboard.common.util.DonAsynchron.withCallback; -/** - * Created by ashvayka on 19.01.18. - */ -@Slf4j @RuleNode( type = ComponentType.FILTER, name = "check relation presence", @@ -56,7 +51,9 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; "Otherwise, the rule node checks the presence of a relation to any entity. " + "In both cases, relation lookup is based on configured direction and type.

    " + "Output connections: True, False, Failure", - configDirective = "tbFilterNodeCheckRelationConfig") + configDirective = "tbFilterNodeCheckRelationConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/check-relation-presence/" +) public class TbCheckRelationNode implements TbNode { private TbCheckRelationNodeConfiguration config; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbDeviceTypeSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbDeviceTypeSwitchNode.java index 8f6954956a..d12055c422 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbDeviceTypeSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbDeviceTypeSwitchNode.java @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.filter; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -26,7 +25,6 @@ import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.plugin.ComponentType; -@Slf4j @RuleNode( type = ComponentType.FILTER, name = "device profile switch", @@ -36,7 +34,9 @@ import org.thingsboard.server.common.data.plugin.ComponentType; nodeDescription = "Route incoming messages based on the name of the device profile", nodeDetails = "Route incoming messages based on the name of the device profile. The device profile name is case-sensitive

    " + "Output connections: Device profile name or Failure", - configDirective = "tbNodeEmptyConfig") + configDirective = "tbNodeEmptyConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/device-profile-switch/" +) public class TbDeviceTypeSwitchNode extends TbAbstractTypeSwitchNode { @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java index e36494a2b6..5165eba35f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsFilterNode.java @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.filter; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.ScriptEngine; import org.thingsboard.rule.engine.api.TbContext; @@ -30,7 +29,6 @@ import org.thingsboard.server.common.msg.TbMsg; import static org.thingsboard.common.util.DonAsynchron.withCallback; -@Slf4j @RuleNode( type = ComponentType.FILTER, name = "script", @@ -44,18 +42,17 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; "Message metadata can be accessed via metadata property. For example metadata.customerName === 'John';
    " + "Message type can be accessed via msgType property.

    " + "Output connections: True, False, Failure", - configDirective = "tbFilterNodeScriptConfig" + configDirective = "tbFilterNodeScriptConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/script/" ) public class TbJsFilterNode implements TbNode { - private TbJsFilterNodeConfiguration config; private ScriptEngine scriptEngine; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class); - scriptEngine = ctx.createScriptEngine(config.getScriptLang(), - ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getTbelScript() : config.getJsScript()); + var config = TbNodeUtils.convert(configuration, TbJsFilterNodeConfiguration.class); + scriptEngine = ctx.createScriptEngine(config.getScriptLang(), ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getTbelScript() : config.getJsScript()); } @Override @@ -75,4 +72,5 @@ public class TbJsFilterNode implements TbNode { scriptEngine.destroy(); } } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java index e039eaeb14..87bb2113d9 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbJsSwitchNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.filter; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.MoreExecutors; -import lombok.extern.slf4j.Slf4j; import org.checkerframework.checker.nullness.qual.Nullable; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.ScriptEngine; @@ -33,7 +32,6 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.Set; -@Slf4j @RuleNode( type = ComponentType.FILTER, name = "switch", customRelations = true, @@ -46,17 +44,17 @@ import java.util.Set; "Message metadata can be accessed via metadata property. For example metadata.customerName === 'John';
    " + "Message type can be accessed via msgType property.

    " + "Output connections: Custom connection(s) defined by switch node or Failure", - configDirective = "tbFilterNodeSwitchConfig") + configDirective = "tbFilterNodeSwitchConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/switch/" +) public class TbJsSwitchNode implements TbNode { - private TbJsSwitchNodeConfiguration config; private ScriptEngine scriptEngine; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class); - this.scriptEngine = ctx.createScriptEngine(config.getScriptLang(), - ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getTbelScript() : config.getJsScript()); + var config = TbNodeUtils.convert(configuration, TbJsSwitchNodeConfiguration.class); + scriptEngine = ctx.createScriptEngine(config.getScriptLang(), ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getTbelScript() : config.getJsScript()); } @Override @@ -84,4 +82,5 @@ public class TbJsSwitchNode implements TbNode { scriptEngine.destroy(); } } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java index 0e26a89afb..09479de488 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeFilterNode.java @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.filter; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; @@ -26,10 +25,6 @@ import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -/** - * Created by ashvayka on 19.01.18. - */ -@Slf4j @RuleNode( type = ComponentType.FILTER, name = "message type filter", @@ -38,14 +33,16 @@ import org.thingsboard.server.common.msg.TbMsg; nodeDescription = "Filter incoming messages by Message Type", nodeDetails = "If incoming message type is expected - send Message via True chain, otherwise False chain is used.

    " + "Output connections: True, False, Failure", - configDirective = "tbFilterNodeMessageTypeConfig") + configDirective = "tbFilterNodeMessageTypeConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/message-type-filter/" +) public class TbMsgTypeFilterNode implements TbNode { - TbMsgTypeFilterNodeConfiguration config; + private TbMsgTypeFilterNodeConfiguration config; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbMsgTypeFilterNodeConfiguration.class); + config = TbNodeUtils.convert(configuration, TbMsgTypeFilterNodeConfiguration.class); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java index f8d8278cbd..b82eba4e50 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbMsgTypeSwitchNode.java @@ -15,18 +15,14 @@ */ package org.thingsboard.rule.engine.filter; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; -import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -@Slf4j @RuleNode( type = ComponentType.FILTER, name = "message type switch", @@ -36,15 +32,13 @@ import org.thingsboard.server.common.msg.TbMsg; nodeDetails = "Sends messages with message types \"Post attributes\", \"Post telemetry\", \"RPC Request\"" + " etc. via corresponding chain, otherwise Other chain is used.

    " + "Output connections: Message type connection, Other - if message type is custom or Failure", - configDirective = "tbNodeEmptyConfig") + configDirective = "tbNodeEmptyConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/message-type-switch/" +) public class TbMsgTypeSwitchNode implements TbNode { - EmptyNodeConfiguration config; - @Override - public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class); - } + public void init(TbContext ctx, TbNodeConfiguration configuration) {} @Override public void onMsg(TbContext ctx, TbMsg msg) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java index ffaea592f0..57cce66e21 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeFilterNode.java @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.filter; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; @@ -27,7 +26,6 @@ import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -@Slf4j @RuleNode( type = ComponentType.FILTER, name = "entity type filter", @@ -36,14 +34,16 @@ import org.thingsboard.server.common.msg.TbMsg; nodeDescription = "Filter incoming messages by the type of message originator entity", nodeDetails = "Checks that the entity type of the incoming message originator matches one of the values specified in the filter.

    " + "Output connections: True, False, Failure", - configDirective = "tbFilterNodeOriginatorTypeConfig") + configDirective = "tbFilterNodeOriginatorTypeConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/entity-type-filter/" +) public class TbOriginatorTypeFilterNode implements TbNode { - TbOriginatorTypeFilterNodeConfiguration config; + private TbOriginatorTypeFilterNodeConfiguration config; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbOriginatorTypeFilterNodeConfiguration.class); + config = TbNodeUtils.convert(configuration, TbOriginatorTypeFilterNodeConfiguration.class); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java index 5c2a887b1d..5cee36cf14 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/filter/TbOriginatorTypeSwitchNode.java @@ -15,14 +15,12 @@ */ package org.thingsboard.rule.engine.filter; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.plugin.ComponentType; -@Slf4j @RuleNode( type = ComponentType.FILTER, name = "entity type switch", @@ -31,7 +29,9 @@ import org.thingsboard.server.common.data.plugin.ComponentType; nodeDescription = "Route incoming messages by Message Originator Type", nodeDetails = "Routes messages to chain according to the entity type ('Device', 'Asset', etc.).

    " + "Output connections: Message originator type or Failure", - configDirective = "tbNodeEmptyConfig") + configDirective = "tbNodeEmptyConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/entity-type-switch/" +) public class TbOriginatorTypeSwitchNode extends TbAbstractTypeSwitchNode { @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbAckNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbAckNode.java index 8692cd6c55..b3d1ca71cf 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbAckNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbAckNode.java @@ -15,34 +15,27 @@ */ package org.thingsboard.rule.engine.flow; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; -import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -@Slf4j @RuleNode( type = ComponentType.FLOW, name = "acknowledge", configClazz = EmptyNodeConfiguration.class, nodeDescription = "Acknowledges the incoming message", nodeDetails = "After acknowledgement, the message is pushed to related rule nodes. Useful if you don't care what happens to this message next.", - configDirective = "tbNodeEmptyConfig" + configDirective = "tbNodeEmptyConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/flow/acknowledge/" ) public class TbAckNode implements TbNode { - EmptyNodeConfiguration config; - @Override - public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class); - } + public void init(TbContext ctx, TbNodeConfiguration configuration) {} @Override public void onMsg(TbContext ctx, TbMsg msg) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java index d7da783172..dee340af13 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbCheckpointNode.java @@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.flow; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -31,7 +30,6 @@ import org.thingsboard.server.common.msg.TbMsg; import static org.thingsboard.server.common.data.DataConstants.QUEUE_NAME; -@Slf4j @RuleNode( type = ComponentType.FLOW, name = "checkpoint", @@ -40,7 +38,8 @@ import static org.thingsboard.server.common.data.DataConstants.QUEUE_NAME; hasQueueName = true, nodeDescription = "transfers the message to another queue", nodeDetails = "After successful transfer incoming message is automatically acknowledged. Queue name is configurable.", - configDirective = "tbNodeEmptyConfig" + configDirective = "tbNodeEmptyConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/flow/checkpoint/" ) public class TbCheckpointNode implements TbNode { @@ -48,7 +47,7 @@ public class TbCheckpointNode implements TbNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.queueName = ctx.getQueueName(); + queueName = ctx.getQueueName(); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java index 3c72699884..a8efa6ddd7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainInputNode.java @@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.flow; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; @@ -34,7 +33,6 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.Optional; import java.util.UUID; -@Slf4j @RuleNode( type = ComponentType.FLOW, name = "rule chain", @@ -49,7 +47,8 @@ import java.util.UUID; configDirective = "tbFlowNodeRuleChainInputConfig", relationTypes = {}, ruleChainNode = true, - customRelations = true + customRelations = true, + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/flow/rule-chain/" ) public class TbRuleChainInputNode implements TbNode { @@ -106,4 +105,5 @@ public class TbRuleChainInputNode implements TbNode { default -> null; }); } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainOutputNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainOutputNode.java index 968ba741a3..88eba3573b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainOutputNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/flow/TbRuleChainOutputNode.java @@ -15,17 +15,14 @@ */ package org.thingsboard.rule.engine.flow; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; -import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -@Slf4j @RuleNode( type = ComponentType.FLOW, name = "output", @@ -35,13 +32,13 @@ import org.thingsboard.server.common.msg.TbMsg; "The output is forwarded to the caller rule chain, as an output of the corresponding \"input\" rule node. " + "The output rule node name corresponds to the relation type of the output message, and it is used to forward messages to other rule nodes in the caller rule chain. ", configDirective = "tbFlowNodeRuleChainOutputConfig", - outEnabled = false + outEnabled = false, + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/flow/output/" ) public class TbRuleChainOutputNode implements TbNode { @Override - public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - } + public void init(TbContext ctx, TbNodeConfiguration configuration) {} @Override public void onMsg(TbContext ctx, TbMsg msg) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java index ea893cc71b..2c170f7a2d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/gcp/pubsub/TbPubSubNode.java @@ -53,7 +53,8 @@ import java.util.concurrent.TimeUnit; "(messageId in the Message Metadata from the GCP PubSub. " + "messageId field can be accessed with metadata.messageId.", configDirective = "tbExternalNodePubSubConfig", - iconUrl = "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4IiB2aWV3Qm94PSIwIDAgMTI4IDEyOCI+Cjx0aXRsZT5DbG91ZCBQdWJTdWI8L3RpdGxlPgo8Zz4KPHBhdGggZD0iTTEyNi40Nyw1OC4xMmwtMjYuMy00NS43NEExMS41NiwxMS41NiwwLDAsMCw5MC4zMSw2LjVIMzcuN2ExMS41NSwxMS41NSwwLDAsMC05Ljg2LDUuODhMMS41Myw1OGExMS40OCwxMS40OCwwLDAsMCwwLDExLjQ0bDI2LjMsNDZhMTEuNzcsMTEuNzcsMCwwLDAsOS44Niw2LjA5SDkwLjNhMTEuNzMsMTEuNzMsMCwwLDAsOS44Ny02LjA2bDI2LjMtNDUuNzRBMTEuNzMsMTEuNzMsMCwwLDAsMTI2LjQ3LDU4LjEyWiIgc3R5bGU9ImZpbGw6ICM3MzViMmYiLz4KPHBhdGggZD0iTTg5LjIyLDQ3Ljc0LDgzLjM2LDQ5bC0xNC42LTE0LjZMNjQuMDksNDMuMSw2MS41NSw1My4ybDQuMjksNC4yOUw1Ny42LDU5LjE4LDQ2LjMsNDcuODhsLTcuNjcsNy4zOEw1Mi43Niw2OS4zN2wtMTUsMTEuOUw3OCwxMjEuNUg5MC4zYTExLjczLDExLjczLDAsMCwwLDkuODctNi4wNmwyMC43Mi0zNloiIHN0eWxlPSJvcGFjaXR5OiAwLjA3MDAwMDAwMDI5ODAyMztpc29sYXRpb246IGlzb2xhdGUiLz4KPHBhdGggZD0iTTgyLjg2LDQ3YTUuMzIsNS4zMiwwLDEsMS0xLjk1LDcuMjdBNS4zMiw1LjMyLDAsMCwxLDgyLjg2LDQ3IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNMzkuODIsNTYuMThhNS4zMiw1LjMyLDAsMSwxLDcuMjctMS45NSw1LjMyLDUuMzIsMCwwLDEtNy4yNywxLjk1IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNNjkuMzIsODguODVBNS4zMiw1LjMyLDAsMSwxLDY0LDgzLjUyYTUuMzIsNS4zMiwwLDAsMSw1LjMyLDUuMzIiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxnPgo8cGF0aCBkPSJNNjQsNTIuOTRhMTEuMDYsMTEuMDYsMCwwLDEsMi40Ni4yOFYzOS4xNUg2MS41NFY1My4yMkExMS4wNiwxMS4wNiwwLDAsMSw2NCw1Mi45NFoiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxwYXRoIGQ9Ik03NC41Nyw2Ny4yNmExMSwxMSwwLDAsMS0yLjQ3LDQuMjVsMTIuMTksNywyLjQ2LTQuMjZaIiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNNTMuNDMsNjcuMjZsLTEyLjE4LDcsMi40Niw0LjI2LDEyLjE5LTdBMTEsMTEsMCwwLDEsNTMuNDMsNjcuMjZaIiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8L2c+CjxwYXRoIGQ9Ik03Mi42LDY0QTguNiw4LjYsMCwxLDEsNjQsNTUuNCw4LjYsOC42LDAsMCwxLDcyLjYsNjQiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxwYXRoIGQ9Ik0zOS4xLDcwLjU3YTYuNzYsNi43NiwwLDEsMS0yLjQ3LDkuMjMsNi43Niw2Ljc2LDAsMCwxLDIuNDctOS4yMyIgc3R5bGU9ImZpbGw6ICNmZmYiLz4KPHBhdGggZD0iTTgyLjE0LDgyLjI3YTYuNzYsNi43NiwwLDEsMSw5LjIzLTIuNDcsNi43NSw2Ljc1LDAsMCwxLTkuMjMsMi40NyIgc3R5bGU9ImZpbGw6ICNmZmYiLz4KPHBhdGggZD0iTTcwLjc2LDM5LjE1QTYuNzYsNi43NiwwLDEsMSw2NCwzMi4zOWE2Ljc2LDYuNzYsMCwwLDEsNi43Niw2Ljc2IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8L2c+Cjwvc3ZnPgo=" + iconUrl = "data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4IiB2aWV3Qm94PSIwIDAgMTI4IDEyOCI+Cjx0aXRsZT5DbG91ZCBQdWJTdWI8L3RpdGxlPgo8Zz4KPHBhdGggZD0iTTEyNi40Nyw1OC4xMmwtMjYuMy00NS43NEExMS41NiwxMS41NiwwLDAsMCw5MC4zMSw2LjVIMzcuN2ExMS41NSwxMS41NSwwLDAsMC05Ljg2LDUuODhMMS41Myw1OGExMS40OCwxMS40OCwwLDAsMCwwLDExLjQ0bDI2LjMsNDZhMTEuNzcsMTEuNzcsMCwwLDAsOS44Niw2LjA5SDkwLjNhMTEuNzMsMTEuNzMsMCwwLDAsOS44Ny02LjA2bDI2LjMtNDUuNzRBMTEuNzMsMTEuNzMsMCwwLDAsMTI2LjQ3LDU4LjEyWiIgc3R5bGU9ImZpbGw6ICM3MzViMmYiLz4KPHBhdGggZD0iTTg5LjIyLDQ3Ljc0LDgzLjM2LDQ5bC0xNC42LTE0LjZMNjQuMDksNDMuMSw2MS41NSw1My4ybDQuMjksNC4yOUw1Ny42LDU5LjE4LDQ2LjMsNDcuODhsLTcuNjcsNy4zOEw1Mi43Niw2OS4zN2wtMTUsMTEuOUw3OCwxMjEuNUg5MC4zYTExLjczLDExLjczLDAsMCwwLDkuODctNi4wNmwyMC43Mi0zNloiIHN0eWxlPSJvcGFjaXR5OiAwLjA3MDAwMDAwMDI5ODAyMztpc29sYXRpb246IGlzb2xhdGUiLz4KPHBhdGggZD0iTTgyLjg2LDQ3YTUuMzIsNS4zMiwwLDEsMS0xLjk1LDcuMjdBNS4zMiw1LjMyLDAsMCwxLDgyLjg2LDQ3IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNMzkuODIsNTYuMThhNS4zMiw1LjMyLDAsMSwxLDcuMjctMS45NSw1LjMyLDUuMzIsMCwwLDEtNy4yNywxLjk1IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNNjkuMzIsODguODVBNS4zMiw1LjMyLDAsMSwxLDY0LDgzLjUyYTUuMzIsNS4zMiwwLDAsMSw1LjMyLDUuMzIiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxnPgo8cGF0aCBkPSJNNjQsNTIuOTRhMTEuMDYsMTEuMDYsMCwwLDEsMi40Ni4yOFYzOS4xNUg2MS41NFY1My4yMkExMS4wNiwxMS4wNiwwLDAsMSw2NCw1Mi45NFoiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxwYXRoIGQ9Ik03NC41Nyw2Ny4yNmExMSwxMSwwLDAsMS0yLjQ3LDQuMjVsMTIuMTksNywyLjQ2LTQuMjZaIiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8cGF0aCBkPSJNNTMuNDMsNjcuMjZsLTEyLjE4LDcsMi40Niw0LjI2LDEyLjE5LTdBMTEsMTEsMCwwLDEsNTMuNDMsNjcuMjZaIiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8L2c+CjxwYXRoIGQ9Ik03Mi42LDY0QTguNiw4LjYsMCwxLDEsNjQsNTUuNCw4LjYsOC42LDAsMCwxLDcyLjYsNjQiIHN0eWxlPSJmaWxsOiAjZmZmIi8+CjxwYXRoIGQ9Ik0zOS4xLDcwLjU3YTYuNzYsNi43NiwwLDEsMS0yLjQ3LDkuMjMsNi43Niw2Ljc2LDAsMCwxLDIuNDctOS4yMyIgc3R5bGU9ImZpbGw6ICNmZmYiLz4KPHBhdGggZD0iTTgyLjE0LDgyLjI3YTYuNzYsNi43NiwwLDEsMSw5LjIzLTIuNDcsNi43NSw2Ljc1LDAsMCwxLTkuMjMsMi40NyIgc3R5bGU9ImZpbGw6ICNmZmYiLz4KPHBhdGggZD0iTTcwLjc2LDM5LjE1QTYuNzYsNi43NiwwLDEsMSw2NCwzMi4zOWE2Ljc2LDYuNzYsMCwwLDEsNi43Niw2Ljc2IiBzdHlsZT0iZmlsbDogI2ZmZiIvPgo8L2c+Cjwvc3ZnPgo=", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/gcp-pubsub/" ) public class TbPubSubNode extends TbAbstractExternalNode { @@ -155,4 +156,5 @@ public class TbPubSubNode extends TbAbstractExternalNode { .setExecutorProvider(FixedExecutorProvider.create(ctx.getPubSubRuleNodeExecutorProvider().getExecutor())) .build(); } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java index b362b60b8e..488ee2123e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingActionNode.java @@ -20,7 +20,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.gson.Gson; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; @@ -34,10 +33,10 @@ import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -47,10 +46,6 @@ import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.INSIDE; import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.LEFT; import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.OUTSIDE; -/** - * Created by ashvayka on 19.01.18. - */ -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "gps geofencing events", @@ -66,12 +61,13 @@ import static org.thingsboard.rule.engine.util.GpsGeofencingEvents.OUTSIDE; "If the presence monitoring strategy \"On each message\" is selected, sends messages via rule node connection type Inside or Outside every time the geofencing condition is satisfied. " + "

    " + "Output connections: Entered, Left, Inside, Outside, Success", - configDirective = "tbActionNodeGpsGeofencingConfig" + configDirective = "tbActionNodeGpsGeofencingConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/gps-geofencing-events/" ) public class TbGpsGeofencingActionNode extends AbstractGeofencingNode { private static final String REPORT_PRESENCE_STATUS_ON_EACH_MESSAGE = "reportPresenceStatusOnEachMessage"; - private final Map entityStates = new HashMap<>(); + private final ConcurrentMap entityStates = new ConcurrentHashMap<>(); private final Gson gson = new Gson(); @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java index 19c901483c..7c1a561492 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/geo/TbGpsGeofencingFilterNode.java @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.geo; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeException; @@ -23,10 +22,6 @@ import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; -/** - * Created by ashvayka on 19.01.18. - */ -@Slf4j @RuleNode( type = ComponentType.FILTER, name = "gps geofencing filter", @@ -60,7 +55,9 @@ import org.thingsboard.server.common.msg.TbMsg; "

    " + "Available radius units: METER, KILOMETER, FOOT, MILE, NAUTICAL_MILE;

    " + "Output connections: True, False, Failure", - configDirective = "tbFilterNodeGpsGeofencingConfig") + configDirective = "tbFilterNodeGpsGeofencingConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/filter/gps-geofencing-filter/" +) public class TbGpsGeofencingFilterNode extends AbstractGeofencingNode { @Override @@ -72,4 +69,5 @@ public class TbGpsGeofencingFilterNode extends AbstractGeofencingNode getConfigClazz() { return TbGpsGeofencingFilterNodeConfiguration.class; } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java index d544d0647d..a6549dbd15 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/kafka/TbKafkaNode.java @@ -58,7 +58,8 @@ import java.util.Properties; "Outbound message will contain response fields (offset, partition, topic)" + " from the Kafka in the Message Metadata. For example partition field can be accessed with metadata.partition.", configDirective = "tbExternalNodeKafkaConfig", - iconUrl = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUzOCIgaGVpZ2h0PSIyNTAwIiB2aWV3Qm94PSIwIDAgMjU2IDQxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PHBhdGggZD0iTTIwMS44MTYgMjMwLjIxNmMtMTYuMTg2IDAtMzAuNjk3IDcuMTcxLTQwLjYzNCAxOC40NjFsLTI1LjQ2My0xOC4wMjZjMi43MDMtNy40NDIgNC4yNTUtMTUuNDMzIDQuMjU1LTIzLjc5NyAwLTguMjE5LTEuNDk4LTE2LjA3Ni00LjExMi0yMy40MDhsMjUuNDA2LTE3LjgzNWM5LjkzNiAxMS4yMzMgMjQuNDA5IDE4LjM2NSA0MC41NDggMTguMzY1IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI5Ljg3OS0yNC4zMDktNTQuMTg0LTU0LjE4NC01NC4xODQtMjkuODc1IDAtNTQuMTg0IDI0LjMwNS01NC4xODQgNTQuMTg0IDAgNS4zNDguODA4IDEwLjUwNSAyLjI1OCAxNS4zODlsLTI1LjQyMyAxNy44NDRjLTEwLjYyLTEzLjE3NS0yNS45MTEtMjIuMzc0LTQzLjMzMy0yNS4xODJ2LTMwLjY0YzI0LjU0NC01LjE1NSA0My4wMzctMjYuOTYyIDQzLjAzNy01My4wMTlDMTI0LjE3MSAyNC4zMDUgOTkuODYyIDAgNjkuOTg3IDAgNDAuMTEyIDAgMTUuODAzIDI0LjMwNSAxNS44MDMgNTQuMTg0YzAgMjUuNzA4IDE4LjAxNCA0Ny4yNDYgNDIuMDY3IDUyLjc2OXYzMS4wMzhDMjUuMDQ0IDE0My43NTMgMCAxNzIuNDAxIDAgMjA2Ljg1NGMwIDM0LjYyMSAyNS4yOTIgNjMuMzc0IDU4LjM1NSA2OC45NHYzMi43NzRjLTI0LjI5OSA1LjM0MS00Mi41NTIgMjcuMDExLTQyLjU1MiA1Mi44OTQgMCAyOS44NzkgMjQuMzA5IDU0LjE4NCA1NC4xODQgNTQuMTg0IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI1Ljg4My0xOC4yNTMtNDcuNTUzLTQyLjU1Mi01Mi44OTR2LTMyLjc3NWE2OS45NjUgNjkuOTY1IDAgMCAwIDQyLjYtMjQuNzc2bDI1LjYzMyAxOC4xNDNjLTEuNDIzIDQuODQtMi4yMiA5Ljk0Ni0yLjIyIDE1LjI0IDAgMjkuODc5IDI0LjMwOSA1NC4xODQgNTQuMTg0IDU0LjE4NCAyOS44NzUgMCA1NC4xODQtMjQuMzA1IDU0LjE4NC01NC4xODQgMC0yOS44NzktMjQuMzA5LTU0LjE4NC01NC4xODQtNTQuMTg0em0wLTEyNi42OTVjMTQuNDg3IDAgMjYuMjcgMTEuNzg4IDI2LjI3IDI2LjI3MXMtMTEuNzgzIDI2LjI3LTI2LjI3IDI2LjI3LTI2LjI3LTExLjc4Ny0yNi4yNy0yNi4yN2MwLTE0LjQ4MyAxMS43ODMtMjYuMjcxIDI2LjI3LTI2LjI3MXptLTE1OC4xLTQ5LjMzN2MwLTE0LjQ4MyAxMS43ODQtMjYuMjcgMjYuMjcxLTI2LjI3czI2LjI3IDExLjc4NyAyNi4yNyAyNi4yN2MwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3em01Mi41NDEgMzA3LjI3OGMwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3YzAtMTQuNDgzIDExLjc4NC0yNi4yNyAyNi4yNzEtMjYuMjdzMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3em0tMjYuMjcyLTExNy45N2MtMjAuMjA1IDAtMzYuNjQyLTE2LjQzNC0zNi42NDItMzYuNjM4IDAtMjAuMjA1IDE2LjQzNy0zNi42NDIgMzYuNjQyLTM2LjY0MiAyMC4yMDQgMCAzNi42NDEgMTYuNDM3IDM2LjY0MSAzNi42NDIgMCAyMC4yMDQtMTYuNDM3IDM2LjYzOC0zNi42NDEgMzYuNjM4em0xMzEuODMxIDY3LjE3OWMtMTQuNDg3IDAtMjYuMjctMTEuNzg4LTI2LjI3LTI2LjI3MXMxMS43ODMtMjYuMjcgMjYuMjctMjYuMjcgMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3YzAgMTQuNDgzLTExLjc4MyAyNi4yNzEtMjYuMjcgMjYuMjcxeiIvPjwvc3ZnPg==" + iconUrl = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTUzOCIgaGVpZ2h0PSIyNTAwIiB2aWV3Qm94PSIwIDAgMjU2IDQxNiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCI+PHBhdGggZD0iTTIwMS44MTYgMjMwLjIxNmMtMTYuMTg2IDAtMzAuNjk3IDcuMTcxLTQwLjYzNCAxOC40NjFsLTI1LjQ2My0xOC4wMjZjMi43MDMtNy40NDIgNC4yNTUtMTUuNDMzIDQuMjU1LTIzLjc5NyAwLTguMjE5LTEuNDk4LTE2LjA3Ni00LjExMi0yMy40MDhsMjUuNDA2LTE3LjgzNWM5LjkzNiAxMS4yMzMgMjQuNDA5IDE4LjM2NSA0MC41NDggMTguMzY1IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI5Ljg3OS0yNC4zMDktNTQuMTg0LTU0LjE4NC01NC4xODQtMjkuODc1IDAtNTQuMTg0IDI0LjMwNS01NC4xODQgNTQuMTg0IDAgNS4zNDguODA4IDEwLjUwNSAyLjI1OCAxNS4zODlsLTI1LjQyMyAxNy44NDRjLTEwLjYyLTEzLjE3NS0yNS45MTEtMjIuMzc0LTQzLjMzMy0yNS4xODJ2LTMwLjY0YzI0LjU0NC01LjE1NSA0My4wMzctMjYuOTYyIDQzLjAzNy01My4wMTlDMTI0LjE3MSAyNC4zMDUgOTkuODYyIDAgNjkuOTg3IDAgNDAuMTEyIDAgMTUuODAzIDI0LjMwNSAxNS44MDMgNTQuMTg0YzAgMjUuNzA4IDE4LjAxNCA0Ny4yNDYgNDIuMDY3IDUyLjc2OXYzMS4wMzhDMjUuMDQ0IDE0My43NTMgMCAxNzIuNDAxIDAgMjA2Ljg1NGMwIDM0LjYyMSAyNS4yOTIgNjMuMzc0IDU4LjM1NSA2OC45NHYzMi43NzRjLTI0LjI5OSA1LjM0MS00Mi41NTIgMjcuMDExLTQyLjU1MiA1Mi44OTQgMCAyOS44NzkgMjQuMzA5IDU0LjE4NCA1NC4xODQgNTQuMTg0IDI5Ljg3NSAwIDU0LjE4NC0yNC4zMDUgNTQuMTg0LTU0LjE4NCAwLTI1Ljg4My0xOC4yNTMtNDcuNTUzLTQyLjU1Mi01Mi44OTR2LTMyLjc3NWE2OS45NjUgNjkuOTY1IDAgMCAwIDQyLjYtMjQuNzc2bDI1LjYzMyAxOC4xNDNjLTEuNDIzIDQuODQtMi4yMiA5Ljk0Ni0yLjIyIDE1LjI0IDAgMjkuODc5IDI0LjMwOSA1NC4xODQgNTQuMTg0IDU0LjE4NCAyOS44NzUgMCA1NC4xODQtMjQuMzA1IDU0LjE4NC01NC4xODQgMC0yOS44NzktMjQuMzA5LTU0LjE4NC01NC4xODQtNTQuMTg0em0wLTEyNi42OTVjMTQuNDg3IDAgMjYuMjcgMTEuNzg4IDI2LjI3IDI2LjI3MXMtMTEuNzgzIDI2LjI3LTI2LjI3IDI2LjI3LTI2LjI3LTExLjc4Ny0yNi4yNy0yNi4yN2MwLTE0LjQ4MyAxMS43ODMtMjYuMjcxIDI2LjI3LTI2LjI3MXptLTE1OC4xLTQ5LjMzN2MwLTE0LjQ4MyAxMS43ODQtMjYuMjcgMjYuMjcxLTI2LjI3czI2LjI3IDExLjc4NyAyNi4yNyAyNi4yN2MwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3em01Mi41NDEgMzA3LjI3OGMwIDE0LjQ4My0xMS43ODMgMjYuMjctMjYuMjcgMjYuMjdzLTI2LjI3MS0xMS43ODctMjYuMjcxLTI2LjI3YzAtMTQuNDgzIDExLjc4NC0yNi4yNyAyNi4yNzEtMjYuMjdzMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3em0tMjYuMjcyLTExNy45N2MtMjAuMjA1IDAtMzYuNjQyLTE2LjQzNC0zNi42NDItMzYuNjM4IDAtMjAuMjA1IDE2LjQzNy0zNi42NDIgMzYuNjQyLTM2LjY0MiAyMC4yMDQgMCAzNi42NDEgMTYuNDM3IDM2LjY0MSAzNi42NDIgMCAyMC4yMDQtMTYuNDM3IDM2LjYzOC0zNi42NDEgMzYuNjM4em0xMzEuODMxIDY3LjE3OWMtMTQuNDg3IDAtMjYuMjctMTEuNzg4LTI2LjI3LTI2LjI3MXMxMS43ODMtMjYuMjcgMjYuMjctMjYuMjcgMjYuMjcgMTEuNzg3IDI2LjI3IDI2LjI3YzAgMTQuNDgzLTExLjc4MyAyNi4yNzEtMjYuMjcgMjYuMjcxeiIvPjwvc3ZnPg==", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/kafka/" ) public class TbKafkaNode extends TbAbstractExternalNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java index f20aeca55f..4867735a2b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java @@ -16,7 +16,6 @@ package org.thingsboard.rule.engine.mail; import com.fasterxml.jackson.core.type.TypeReference; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -34,7 +33,6 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.HashMap; import java.util.Map; -@Slf4j @RuleNode( type = ComponentType.TRANSFORMATION, name = "to email", @@ -43,7 +41,8 @@ import java.util.Map; nodeDetails = "Transforms message to email message. If transformation completed successfully output message type will be set to SEND_EMAIL.

    " + "Output connections: Success, Failure.", configDirective = "tbTransformationNodeToEmailConfig", - icon = "email" + icon = "email", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/to-email/" ) public class TbMsgToEmailNode implements TbNode { @@ -55,20 +54,15 @@ public class TbMsgToEmailNode implements TbNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbMsgToEmailNodeConfiguration.class); - this.dynamicMailBodyType = DYNAMIC.equals(this.config.getMailBodyType()); - } + config = TbNodeUtils.convert(configuration, TbMsgToEmailNodeConfiguration.class); + dynamicMailBodyType = DYNAMIC.equals(config.getMailBodyType()); + } @Override public void onMsg(TbContext ctx, TbMsg msg) { - try { - TbEmail email = convert(msg); - TbMsg emailMsg = buildEmailMsg(ctx, msg, email); - ctx.tellNext(emailMsg, TbNodeConnectionType.SUCCESS); - } catch (Exception ex) { - log.warn("Can not convert message to email " + ex.getMessage()); - ctx.tellFailure(msg, ex); - } + TbEmail email = convert(msg); + TbMsg emailMsg = buildEmailMsg(ctx, msg, email); + ctx.tellNext(emailMsg, TbNodeConnectionType.SUCCESS); } private TbMsg buildEmailMsg(TbContext ctx, TbMsg msg, TbEmail email) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java index 630d87cb54..198808297e 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbSendEmailNode.java @@ -45,7 +45,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; " where created using to Email transformation Node, please connect this Node " + "with to Email Node using Successful chain.", configDirective = "tbExternalNodeSendEmailConfig", - icon = "send" + icon = "send", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/send-email/" ) public class TbSendEmailNode extends TbAbstractExternalNode { @@ -91,7 +92,7 @@ public class TbSendEmailNode extends TbAbstractExternalNode { } } - private TbEmail getEmail(TbMsg msg) throws IOException { + private TbEmail getEmail(TbMsg msg) { TbEmail email = JacksonUtil.fromString(msg.getData(), TbEmail.class); if (StringUtils.isBlank(email.getTo())) { throw new IllegalStateException("Email destination can not be blank [" + email.getTo() + "]"); @@ -141,4 +142,5 @@ public class TbSendEmailNode extends TbAbstractExternalNode { } return javaMailProperties; } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java index 76ba71163a..997bbfa2e2 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java @@ -20,7 +20,6 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.SettableFuture; -import lombok.extern.slf4j.Slf4j; import net.objecthunter.exp4j.Expression; import net.objecthunter.exp4j.ExpressionBuilder; import org.springframework.util.ConcurrentReferenceHashMap; @@ -56,8 +55,6 @@ import java.util.stream.Collectors; import static org.thingsboard.common.util.ExpressionFunctionsUtil.userDefinedFunctions; import static org.thingsboard.rule.engine.math.TbMathArgumentType.CONSTANT; -@SuppressWarnings("UnstableApiUsage") -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "math function", @@ -78,8 +75,8 @@ import static org.thingsboard.rule.engine.math.TbMathArgumentType.CONSTANT; "The execution is synchronized in scope of message originator (e.g. device) and server node. " + "If you have rule nodes in different rule chains, they will process messages from the same originator synchronously in the scope of the server node.", configDirective = "tbActionNodeMathFunctionConfig", - icon = "calculate" - + icon = "calculate", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/math-function/" ) public class TbMathNode implements TbNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java index 29c7dc51e5..dd1888f3de 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java @@ -20,7 +20,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import lombok.extern.slf4j.Slf4j; import org.springframework.util.ConcurrentReferenceHashMap; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; @@ -43,8 +42,8 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Map; -@Slf4j -@RuleNode(type = ComponentType.ENRICHMENT, +@RuleNode( + type = ComponentType.ENRICHMENT, name = "calculate delta", version = 1, relationTypes = {TbNodeConnectionType.SUCCESS, TbNodeConnectionType.FAILURE, TbNodeConnectionType.OTHER}, @@ -53,7 +52,9 @@ import java.util.Map; "and current value for this key from the incoming message", nodeDetails = "Useful for metering use cases, when you need to calculate consumption based on pulse counter reading.

    " + "Output connections: Success, Other or Failure.", - configDirective = "tbEnrichmentNodeCalculateDeltaConfig") + configDirective = "tbEnrichmentNodeCalculateDeltaConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/calculate-delta/" +) public class CalculateDeltaNode implements TbNode { private Map cache; @@ -189,7 +190,6 @@ public class CalculateDeltaNode implements TbNode { return fetchLatestValueAsync(ctx, originator); } - private record ValueWithTs(long ts, double value) { - } + private record ValueWithTs(long ts, double value) {} } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java index 294354279a..421da9c768 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java @@ -16,7 +16,6 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -33,7 +32,6 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.concurrent.ExecutionException; -@Slf4j @RuleNode( type = ComponentType.ENRICHMENT, name = "fetch device credentials", @@ -45,7 +43,9 @@ import java.util.concurrent.ExecutionException; "Useful when you need to fetch device credentials and use them for further message processing. " + "For example, use device credentials to interact with external systems.

    " + "Output connections: Success, Failure.", - configDirective = "tbEnrichmentNodeFetchDeviceCredentialsConfig") + configDirective = "tbEnrichmentNodeFetchDeviceCredentialsConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/fetch-device-credentials/" +) public class TbFetchDeviceCredentialsNode extends TbAbstractNodeWithFetchTo { private static final String CREDENTIALS = "credentials"; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java index 68ec3bb373..8af44f67f0 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -30,11 +29,8 @@ import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; -/** - * Created by ashvayka on 19.01.18. - */ -@Slf4j -@RuleNode(type = ComponentType.ENRICHMENT, +@RuleNode( + type = ComponentType.ENRICHMENT, name = "originator attributes", configClazz = TbGetAttributesNodeConfiguration.class, version = 1, @@ -43,7 +39,9 @@ import org.thingsboard.server.common.msg.TbMsg; "that are not included in the incoming message to use them for further message processing. " + "For example to filter messages based on the threshold value stored in the attributes.

    " + "Output connections: Success, Failure.", - configDirective = "tbEnrichmentNodeOriginatorAttributesConfig") + configDirective = "tbEnrichmentNodeOriginatorAttributesConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/originator-attributes/" +) public class TbGetAttributesNode extends TbAbstractGetAttributesNode { @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java index 60ca50f2b3..d779bacb77 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -30,7 +29,6 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; -@Slf4j @RuleNode( type = ComponentType.ENRICHMENT, name = "customer attributes", @@ -41,7 +39,9 @@ import org.thingsboard.server.common.data.util.TbPair; "that is stored as customer attributes or telemetry data and used for dynamic message filtering, transformation, " + "or actions such as alarm creation if the threshold is exceeded.

    " + "Output connections: Success, Failure.", - configDirective = "tbEnrichmentNodeCustomerAttributesConfig") + configDirective = "tbEnrichmentNodeCustomerAttributesConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/customer-attributes/" +) public class TbGetCustomerAttributeNode extends TbAbstractGetEntityDataNode { private static final String CUSTOMER_NOT_FOUND_MESSAGE = "Failed to find customer for entity with id: %s and type: %s"; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java index 55def65d46..7848c3c1c6 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -41,8 +40,8 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.NoSuchElementException; -@Slf4j -@RuleNode(type = ComponentType.ENRICHMENT, +@RuleNode( + type = ComponentType.ENRICHMENT, name = "customer details", configClazz = TbGetCustomerDetailsNodeConfiguration.class, version = 1, @@ -50,7 +49,9 @@ import java.util.NoSuchElementException; nodeDetails = "Useful in multi-customer solutions where we need dynamically use customer contact information " + "such as email, phone, address, etc., for notifications via email, SMS, and other notification providers.

    " + "Output connections: Success, Failure.", - configDirective = "tbEnrichmentNodeEntityDetailsConfig") + configDirective = "tbEnrichmentNodeEntityDetailsConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/customer-details/" +) public class TbGetCustomerDetailsNode extends TbAbstractGetEntityDetailsNode { private static final String CUSTOMER_PREFIX = "customer_"; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java index 031aae8859..cf370114c3 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -31,8 +30,8 @@ import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; -@Slf4j -@RuleNode(type = ComponentType.ENRICHMENT, +@RuleNode( + type = ComponentType.ENRICHMENT, name = "related device attributes", configClazz = TbGetDeviceAttrNodeConfiguration.class, version = 1, @@ -42,7 +41,9 @@ import org.thingsboard.server.common.msg.TbMsg; "Useful when you need to retrieve attributes and/or latest telemetry values from device that has a relation " + "to the message originator and use them for further message processing.

    " + "Output connections: Success, Failure.", - configDirective = "tbEnrichmentNodeDeviceAttributesConfig") + configDirective = "tbEnrichmentNodeDeviceAttributesConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/related-device-attributes/" +) public class TbGetDeviceAttrNode extends TbAbstractGetAttributesNode { private static final String RELATED_DEVICE_NOT_FOUND_MESSAGE = "Failed to find related device to message originator using relation query specified in the configuration!"; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java index 816b198836..1c3269db9d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java @@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -31,11 +30,8 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.concurrent.ExecutionException; -/** - * Created by ashvayka on 19.01.18. - */ -@Slf4j -@RuleNode(type = ComponentType.ENRICHMENT, +@RuleNode( + type = ComponentType.ENRICHMENT, name = "originator fields", configClazz = TbGetOriginatorFieldsConfiguration.class, version = 1, @@ -43,7 +39,9 @@ import java.util.concurrent.ExecutionException; nodeDetails = "Fetches fields values specified in the mapping. If specified field is not part of originator fields it will be ignored. " + "Useful when you need to retrieve originator fields and use them for further message processing.

    " + "Output connections: Success, Failure.", - configDirective = "tbEnrichmentNodeOriginatorFieldsConfig") + configDirective = "tbEnrichmentNodeOriginatorFieldsConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/originator-fields/" +) public class TbGetOriginatorFieldsNode extends TbAbstractGetMappedDataNode { protected final static String DATA_MAPPING_PROPERTY_NAME = "dataMapping"; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java index d6df8123c2..dffcaf35ba 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -31,7 +30,6 @@ import org.thingsboard.server.common.data.util.TbPair; import java.util.Arrays; -@Slf4j @RuleNode( type = ComponentType.ENRICHMENT, name = "related entity data", @@ -42,7 +40,9 @@ import java.util.Arrays; "If multiple related entities are found, only first entity is used for message enrichment, other entities are discarded. " + "Useful when you need to retrieve data from an entity that has a relation to the message originator and use them for further message processing.

    " + "Output connections: Success, Failure.", - configDirective = "tbEnrichmentNodeRelatedAttributesConfig") + configDirective = "tbEnrichmentNodeRelatedAttributesConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/related-entity-data/" +) public class TbGetRelatedAttributeNode extends TbAbstractGetEntityDataNode { private static final String RELATED_ENTITY_NOT_FOUND_MESSAGE = "Failed to find related entity to message originator using relation query specified in the configuration!"; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java index cc6988580a..3102b3b3fd 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java @@ -21,7 +21,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.ListenableFuture; import lombok.Data; import lombok.NoArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.math.NumberUtils; import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.JacksonUtil; @@ -45,11 +44,8 @@ import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -/** - * Created by mshvayka on 04.09.18. - */ -@Slf4j -@RuleNode(type = ComponentType.ENRICHMENT, +@RuleNode( + type = ComponentType.ENRICHMENT, name = "originator telemetry", configClazz = TbGetTelemetryNodeConfiguration.class, version = 2, @@ -58,7 +54,9 @@ import java.util.stream.Collectors; "instead of fetching just the latest telemetry or if you need to get the closest telemetry to the fetch interval start or end. " + "Also, this node can be used for telemetry aggregation within configured fetch interval.

    " + "Output connections: Success, Failure.", - configDirective = "tbEnrichmentNodeGetTelemetryFromDatabase") + configDirective = "tbEnrichmentNodeGetTelemetryFromDatabase", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/originator-telemetry/" +) public class TbGetTelemetryNode implements TbNode { private TbGetTelemetryNodeConfiguration config; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java index 2fa065abf4..cc7327074f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -29,7 +28,6 @@ import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; -@Slf4j @RuleNode( type = ComponentType.ENRICHMENT, name = "tenant attributes", @@ -39,7 +37,9 @@ import org.thingsboard.server.common.data.util.TbPair; nodeDetails = "Useful when you need to retrieve some common configuration or threshold set " + "that is stored as tenant attributes or telemetry data and use it for further message processing.

    " + "Output connections: Success, Failure.", - configDirective = "tbEnrichmentNodeTenantAttributesConfig") + configDirective = "tbEnrichmentNodeTenantAttributesConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/tenant-attributes/" +) public class TbGetTenantAttributeNode extends TbAbstractGetEntityDataNode { @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java index 4808529436..3eafb8d165 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java @@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -30,8 +29,8 @@ import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; -@Slf4j -@RuleNode(type = ComponentType.ENRICHMENT, +@RuleNode( + type = ComponentType.ENRICHMENT, name = "tenant details", configClazz = TbGetTenantDetailsNodeConfiguration.class, version = 1, @@ -39,7 +38,9 @@ import org.thingsboard.server.common.msg.TbMsg; nodeDetails = "Useful when we need to retrieve contact information from your tenant " + "such as email, phone, address, etc., for notifications via email, SMS, and other notification providers.

    " + "Output connections: Success, Failure.", - configDirective = "tbEnrichmentNodeEntityDetailsConfig") + configDirective = "tbEnrichmentNodeEntityDetailsConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/enrichment/tenant-details/" +) public class TbGetTenantDetailsNode extends TbAbstractGetEntityDetailsNode { private static final String TENANT_PREFIX = "tenant_"; diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java index 87643ae46d..e4a1c2c1c4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java @@ -22,7 +22,6 @@ import io.netty.handler.codec.mqtt.MqttQoS; import io.netty.handler.codec.mqtt.MqttVersion; import io.netty.handler.ssl.SslContext; import io.netty.util.concurrent.Promise; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClientConfig; @@ -49,7 +48,6 @@ import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -@Slf4j @RuleNode( type = ComponentType.EXTERNAL, name = "mqtt", @@ -59,7 +57,8 @@ import java.util.concurrent.TimeoutException; nodeDescription = "Publish messages to the MQTT broker", nodeDetails = "Will publish message payload to the MQTT broker with QoS AT_LEAST_ONCE.", configDirective = "tbExternalNodeMqttConfig", - icon = "call_split" + icon = "call_split", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/mqtt/" ) public class TbMqttNode extends TbAbstractExternalNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java index 26c5b3fa42..34d06f7192 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/azure/TbAzureIotHubNode.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.annotations.VisibleForTesting; import io.netty.handler.codec.mqtt.MqttVersion; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.AzureIotHubUtil; import org.thingsboard.mqtt.MqttClient; import org.thingsboard.mqtt.MqttClientConfig; @@ -39,7 +38,6 @@ import org.thingsboard.server.common.data.util.TbPair; import java.time.Clock; -@Slf4j @RuleNode( type = ComponentType.EXTERNAL, name = "azure iot hub", @@ -48,7 +46,8 @@ import java.time.Clock; clusteringMode = ComponentClusteringMode.SINGLETON, nodeDescription = "Publish messages to the Azure IoT Hub", nodeDetails = "Will publish message payload to the Azure IoT Hub with QoS AT_LEAST_ONCE.", - configDirective = "tbExternalNodeAzureIotHubConfig" + configDirective = "tbExternalNodeAzureIotHubConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/azure-iot-hub/" ) public class TbAzureIotHubNode extends TbMqttNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java index 6343191aa2..9838610d5b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbNotificationNode.java @@ -42,7 +42,8 @@ import java.util.concurrent.ExecutionException; nodeDescription = "Sends notification to targets using the template", nodeDetails = "Will send notification to the specified targets using the template", configDirective = "tbExternalNodeNotificationConfig", - icon = "notifications" + icon = "notifications", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/send-notification/" ) public class TbNotificationNode extends TbAbstractExternalNode { @@ -51,7 +52,7 @@ public class TbNotificationNode extends TbAbstractExternalNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { super.init(ctx); - this.config = TbNodeUtils.convert(configuration, TbNotificationNodeConfiguration.class); + config = TbNodeUtils.convert(configuration, TbNotificationNodeConfiguration.class); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbSlackNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbSlackNode.java index 0abbc48974..f6303a51f2 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbSlackNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/notification/TbSlackNode.java @@ -34,7 +34,8 @@ import java.util.concurrent.ExecutionException; nodeDescription = "Send message via Slack", nodeDetails = "Sends message to a Slack channel or user", configDirective = "tbExternalNodeSlackConfig", - iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTYsMTVBMiwyIDAgMCwxIDQsMTdBMiwyIDAgMCwxIDIsMTVBMiwyIDAgMCwxIDQsMTNINlYxNU03LDE1QTIsMiAwIDAsMSA5LDEzQTIsMiAwIDAsMSAxMSwxNVYyMEEyLDIgMCAwLDEgOSwyMkEyLDIgMCAwLDEgNywyMFYxNU05LDdBMiwyIDAgMCwxIDcsNUEyLDIgMCAwLDEgOSwzQTIsMiAwIDAsMSAxMSw1VjdIOU05LDhBMiwyIDAgMCwxIDExLDEwQTIsMiAwIDAsMSA5LDEySDRBMiwyIDAgMCwxIDIsMTBBMiwyIDAgMCwxIDQsOEg5TTE3LDEwQTIsMiAwIDAsMSAxOSw4QTIsMiAwIDAsMSAyMSwxMEEyLDIgMCAwLDEgMTksMTJIMTdWMTBNMTYsMTBBMiwyIDAgMCwxIDE0LDEyQTIsMiAwIDAsMSAxMiwxMFY1QTIsMiAwIDAsMSAxNCwzQTIsMiAwIDAsMSAxNiw1VjEwTTE0LDE4QTIsMiAwIDAsMSAxNiwyMEEyLDIgMCAwLDEgMTQsMjJBMiwyIDAgMCwxIDEyLDIwVjE4SDE0TTE0LDE3QTIsMiAwIDAsMSAxMiwxNUEyLDIgMCAwLDEgMTQsMTNIMTlBMiwyIDAgMCwxIDIxLDE1QTIsMiAwIDAsMSAxOSwxN0gxNFoiIC8+PC9zdmc+" + iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTYsMTVBMiwyIDAgMCwxIDQsMTdBMiwyIDAgMCwxIDIsMTVBMiwyIDAgMCwxIDQsMTNINlYxNU03LDE1QTIsMiAwIDAsMSA5LDEzQTIsMiAwIDAsMSAxMSwxNVYyMEEyLDIgMCAwLDEgOSwyMkEyLDIgMCAwLDEgNywyMFYxNU05LDdBMiwyIDAgMCwxIDcsNUEyLDIgMCAwLDEgOSwzQTIsMiAwIDAsMSAxMSw1VjdIOU05LDhBMiwyIDAgMCwxIDExLDEwQTIsMiAwIDAsMSA5LDEySDRBMiwyIDAgMCwxIDIsMTBBMiwyIDAgMCwxIDQsOEg5TTE3LDEwQTIsMiAwIDAsMSAxOSw4QTIsMiAwIDAsMSAyMSwxMEEyLDIgMCAwLDEgMTksMTJIMTdWMTBNMTYsMTBBMiwyIDAgMCwxIDE0LDEyQTIsMiAwIDAsMSAxMiwxMFY1QTIsMiAwIDAsMSAxNCwzQTIsMiAwIDAsMSAxNiw1VjEwTTE0LDE4QTIsMiAwIDAsMSAxNiwyMEEyLDIgMCAwLDEgMTQsMjJBMiwyIDAgMCwxIDEyLDIwVjE4SDE0TTE0LDE3QTIsMiAwIDAsMSAxMiwxNUEyLDIgMCAwLDEgMTQsMTNIMTlBMiwyIDAgMCwxIDIxLDE1QTIsMiAwIDAsMSAxOSwxN0gxNFoiIC8+PC9zdmc+", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/send-to-slack/" ) public class TbSlackNode extends TbAbstractExternalNode { @@ -43,7 +44,7 @@ public class TbSlackNode extends TbAbstractExternalNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { super.init(ctx); - this.config = TbNodeUtils.convert(configuration, TbSlackNodeConfiguration.class); + config = TbNodeUtils.convert(configuration, TbSlackNodeConfiguration.class); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java index 9a1d04eefa..2bb172bd5b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNode.java @@ -59,7 +59,8 @@ import java.util.concurrent.TimeUnit; nodeDescription = "Process device messages based on device profile settings", nodeDetails = "Create and clear alarms based on alarm rules defined in device profile. The output relation type is either " + "'Alarm Created', 'Alarm Updated', 'Alarm Severity Updated' and 'Alarm Cleared' or simply 'Success' if no alarms were affected.", - configDirective = "tbActionNodeDeviceProfileConfig" + configDirective = "tbActionNodeDeviceProfileConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/device-profile/" ) public class TbDeviceProfileNode implements TbNode { @@ -137,7 +138,7 @@ public class TbDeviceProfileNode implements TbNode { if (deviceState != null) { deviceState.process(ctx, msg); } else { - log.info("Device was not found! Most probably device [" + deviceId + "] has been removed from the database. Acknowledging msg."); + log.info("Device was not found! Most probably device [{}] has been removed from the database. Acknowledging msg.", deviceId); ctx.ack(msg); } } @@ -160,7 +161,7 @@ public class TbDeviceProfileNode implements TbNode { deviceStates.clear(); } - protected DeviceState getOrCreateDeviceState(TbContext ctx, DeviceId deviceId, RuleNodeState rns, boolean printNewlyAddedDeviceStates) { + private DeviceState getOrCreateDeviceState(TbContext ctx, DeviceId deviceId, RuleNodeState rns, boolean printNewlyAddedDeviceStates) { DeviceState deviceState = deviceStates.get(deviceId); if (deviceState == null) { DeviceProfile deviceProfile = cache.get(ctx.getTenantId(), deviceId); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java index a067bb43bd..6ae0f48658 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rabbitmq/TbRabbitMqNode.java @@ -46,7 +46,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; nodeDescription = "Publish messages to the RabbitMQ", nodeDetails = "Will publish message payload to RabbitMQ queue.", configDirective = "tbExternalNodeRabbitMqConfig", - iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZlcnNpb249IjEuMSIgeT0iMHB4IiB4PSIwcHgiIHZpZXdCb3g9IjAgMCAxMDAwIDEwMDAiPjxwYXRoIHN0cm9rZS13aWR0aD0iLjg0OTU2IiBkPSJtODYwLjQ3IDQxNi4zMmgtMjYyLjAxYy0xMi45MTMgMC0yMy42MTgtMTAuNzA0LTIzLjYxOC0yMy42MTh2LTI3Mi43MWMwLTIwLjMwNS0xNi4yMjctMzYuMjc2LTM2LjI3Ni0zNi4yNzZoLTkzLjc5MmMtMjAuMzA1IDAtMzYuMjc2IDE2LjIyNy0zNi4yNzYgMzYuMjc2djI3MC44NGMtMC4yNTQ4NyAxNC4xMDMtMTEuNDY5IDI1LjU3Mi0yNS43NDIgMjUuNTcybC04NS42MzYgMC42Nzk2NWMtMTQuMTAzIDAtMjUuNTcyLTExLjQ2OS0yNS41NzItMjUuNTcybDAuNjc5NjUtMjcxLjUyYzAtMjAuMzA1LTE2LjIyNy0zNi4yNzYtMzYuMjc2LTM2LjI3NmgtOTMuNTM3Yy0yMC4zMDUgMC0zNi4yNzYgMTYuMjI3LTM2LjI3NiAzNi4yNzZ2NzYzLjg0YzAgMTguMDk2IDE0Ljc4MiAzMi40NTMgMzIuNDUzIDMyLjQ1M2g3MjIuODFjMTguMDk2IDAgMzIuNDUzLTE0Ljc4MiAzMi40NTMtMzIuNDUzdi00MzUuMzFjLTEuMTg5NC0xOC4xODEtMTUuMjkyLTMyLjE5OC0zMy4zODgtMzIuMTk4em0tMTIyLjY4IDI4Ny4wN2MwIDIzLjYxOC0xOC44NiA0Mi40NzgtNDIuNDc4IDQyLjQ3OGgtNzMuOTk3Yy0yMy42MTggMC00Mi40NzgtMTguODYtNDIuNDc4LTQyLjQ3OHYtNzQuMjUyYzAtMjMuNjE4IDE4Ljg2LTQyLjQ3OCA0Mi40NzgtNDIuNDc4aDczLjk5N2MyMy42MTggMCA0Mi40NzggMTguODYgNDIuNDc4IDQyLjQ3OHoiLz48L3N2Zz4=" + iconUrl = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZlcnNpb249IjEuMSIgeT0iMHB4IiB4PSIwcHgiIHZpZXdCb3g9IjAgMCAxMDAwIDEwMDAiPjxwYXRoIHN0cm9rZS13aWR0aD0iLjg0OTU2IiBkPSJtODYwLjQ3IDQxNi4zMmgtMjYyLjAxYy0xMi45MTMgMC0yMy42MTgtMTAuNzA0LTIzLjYxOC0yMy42MTh2LTI3Mi43MWMwLTIwLjMwNS0xNi4yMjctMzYuMjc2LTM2LjI3Ni0zNi4yNzZoLTkzLjc5MmMtMjAuMzA1IDAtMzYuMjc2IDE2LjIyNy0zNi4yNzYgMzYuMjc2djI3MC44NGMtMC4yNTQ4NyAxNC4xMDMtMTEuNDY5IDI1LjU3Mi0yNS43NDIgMjUuNTcybC04NS42MzYgMC42Nzk2NWMtMTQuMTAzIDAtMjUuNTcyLTExLjQ2OS0yNS41NzItMjUuNTcybDAuNjc5NjUtMjcxLjUyYzAtMjAuMzA1LTE2LjIyNy0zNi4yNzYtMzYuMjc2LTM2LjI3NmgtOTMuNTM3Yy0yMC4zMDUgMC0zNi4yNzYgMTYuMjI3LTM2LjI3NiAzNi4yNzZ2NzYzLjg0YzAgMTguMDk2IDE0Ljc4MiAzMi40NTMgMzIuNDUzIDMyLjQ1M2g3MjIuODFjMTguMDk2IDAgMzIuNDUzLTE0Ljc4MiAzMi40NTMtMzIuNDUzdi00MzUuMzFjLTEuMTg5NC0xOC4xODEtMTUuMjkyLTMyLjE5OC0zMy4zODgtMzIuMTk4em0tMTIyLjY4IDI4Ny4wN2MwIDIzLjYxOC0xOC44NiA0Mi40NzgtNDIuNDc4IDQyLjQ3OGgtNzMuOTk3Yy0yMy42MTggMC00Mi40NzgtMTguODYtNDIuNDc4LTQyLjQ3OHYtNzQuMjUyYzAtMjMuNjE4IDE4Ljg2LTQyLjQ3OCA0Mi40NzgtNDIuNDc4aDczLjk5N2MyMy42MTggMCA0Mi40NzggMTguODYgNDIuNDc4IDQyLjQ3OHoiLz48L3N2Zz4=", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/rabbitmq/" ) public class TbRabbitMqNode extends TbAbstractExternalNode { @@ -99,10 +100,10 @@ public class TbRabbitMqNode extends TbAbstractExternalNode { } private ListenableFuture publishMessageAsync(TbContext ctx, TbMsg msg) { - return ctx.getExternalCallExecutor().executeAsync(() -> publishMessage(ctx, msg)); + return ctx.getExternalCallExecutor().executeAsync(() -> publishMessage(msg)); } - private TbMsg publishMessage(TbContext ctx, TbMsg msg) throws Exception { + private TbMsg publishMessage(TbMsg msg) throws Exception { String exchangeName = ""; if (!StringUtils.isEmpty(this.config.getExchangeNamePattern())) { exchangeName = TbNodeUtils.processPattern(this.config.getExchangeNamePattern(), msg); @@ -143,23 +144,15 @@ public class TbRabbitMqNode extends TbAbstractExternalNode { } static AMQP.BasicProperties convert(String name) throws TbNodeException { - switch (name) { - case "BASIC": - return MessageProperties.BASIC; - case "TEXT_PLAIN": - return MessageProperties.TEXT_PLAIN; - case "MINIMAL_BASIC": - return MessageProperties.MINIMAL_BASIC; - case "MINIMAL_PERSISTENT_BASIC": - return MessageProperties.MINIMAL_PERSISTENT_BASIC; - case "PERSISTENT_BASIC": - return MessageProperties.PERSISTENT_BASIC; - case "PERSISTENT_TEXT_PLAIN": - return MessageProperties.PERSISTENT_TEXT_PLAIN; - default: - throw new TbNodeException("Undefined message properties type '" + name + - "'! Only " + supportedPropertiesStr + " message properties types are supported!"); - } + return switch (name) { + case "BASIC" -> MessageProperties.BASIC; + case "TEXT_PLAIN" -> MessageProperties.TEXT_PLAIN; + case "MINIMAL_BASIC" -> MessageProperties.MINIMAL_BASIC; + case "MINIMAL_PERSISTENT_BASIC" -> MessageProperties.MINIMAL_PERSISTENT_BASIC; + case "PERSISTENT_BASIC" -> MessageProperties.PERSISTENT_BASIC; + case "PERSISTENT_TEXT_PLAIN" -> MessageProperties.PERSISTENT_TEXT_PLAIN; + default -> throw new TbNodeException("Undefined message properties type '" + name + "'! Only " + supportedPropertiesStr + " message properties types are supported!"); + }; } -} +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java index 8341547ef0..a2290a4eda 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbRestApiCallNode.java @@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.rest; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -30,7 +29,6 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.List; -@Slf4j @RuleNode( type = ComponentType.EXTERNAL, name = "rest api call", @@ -46,7 +44,8 @@ import java.util.List; "
    Note- if you use system proxy properties, the next system proxy properties should be added: \"http.proxyHost\" and \"http.proxyPort\" or \"https.proxyHost\" and \"https.proxyPort\" or \"socksProxyHost\" and \"socksProxyPort\"," + "and if your proxy with auth, the next ones should be added: \"tb.proxy.user\" and \"tb.proxy.password\" to the thingsboard.conf file.", configDirective = "tbExternalNodeRestApiCallConfig", - iconUrl = "data:image/svg+xml;base64,PHN2ZyBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB2ZXJzaW9uPSIxLjEiIHk9IjBweCIgeD0iMHB4Ij48ZyB0cmFuc2Zvcm09Im1hdHJpeCguOTQ5NzUgMCAwIC45NDk3NSAxNy4xMiAyNi40OTIpIj48cGF0aCBkPSJtMTY5LjExIDEwOC41NGMtOS45MDY2IDAuMDczNC0xOS4wMTQgNi41NzI0LTIyLjAxNCAxNi40NjlsLTY5Ljk5MyAyMzEuMDhjLTMuNjkwNCAxMi4xODEgMy4yODkyIDI1LjIyIDE1LjQ2OSAyOC45MSAyLjIyNTkgMC42NzQ4MSA0LjQ5NjkgMSA2LjcyODUgMSA5Ljk3MjEgMCAxOS4xNjUtNi41MTUzIDIyLjE4Mi0xNi40NjdhNi41MjI0IDYuNTIyNCAwIDAgMCAwLjAwMiAtMC4wMDJsNjkuOTktMjMxLjA3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMCAtMC4wMDJjMy42ODU1LTEyLjE4MS0zLjI4Ny0yNS4yMjUtMTUuNDcxLTI4LjkxMi0yLjI4MjUtMC42OTE0NS00LjYxMTYtMS4wMTY5LTYuODk4NC0xem04NC45ODggMGMtOS45MDQ4IDAuMDczNC0xOS4wMTggNi41Njc1LTIyLjAxOCAxNi40NjlsLTY5Ljk4NiAyMzEuMDhjLTMuNjg5OCAxMi4xNzkgMy4yODUzIDI1LjIxNyAxNS40NjUgMjguOTA4IDIuMjI5NyAwLjY3NjQ3IDQuNTAwOCAxLjAwMiA2LjczMjQgMS4wMDIgOS45NzIxIDAgMTkuMTY1LTYuNTE1MyAyMi4xODItMTYuNDY3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMC4wMDIgLTAuMDAybDY5Ljk4OC0yMzEuMDdjMy42OTA4LTEyLjE4MS0zLjI4NTItMjUuMjIzLTE1LjQ2Ny0yOC45MTItMi4yODE0LTAuNjkyMzEtNC42MTA4LTEuMDE4OS02Ljg5ODQtMS4wMDJ6bS0yMTcuMjkgNDIuMjNjLTEyLjcyOS0wLjAwMDg3LTIzLjE4OCAxMC40NTYtMjMuMTg4IDIzLjE4NiAwLjAwMSAxMi43MjggMTAuNDU5IDIzLjE4NiAyMy4xODggMjMuMTg2IDEyLjcyNy0wLjAwMSAyMy4xODMtMTAuNDU5IDIzLjE4NC0yMy4xODYgMC4wMDA4NzYtMTIuNzI4LTEwLjQ1Ni0yMy4xODUtMjMuMTg0LTIzLjE4NnptMCAxNDYuNjRjLTEyLjcyNy0wLjAwMDg3LTIzLjE4NiAxMC40NTUtMjMuMTg4IDIzLjE4NC0wLjAwMDg3MyAxMi43MjkgMTAuNDU4IDIzLjE4OCAyMy4xODggMjMuMTg4IDEyLjcyOC0wLjAwMSAyMy4xODQtMTAuNDYgMjMuMTg0LTIzLjE4OC0wLjAwMS0xMi43MjYtMTAuNDU3LTIzLjE4My0yMy4xODQtMjMuMTg0em0yNzAuNzkgNDIuMjExYy0xMi43MjcgMC0yMy4xODQgMTAuNDU3LTIzLjE4NCAyMy4xODRzMTAuNDU1IDIzLjE4OCAyMy4xODQgMjMuMTg4aDE1NC45OGMxMi43MjkgMCAyMy4xODYtMTAuNDYgMjMuMTg2LTIzLjE4OCAwLjAwMS0xMi43MjgtMTAuNDU4LTIzLjE4NC0yMy4xODYtMjMuMTg0eiIgdHJhbnNmb3JtPSJtYXRyaXgoMS4wMzc2IDAgMCAxLjAzNzYgLTcuNTY3NiAtMTQuOTI1KSIgc3Ryb2tlLXdpZHRoPSIxLjI2OTMiLz48L2c+PC9zdmc+" + iconUrl = "data:image/svg+xml;base64,PHN2ZyBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB2ZXJzaW9uPSIxLjEiIHk9IjBweCIgeD0iMHB4Ij48ZyB0cmFuc2Zvcm09Im1hdHJpeCguOTQ5NzUgMCAwIC45NDk3NSAxNy4xMiAyNi40OTIpIj48cGF0aCBkPSJtMTY5LjExIDEwOC41NGMtOS45MDY2IDAuMDczNC0xOS4wMTQgNi41NzI0LTIyLjAxNCAxNi40NjlsLTY5Ljk5MyAyMzEuMDhjLTMuNjkwNCAxMi4xODEgMy4yODkyIDI1LjIyIDE1LjQ2OSAyOC45MSAyLjIyNTkgMC42NzQ4MSA0LjQ5NjkgMSA2LjcyODUgMSA5Ljk3MjEgMCAxOS4xNjUtNi41MTUzIDIyLjE4Mi0xNi40NjdhNi41MjI0IDYuNTIyNCAwIDAgMCAwLjAwMiAtMC4wMDJsNjkuOTktMjMxLjA3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMCAtMC4wMDJjMy42ODU1LTEyLjE4MS0zLjI4Ny0yNS4yMjUtMTUuNDcxLTI4LjkxMi0yLjI4MjUtMC42OTE0NS00LjYxMTYtMS4wMTY5LTYuODk4NC0xem04NC45ODggMGMtOS45MDQ4IDAuMDczNC0xOS4wMTggNi41Njc1LTIyLjAxOCAxNi40NjlsLTY5Ljk4NiAyMzEuMDhjLTMuNjg5OCAxMi4xNzkgMy4yODUzIDI1LjIxNyAxNS40NjUgMjguOTA4IDIuMjI5NyAwLjY3NjQ3IDQuNTAwOCAxLjAwMiA2LjczMjQgMS4wMDIgOS45NzIxIDAgMTkuMTY1LTYuNTE1MyAyMi4xODItMTYuNDY3YTYuNTIyNCA2LjUyMjQgMCAwIDAgMC4wMDIgLTAuMDAybDY5Ljk4OC0yMzEuMDdjMy42OTA4LTEyLjE4MS0zLjI4NTItMjUuMjIzLTE1LjQ2Ny0yOC45MTItMi4yODE0LTAuNjkyMzEtNC42MTA4LTEuMDE4OS02Ljg5ODQtMS4wMDJ6bS0yMTcuMjkgNDIuMjNjLTEyLjcyOS0wLjAwMDg3LTIzLjE4OCAxMC40NTYtMjMuMTg4IDIzLjE4NiAwLjAwMSAxMi43MjggMTAuNDU5IDIzLjE4NiAyMy4xODggMjMuMTg2IDEyLjcyNy0wLjAwMSAyMy4xODMtMTAuNDU5IDIzLjE4NC0yMy4xODYgMC4wMDA4NzYtMTIuNzI4LTEwLjQ1Ni0yMy4xODUtMjMuMTg0LTIzLjE4NnptMCAxNDYuNjRjLTEyLjcyNy0wLjAwMDg3LTIzLjE4NiAxMC40NTUtMjMuMTg4IDIzLjE4NC0wLjAwMDg3MyAxMi43MjkgMTAuNDU4IDIzLjE4OCAyMy4xODggMjMuMTg4IDEyLjcyOC0wLjAwMSAyMy4xODQtMTAuNDYgMjMuMTg0LTIzLjE4OC0wLjAwMS0xMi43MjYtMTAuNDU3LTIzLjE4My0yMy4xODQtMjMuMTg0em0yNzAuNzkgNDIuMjExYy0xMi43MjcgMC0yMy4xODQgMTAuNDU3LTIzLjE4NCAyMy4xODRzMTAuNDU1IDIzLjE4OCAyMy4xODQgMjMuMTg4aDE1NC45OGMxMi43MjkgMCAyMy4xODYtMTAuNDYgMjMuMTg2LTIzLjE4OCAwLjAwMS0xMi43MjgtMTAuNDU4LTIzLjE4NC0yMy4xODYtMjMuMTg0eiIgdHJhbnNmb3JtPSJtYXRyaXgoMS4wMzc2IDAgMCAxLjAzNzYgLTcuNTY3NiAtMTQuOTI1KSIgc3Ryb2tlLXdpZHRoPSIxLjI2OTMiLz48L2c+PC9zdmc+", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/rest-api-call/" ) public class TbRestApiCallNode extends TbAbstractExternalNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNode.java index 5be2768a0f..8148e5ea58 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rest/TbSendRestApiCallReplyNode.java @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.rest; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; @@ -28,7 +27,6 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.UUID; -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "rest call reply", @@ -36,7 +34,8 @@ import java.util.UUID; nodeDescription = "Sends reply to REST API call to rule engine", nodeDetails = "Expects messages with any message type. Forwards incoming message as a reply to REST API call sent to rule engine.", configDirective = "tbActionNodeSendRestApiCallReplyConfig", - icon = "call_merge" + icon = "call_merge", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/rest-call-reply/" ) public class TbSendRestApiCallReplyNode implements TbNode { @@ -44,7 +43,7 @@ public class TbSendRestApiCallReplyNode implements TbNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbSendRestApiCallReplyNodeConfiguration.class); + config = TbNodeUtils.convert(configuration, TbSendRestApiCallReplyNodeConfiguration.class); } @Override @@ -62,4 +61,5 @@ public class TbSendRestApiCallReplyNode implements TbNode { ctx.tellSuccess(msg); } } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java index a8e7142cfb..0f136772d4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCReplyNode.java @@ -19,7 +19,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import lombok.extern.slf4j.Slf4j; +import org.checkerframework.checker.nullness.qual.NonNull; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -41,7 +41,6 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.UUID; -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "rpc call reply", @@ -49,7 +48,8 @@ import java.util.UUID; nodeDescription = "Sends reply to RPC call from device", nodeDetails = "Expects messages with any message type. Will forward message body to the device.", configDirective = "tbActionNodeRpcReplyConfig", - icon = "call_merge" + icon = "call_merge", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/rpc-call-reply/" ) public class TbSendRPCReplyNode implements TbNode { @@ -57,7 +57,7 @@ public class TbSendRPCReplyNode implements TbNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbSendRpcReplyNodeConfiguration.class); + config = TbNodeUtils.convert(configuration, TbSendRpcReplyNodeConfiguration.class); } @Override @@ -103,7 +103,7 @@ public class TbSendRPCReplyNode implements TbNode { body.put("requestId", requestIdStr); body.put("response", msg.getData()); EdgeEvent edgeEvent = EdgeUtils.constructEdgeEvent(ctx.getTenantId(), edgeId, EdgeEventType.DEVICE, - EdgeEventActionType.RPC_CALL, deviceId, JacksonUtil.valueToTree(body)); + EdgeEventActionType.RPC_CALL, deviceId, JacksonUtil.valueToTree(body)); ListenableFuture future = ctx.getEdgeEventService().saveAsync(edgeEvent); Futures.addCallback(future, new FutureCallback<>() { @Override @@ -112,9 +112,10 @@ public class TbSendRPCReplyNode implements TbNode { } @Override - public void onFailure(Throwable t) { + public void onFailure(@NonNull Throwable t) { ctx.tellFailure(msg, t); } }, ctx.getDbCallbackExecutor()); } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java index 51f5166653..ead79a3793 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java @@ -20,7 +20,6 @@ import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleEngineDeviceRpcRequest; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -41,7 +40,6 @@ import java.util.Random; import java.util.UUID; import java.util.concurrent.TimeUnit; -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "rpc call request", @@ -50,17 +48,18 @@ import java.util.concurrent.TimeUnit; nodeDetails = "Expects messages with \"method\" and \"params\". Will forward response from device to next nodes." + "If the RPC call request is originated by REST API call from user, will forward the response to user immediately.", configDirective = "tbActionNodeRpcRequestConfig", - icon = "call_made" + icon = "call_made", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/rpc-call-request/" ) public class TbSendRPCRequestNode implements TbNode { - private Random random = new Random(); - private Gson gson = new Gson(); + private final Random random = new Random(); + private final Gson gson = new Gson(); private TbSendRpcRequestNodeConfiguration config; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbSendRpcRequestNodeConfiguration.class); + config = TbNodeUtils.convert(configuration, TbSendRpcRequestNodeConfiguration.class); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNode.java index d5583037ac..3033c9d382 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/sms/TbSendSmsNode.java @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.sms; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -28,7 +27,6 @@ import org.thingsboard.server.common.msg.TbMsg; import static org.thingsboard.common.util.DonAsynchron.withCallback; -@Slf4j @RuleNode( type = ComponentType.EXTERNAL, name = "send sms", @@ -36,7 +34,8 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback; nodeDescription = "Sends SMS message via SMS provider.", nodeDetails = "Will send SMS message by populating target phone numbers and sms message fields using values derived from message metadata.", configDirective = "tbExternalNodeSendSmsConfig", - icon = "sms" + icon = "sms", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/external/send-sms/" ) public class TbSendSmsNode extends TbAbstractExternalNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java index e703e9dd25..c819b98466 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbCalculatedFieldsNode.java @@ -16,16 +16,13 @@ package org.thingsboard.rule.engine.telemetry; import com.google.gson.JsonParser; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.AttributesSaveRequest; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; -import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TimeseriesSaveRequest; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.adaptor.JsonConverter; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.kv.AttributeKvEntry; @@ -41,7 +38,6 @@ import java.util.Map; import static org.thingsboard.server.common.data.DataConstants.SCOPE; -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "calculated fields", @@ -52,16 +48,13 @@ import static org.thingsboard.server.common.data.DataConstants.SCOPE; "This rule node accepts the same messages as these nodes but allows you to trigger the processing of calculated " + "fields independently, ensuring that derived data can be computed and utilized in real time without storing the original message in the database.", configDirective = "tbNodeEmptyConfig", - icon = "published_with_changes" + icon = "published_with_changes", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/calculated-fields/" ) public class TbCalculatedFieldsNode implements TbNode { - private EmptyNodeConfiguration config; - @Override - public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class); - } + public void init(TbContext ctx, TbNodeConfiguration configuration) {} @Override public void onMsg(TbContext ctx, TbMsg msg) { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java index 280d8de824..533f7d13dd 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgAttributesNode.java @@ -21,7 +21,6 @@ import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import com.google.gson.JsonParser; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.DonAsynchron; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.AttributesSaveRequest; @@ -56,7 +55,6 @@ import static org.thingsboard.server.common.data.DataConstants.NOTIFY_DEVICE_MET import static org.thingsboard.server.common.data.DataConstants.SCOPE; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_REQUEST; -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "save attributes", @@ -107,7 +105,8 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.POST_ATTRIBUTES_R Output connections: Success, Failure. """, configDirective = "tbActionNodeAttributesConfig", - icon = "file_upload" + icon = "file_upload", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/save-attributes/" ) public class TbMsgAttributesNode implements TbNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgDeleteAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgDeleteAttributesNode.java index 32d700dc1d..331c28b686 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgDeleteAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgDeleteAttributesNode.java @@ -15,7 +15,6 @@ */ package org.thingsboard.rule.engine.telemetry; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.rule.engine.api.AttributesDeleteRequest; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -35,7 +34,6 @@ import java.util.stream.Collectors; import static org.thingsboard.server.common.data.DataConstants.NOTIFY_DEVICE_METADATA_KEY; import static org.thingsboard.server.common.data.DataConstants.SCOPE; -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "delete attributes", @@ -46,7 +44,8 @@ import static org.thingsboard.server.common.data.DataConstants.SCOPE; " rule node will send the \"Attributes Deleted\" event to the root chain of the message originator and " + " send the incoming message via Success chain, otherwise, Failure chain is used.", configDirective = "tbActionNodeDeleteAttributesConfig", - icon = "remove_circle" + icon = "remove_circle", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/delete-attributes/" ) public class TbMsgDeleteAttributesNode implements TbNode { @@ -55,8 +54,8 @@ public class TbMsgDeleteAttributesNode implements TbNode { @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbMsgDeleteAttributesNodeConfiguration.class); - this.keys = config.getKeys(); + config = TbNodeUtils.convert(configuration, TbMsgDeleteAttributesNodeConfiguration.class); + keys = config.getKeys(); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java index 8017be9a04..13dab98c54 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/telemetry/TbMsgTimeseriesNode.java @@ -18,7 +18,6 @@ package org.thingsboard.rule.engine.telemetry; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.gson.JsonParser; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -52,7 +51,6 @@ import static org.thingsboard.rule.engine.telemetry.settings.TimeseriesProcessin import static org.thingsboard.rule.engine.telemetry.settings.TimeseriesProcessingSettings.WebSocketsOnly; import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; -@Slf4j @RuleNode( type = ComponentType.ACTION, name = "save time series", @@ -103,7 +101,8 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_RE """, configDirective = "tbActionNodeTimeseriesConfig", icon = "file_upload", - version = 1 + version = 1, + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/action/save-timeseries/" ) public class TbMsgTimeseriesNode implements TbNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java index eff08d09e7..e3ab60a6e0 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java @@ -55,7 +55,8 @@ import static org.thingsboard.rule.engine.transform.OriginatorSource.RELATED; "'Device', 'Asset', 'Entity View', 'Edge' or 'User'." + "Output connections: Success, Failure.", configDirective = "tbTransformationNodeChangeOriginatorConfig", - icon = "find_replace" + icon = "find_replace", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/change-originator/" ) public class TbChangeOriginatorNode extends TbAbstractTransformNode { diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java index 28864ad2db..d9e821e77f 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java @@ -47,22 +47,22 @@ import java.util.stream.Collectors; "Regular expressions can be used to define which keys-value pairs to copy. Any configured key not found in the source will be ignored.

    " + "Output connections: Success, Failure.", configDirective = "tbTransformationNodeCopyKeysConfig", - icon = "content_copy" + icon = "content_copy", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/copy-key-value-pairs/" ) public class TbCopyKeysNode extends TbAbstractTransformNodeWithTbMsgSource { - private TbCopyKeysNodeConfiguration config; private TbMsgSource copyFrom; private List compiledKeyPatterns; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbCopyKeysNodeConfiguration.class); - this.copyFrom = config.getCopyFrom(); + var config = TbNodeUtils.convert(configuration, TbCopyKeysNodeConfiguration.class); + copyFrom = config.getCopyFrom(); if (copyFrom == null) { throw new TbNodeException("CopyFrom can't be null! Allowed values: " + Arrays.toString(TbMsgSource.values())); } - this.compiledKeyPatterns = config.getKeys().stream().map(Pattern::compile).collect(Collectors.toList()); + compiledKeyPatterns = config.getKeys().stream().map(Pattern::compile).collect(Collectors.toList()); } @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java index 91f16f0b3b..c2eead99fe 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java @@ -47,22 +47,22 @@ import java.util.stream.Collectors; "keys and/or regular expressions.

    " + "Output connections: Success, Failure.", configDirective = "tbTransformationNodeDeleteKeysConfig", - icon = "remove_circle" + icon = "remove_circle", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/delete-key-value-pairs/" ) public class TbDeleteKeysNode extends TbAbstractTransformNodeWithTbMsgSource { - private TbDeleteKeysNodeConfiguration config; private TbMsgSource deleteFrom; private List compiledKeyPatterns; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbDeleteKeysNodeConfiguration.class); - this.deleteFrom = config.getDeleteFrom(); + var config = TbNodeUtils.convert(configuration, TbDeleteKeysNodeConfiguration.class); + deleteFrom = config.getDeleteFrom(); if (deleteFrom == null) { throw new TbNodeException("DeleteFrom can't be null! Allowed values: " + Arrays.toString(TbMsgSource.values())); } - this.compiledKeyPatterns = config.getKeys().stream().map(Pattern::compile).collect(Collectors.toList()); + compiledKeyPatterns = config.getKeys().stream().map(Pattern::compile).collect(Collectors.toList()); } @Override @@ -76,7 +76,7 @@ public class TbDeleteKeysNode extends TbAbstractTransformNodeWithTbMsgSource { var mdKeysToDelete = metaDataMap.keySet() .stream() .filter(this::matches) - .collect(Collectors.toList()); + .toList(); mdKeysToDelete.forEach(metaDataMap::remove); metaDataCopy = new TbMsgMetaData(metaDataMap); hasNoChanges = mdKeysToDelete.isEmpty(); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java index eb6c0b7a68..717bdf70f2 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java @@ -19,7 +19,6 @@ import com.jayway.jsonpath.Configuration; import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.PathNotFoundException; import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; @@ -32,7 +31,6 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.concurrent.ExecutionException; -@Slf4j @RuleNode( type = ComponentType.TRANSFORMATION, name = "json path", @@ -41,32 +39,32 @@ import java.util.concurrent.ExecutionException; nodeDetails = "JSONPath expression specifies a path to an element or a set of elements in a JSON structure.

    " + "Output connections: Success, Failure.", icon = "functions", - configDirective = "tbTransformationNodeJsonPathConfig" + configDirective = "tbTransformationNodeJsonPathConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/json-path/" ) public class TbJsonPathNode implements TbNode { - private TbJsonPathNodeConfiguration config; private Configuration configurationJsonPath; private JsonPath jsonPath; private String jsonPathValue; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbJsonPathNodeConfiguration.class); - this.jsonPathValue = config.getJsonPath(); - if (!TbJsonPathNodeConfiguration.DEFAULT_JSON_PATH.equals(this.jsonPathValue)) { - this.configurationJsonPath = Configuration.builder() + var config = TbNodeUtils.convert(configuration, TbJsonPathNodeConfiguration.class); + jsonPathValue = config.getJsonPath(); + if (!TbJsonPathNodeConfiguration.DEFAULT_JSON_PATH.equals(jsonPathValue)) { + configurationJsonPath = Configuration.builder() .jsonProvider(new JacksonJsonNodeJsonProvider()) .build(); - this.jsonPath = JsonPath.compile(config.getJsonPath()); + jsonPath = JsonPath.compile(config.getJsonPath()); } } @Override public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { - if (!TbJsonPathNodeConfiguration.DEFAULT_JSON_PATH.equals(this.jsonPathValue)) { + if (!TbJsonPathNodeConfiguration.DEFAULT_JSON_PATH.equals(jsonPathValue)) { try { - Object jsonPathData = jsonPath.read(msg.getData(), this.configurationJsonPath); + Object jsonPathData = jsonPath.read(msg.getData(), configurationJsonPath); ctx.tellSuccess(msg.transform() .data(JacksonUtil.toString(jsonPathData)) .build()); @@ -77,4 +75,5 @@ public class TbJsonPathNode implements TbNode { ctx.tellSuccess(msg); } } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java index f2865bfc0b..9966ff6bb4 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java @@ -44,19 +44,19 @@ import java.util.concurrent.ExecutionException; "If key to rename doesn't exist in the specified source (message or message metadata) it will be ignored.

    " + "Output connections: Success, Failure.", configDirective = "tbTransformationNodeRenameKeysConfig", - icon = "find_replace" + icon = "find_replace", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/rename-keys/" ) public class TbRenameKeysNode extends TbAbstractTransformNodeWithTbMsgSource { - private TbRenameKeysNodeConfiguration config; private Map renameKeysMapping; private TbMsgSource renameIn; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, TbRenameKeysNodeConfiguration.class); - this.renameIn = config.getRenameIn(); - this.renameKeysMapping = config.getRenameKeysMapping(); + var config = TbNodeUtils.convert(configuration, TbRenameKeysNodeConfiguration.class); + renameIn = config.getRenameIn(); + renameKeysMapping = config.getRenameKeysMapping(); if (renameIn == null) { throw new TbNodeException("RenameIn can't be null! Allowed values: " + Arrays.toString(TbMsgSource.values())); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java index 0eac8b073d..a1d94782fc 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java @@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.transform; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; -import lombok.extern.slf4j.Slf4j; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.EmptyNodeConfiguration; import org.thingsboard.rule.engine.api.RuleNode; @@ -25,7 +24,6 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNode; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.msg.TbNodeConnectionType; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; @@ -34,7 +32,6 @@ import org.thingsboard.server.common.msg.queue.TbMsgCallback; import java.util.concurrent.ExecutionException; -@Slf4j @RuleNode( type = ComponentType.TRANSFORMATION, name = "split array msg", @@ -44,16 +41,13 @@ import java.util.concurrent.ExecutionException; "All outbound messages will have the same type and metadata as the original array message.

    " + "Output connections: Success, Failure.", icon = "content_copy", - configDirective = "tbNodeEmptyConfig" + configDirective = "tbNodeEmptyConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/split-array-msg/" ) public class TbSplitArrayMsgNode implements TbNode { - private EmptyNodeConfiguration config; - @Override - public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { - this.config = TbNodeUtils.convert(configuration, EmptyNodeConfiguration.class); - } + public void init(TbContext ctx, TbNodeConfiguration configuration) {} @Override public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { @@ -89,4 +83,5 @@ public class TbSplitArrayMsgNode implements TbNode { ctx.tellFailure(msg, new RuntimeException("Msg data is not a JSON Array!")); } } + } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java index 9373149cf5..487d92a2b0 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java @@ -41,7 +41,8 @@ import java.util.List; "{ msg: new payload,
       metadata: new metadata,
       msgType: new msgType }

    " + "All fields in resulting object are optional and will be taken from original message if not specified.

    " + "Output connections: Success, Failure.", - configDirective = "tbTransformationNodeScriptConfig" + configDirective = "tbTransformationNodeScriptConfig", + docUrl = "https://thingsboard.io/docs/user-guide/rule-engine-2-0/nodes/transformation/script/" ) public class TbTransformMsgNode extends TbAbstractTransformNode { @@ -71,4 +72,5 @@ public class TbTransformMsgNode extends TbAbstractTransformNode entityStates; + private ConcurrentMap entityStates; private boolean msgInside; private boolean reportPresenceStatusOnEachMessage; @@ -33,7 +33,8 @@ public class GpsGeofencingActionTestCase { this.entityId = entityId; this.msgInside = msgInside; this.reportPresenceStatusOnEachMessage = reportPresenceStatusOnEachMessage; - this.entityStates = new HashMap<>(); + this.entityStates = new ConcurrentHashMap<>(); this.entityStates.put(entityId, entityGeofencingState); } + } diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 76a501cc63..34deb3a7d5 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -94,76 +94,83 @@ export const HelpLinks = { oauth2Apple: 'https://developer.apple.com/sign-in-with-apple/get-started/', oauth2Facebook: 'https://developers.facebook.com/docs/facebook-login/web#logindialog', oauth2Github: 'https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app', - oauth2Google: 'https://developers.google.com/google-ads/api/docs/start', + oauth2Google: 'https://developers.google.com/identity/protocols/oauth2', ruleEngine: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/overview/`, - ruleNodeCheckRelation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#check-relation-filter-node`, - ruleNodeCheckExistenceFields: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#check-existence-fields-node`, - ruleNodeGpsGeofencingFilter: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#gps-geofencing-filter-node`, - ruleNodeJsFilter: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#script-filter-node`, - ruleNodeJsSwitch: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#switch-node`, - ruleNodeAssetProfileSwitch: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#asset-profile-switch`, - ruleNodeDeviceProfileSwitch: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#device-profile-switch`, - ruleNodeCheckAlarmStatus: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#check-alarm-status`, - ruleNodeMessageTypeFilter: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#message-type-filter-node`, - ruleNodeMessageTypeSwitch: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#message-type-switch-node`, - ruleNodeOriginatorTypeFilter: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#originator-type-filter-node`, - ruleNodeOriginatorTypeSwitch: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/filter-nodes/#originator-type-switch-node`, - ruleNodeOriginatorAttributes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#originator-attributes`, - ruleNodeOriginatorFields: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#originator-fields`, - ruleNodeOriginatorTelemetry: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#originator-telemetry`, - ruleNodeCustomerAttributes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#customer-attributes`, - ruleNodeCustomerDetails: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#customer-details`, - ruleNodeFetchDeviceCredentials: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#fetch-device-credentials`, - ruleNodeDeviceAttributes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#device-attributes`, - ruleNodeRelatedAttributes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#related-attributes`, - ruleNodeTenantAttributes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#tenant-attributes`, - ruleNodeTenantDetails: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#tenant-details`, - ruleNodeChangeOriginator: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/transformation-nodes/#change-originator`, - ruleNodeTransformMsg: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/transformation-nodes/#script-transformation-node`, - ruleNodeMsgToEmail: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/transformation-nodes/#to-email-node`, - ruleNodeAssignToCustomer: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#assign-to-customer-node`, - ruleNodeUnassignFromCustomer: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#unassign-from-customer-node`, - ruleNodeCalculatedFields: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#calculated-fields-node`, - ruleNodeClearAlarm: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#clear-alarm-node`, - ruleNodeCreateAlarm: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#create-alarm-node`, - ruleNodeCopyToView: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#copy-to-view-node`, - ruleNodeCreateRelation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#create-relation-node`, - ruleNodeDeleteRelation: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#delete-relation-node`, - ruleNodeDeviceState: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#device-state-node`, - ruleNodeMessageCount: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#message-count-node`, - ruleNodeMsgDelay: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#delay-node-deprecated`, - ruleNodeMsgGenerator: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#generator-node`, - ruleNodeGpsGeofencingEvents: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#gps-geofencing-events-node`, - ruleNodeLog: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#log-node`, - ruleNodeRpcCallReply: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#rpc-call-reply-node`, - ruleNodeRpcCallRequest: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#rpc-call-request-node`, - ruleNodeSaveAttributes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#save-attributes-node`, - ruleNodeDeleteAttributes: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#delete-attributes-node`, - ruleNodeSaveTimeseries: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#save-timeseries-node`, - ruleNodeSaveToCustomTable: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#save-to-custom-table-node`, - ruleNodeRuleChain: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/flow-nodes/#rule-chain-node`, - ruleNodeOutputNode: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/flow-nodes/#output-node`, - ruleNodeAiRequest: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#ai-request-node`, - ruleNodeAwsLambda: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#aws-lambda-node`, - ruleNodeAwsSns: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#aws-sns-node`, - ruleNodeAwsSqs: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#aws-sqs-node`, - ruleNodeKafka: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#kafka-node`, - ruleNodeMqtt: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#mqtt-node`, - ruleNodeAzureIotHub: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#azure-iot-hub-node`, - ruleNodeRabbitMq: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#rabbitmq-node`, - ruleNodeRestApiCall: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#rest-api-call-node`, - ruleNodeSendEmail: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#send-email-node`, - ruleNodeSendSms: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#send-sms-node`, - ruleNodeMath: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#math-function-node`, - ruleNodeCalculateDelta: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/enrichment-nodes/#calculate-delta`, - ruleNodeRestCallReply: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#rest-call-reply-node`, - ruleNodePushToCloud: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#push-to-cloud`, - ruleNodePushToEdge: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#push-to-edge`, - ruleNodeDeviceProfile: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/action-nodes/#device-profile-node`, - ruleNodeAcknowledge: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/flow-nodes/#acknowledge-node`, - ruleNodeCheckpoint: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/flow-nodes/#checkpoint-node`, - ruleNodeSendNotification: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#send-notification-node`, - ruleNodeSendSlack: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/rule-engine-2-0/external-nodes/#send-to-slack-node`, + ruleNodeCheckRelation: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/check-relation-presence/`, + ruleNodeCheckExistenceFields: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/check-fields-presence/`, + ruleNodeGpsGeofencingFilter: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/gps-geofencing-filter/`, + ruleNodeJsFilter: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/script/`, + ruleNodeJsSwitch: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/switch/`, + ruleNodeAssetProfileSwitch: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/asset-profile-switch/`, + ruleNodeDeviceProfileSwitch: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/device-profile-switch/`, + ruleNodeCheckAlarmStatus: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/alarm-status-filter/`, + ruleNodeMessageTypeFilter: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/message-type-filter/`, + ruleNodeMessageTypeSwitch: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/message-type-switch/`, + ruleNodeOriginatorTypeFilter: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/entity-type-filter/`, + ruleNodeOriginatorTypeSwitch: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/filter/entity-type-switch/`, + ruleNodeOriginatorAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/originator-attributes/`, + ruleNodeOriginatorFields: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/originator-fields/`, + ruleNodeOriginatorTelemetry: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/originator-telemetry/`, + ruleNodeCustomerAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/customer-attributes/`, + ruleNodeCustomerDetails: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/customer-details/`, + ruleNodeFetchDeviceCredentials: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/fetch-device-credentials/`, + ruleNodeDeviceAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/related-device-attributes/`, + ruleNodeRelatedAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/related-entity-data/`, + ruleNodeTenantAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/tenant-attributes/`, + ruleNodeTenantDetails: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/tenant-details/`, + ruleNodeChangeOriginator: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/change-originator/`, + ruleNodeCopyKeyValuePairs: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/copy-key-value-pairs/`, + ruleNodeDeduplication: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/deduplication/`, + ruleNodeDeleteKeyValuePairs: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/delete-key-value-pairs/`, + ruleNodeJsonPath: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/json-path/`, + ruleNodeRenameKeys: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/rename-keys/`, + ruleNodeTransformMsg: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/script/`, + ruleNodeSplitArrayMsg: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/split-array-msg/`, + ruleNodeMsgToEmail: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/transformation/to-email/`, + ruleNodeAssignToCustomer: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/assign-to-customer/`, + ruleNodeUnassignFromCustomer: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/unassign-from-customer/`, + ruleNodeCalculatedFields: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/calculated-fields/`, + ruleNodeClearAlarm: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/clear-alarm/`, + ruleNodeCreateAlarm: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/create-alarm/`, + ruleNodeCopyToView: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/copy-to-view/`, + ruleNodeCreateRelation: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/create-relation/`, + ruleNodeDeleteRelation: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/delete-relation/`, + ruleNodeDeviceState: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/device-state/`, + ruleNodeMessageCount: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/message-count/`, + ruleNodeMsgDelay: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/delay/`, + ruleNodeMsgGenerator: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/generator/`, + ruleNodeGpsGeofencingEvents: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/gps-geofencing-events/`, + ruleNodeLog: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/log/`, + ruleNodeRpcCallReply: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/rpc-call-reply/`, + ruleNodeRpcCallRequest: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/rpc-call-request/`, + ruleNodeSaveAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/save-attributes/`, + ruleNodeDeleteAttributes: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/delete-attributes/`, + ruleNodeSaveTimeseries: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/save-timeseries/`, + ruleNodeSaveToCustomTable: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/save-to-custom-table/`, + ruleNodeRuleChain: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/flow/rule-chain/`, + ruleNodeOutputNode: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/flow/output/`, + ruleNodeAiRequest: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/ai-request/`, + ruleNodeAwsLambda: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/aws-lambda/`, + ruleNodeAwsSns: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/aws-sns/`, + ruleNodeAwsSqs: `${helpBaseUrl}docs/user-guide/rule-engine-2-0/nodes/external/aws-sqs/`, + ruleNodeKafka: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/kafka/`, + ruleNodeMqtt: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/mqtt/`, + ruleNodeAzureIotHub: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/azure-iot-hub/`, + ruleNodeGcpPubSub: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/gcp-pubsub/`, + ruleNodeRabbitMq: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/rabbitmq/`, + ruleNodeRestApiCall: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/rest-api-call/`, + ruleNodeSendEmail: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/send-email/`, + ruleNodeSendSms: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/send-sms/`, + ruleNodeMath: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/math-function/`, + ruleNodeCalculateDelta: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/enrichment/calculate-delta/`, + ruleNodeRestCallReply: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/rest-call-reply/`, + ruleNodePushToCloud: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/push-to-cloud/`, + ruleNodePushToEdge: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/push-to-edge/`, + ruleNodeDeviceProfile: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/action/device-profile/`, + ruleNodeAcknowledge: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/flow/acknowledge/`, + ruleNodeCheckpoint: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/flow/checkpoint/`, + ruleNodeSendNotification: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/send-notification/`, + ruleNodeSendSlack: `${helpBaseUrl}docs/user-guide/rule-engine-2-0/nodes/external/send-to-slack/`, tenants: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/tenants`, tenantProfiles: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/tenant-profiles`, customers: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/customers`, diff --git a/ui-ngx/src/app/shared/models/rule-node.models.ts b/ui-ngx/src/app/shared/models/rule-node.models.ts index 8225fbf18f..786fd72448 100644 --- a/ui-ngx/src/app/shared/models/rule-node.models.ts +++ b/ui-ngx/src/app/shared/models/rule-node.models.ts @@ -476,7 +476,13 @@ const ruleNodeClazzHelpLinkMap = { 'org.thingsboard.rule.engine.metadata.TbGetTenantDetailsNode': 'ruleNodeTenantDetails', 'org.thingsboard.rule.engine.metadata.CalculateDeltaNode': 'ruleNodeCalculateDelta', 'org.thingsboard.rule.engine.transform.TbChangeOriginatorNode': 'ruleNodeChangeOriginator', + 'org.thingsboard.rule.engine.transform.TbCopyKeysNode': 'ruleNodeCopyKeyValuePairs', + 'org.thingsboard.rule.engine.deduplication.TbMsgDeduplicationNode': 'ruleNodeDeduplication', + 'org.thingsboard.rule.engine.transform.TbDeleteKeysNode': 'ruleNodeDeleteKeyValuePairs', + 'org.thingsboard.rule.engine.transform.TbJsonPathNode': 'ruleNodeJsonPath', + 'org.thingsboard.rule.engine.transform.TbRenameKeysNode': 'ruleNodeRenameKeys', 'org.thingsboard.rule.engine.transform.TbTransformMsgNode': 'ruleNodeTransformMsg', + 'org.thingsboard.rule.engine.transform.TbSplitArrayMsgNode': 'ruleNodeSplitArrayMsg', 'org.thingsboard.rule.engine.mail.TbMsgToEmailNode': 'ruleNodeMsgToEmail', 'org.thingsboard.rule.engine.action.TbAssignToCustomerNode': 'ruleNodeAssignToCustomer', 'org.thingsboard.rule.engine.action.TbUnassignFromCustomerNode': 'ruleNodeUnassignFromCustomer', @@ -505,6 +511,7 @@ const ruleNodeClazzHelpLinkMap = { 'org.thingsboard.rule.engine.kafka.TbKafkaNode': 'ruleNodeKafka', 'org.thingsboard.rule.engine.mqtt.TbMqttNode': 'ruleNodeMqtt', 'org.thingsboard.rule.engine.mqtt.azure.TbAzureIotHubNode': 'ruleNodeAzureIotHub', + 'org.thingsboard.rule.engine.gcp.pubsub.TbPubSubNode': 'ruleNodeGcpPubSub', 'org.thingsboard.rule.engine.rabbitmq.TbRabbitMqNode': 'ruleNodeRabbitMq', 'org.thingsboard.rule.engine.rest.TbRestApiCallNode': 'ruleNodeRestApiCall', 'org.thingsboard.rule.engine.mail.TbSendEmailNode': 'ruleNodeSendEmail', From e5c595ee3935344bccfe3774ecebe381480d74bd Mon Sep 17 00:00:00 2001 From: Dmytro Skarzhynets Date: Mon, 6 Oct 2025 20:21:25 +0300 Subject: [PATCH 200/216] Fix links for "send to slack" and "aws sqs" nodes --- ui-ngx/src/app/shared/models/constants.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 34deb3a7d5..6047384ec3 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -152,7 +152,7 @@ export const HelpLinks = { ruleNodeAiRequest: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/ai-request/`, ruleNodeAwsLambda: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/aws-lambda/`, ruleNodeAwsSns: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/aws-sns/`, - ruleNodeAwsSqs: `${helpBaseUrl}docs/user-guide/rule-engine-2-0/nodes/external/aws-sqs/`, + ruleNodeAwsSqs: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/aws-sqs/`, ruleNodeKafka: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/kafka/`, ruleNodeMqtt: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/mqtt/`, ruleNodeAzureIotHub: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/azure-iot-hub/`, @@ -170,7 +170,7 @@ export const HelpLinks = { ruleNodeAcknowledge: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/flow/acknowledge/`, ruleNodeCheckpoint: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/flow/checkpoint/`, ruleNodeSendNotification: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/send-notification/`, - ruleNodeSendSlack: `${helpBaseUrl}docs/user-guide/rule-engine-2-0/nodes/external/send-to-slack/`, + ruleNodeSendSlack: `${helpBaseUrl}/docs/user-guide/rule-engine-2-0/nodes/external/send-to-slack/`, tenants: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/tenants`, tenantProfiles: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/tenant-profiles`, customers: `${helpBaseUrl}/docs${docPlatformPrefix}/user-guide/ui/customers`, From 6cfa3f1b0b6b4a699f1df94f4528322b8199d107 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 7 Oct 2025 12:22:57 +0300 Subject: [PATCH 201/216] fixed device session closing: should close every device session if message id wrong --- .../AbstractGatewaySessionHandler.java | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java index 168fef6c2e..fee2f04e24 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java @@ -845,27 +845,31 @@ public abstract class AbstractGatewaySessionHandler Date: Tue, 7 Oct 2025 17:00:01 +0300 Subject: [PATCH 202/216] Support of displayName entity field --- .../controller/EntityQueryControllerTest.java | 155 +++++++++++++++++- .../dao/sql/query/EntityKeyMapping.java | 27 ++- 2 files changed, 173 insertions(+), 9 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 93d67ccd4b..0c40c2f868 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -77,6 +77,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; @@ -1068,6 +1069,119 @@ public class EntityQueryControllerTest extends AbstractControllerTest { countByQueryAndCheck(customerEntitiesQuery, 0); } + @Test + public void testFindDevicesByDisplayName() throws Exception { + loginTenantAdmin(); + int numOfDevices = 3; + + for (int i = 0; i < numOfDevices; i++) { + Device device = new Device(); + String name = "Device" + i; + device.setName(name); + device.setLabel("Device Label " + i); + device.setType("testFindDevicesByDisplayName"); + + Device savedDevice = doPost("/api/device?accessToken=" + name, device, Device.class); + } + + DeviceTypeFilter filter = new DeviceTypeFilter(); + filter.setDeviceTypes(List.of("testFindDevicesByDisplayName")); + filter.setDeviceNameFilter(""); + + KeyFilter displayNameFilter = getEntityFieldEqualFilter("displayName", "Device Label " + 0); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = List.of(new EntityKey(EntityKeyType.ENTITY_FIELD, "name"), new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName")); + + // all devices with ownerName = TEST TENANT + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), Collections.emptyList()); + checkEntitiesByQuery(query, numOfDevices, (i, entity) -> { + String name = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("name", new TsValue(0, "Invalid")).getValue(); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("Device" + i, name); + Assert.assertEquals("Device Label " + i, displayName); + }); + + // all devices with ownerName = TEST TENANT + EntityDataQuery displayNameFilterQuery = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(displayNameFilter)); + checkEntitiesByQuery(displayNameFilterQuery, 1, (i, entity) -> { + String name = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("name", new TsValue(0, "Invalid")).getValue(); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("Device" + i, name); + Assert.assertEquals("Device Label " + i, displayName); + }); + } + + @Test + public void testFindUsersByDisplayName() throws Exception { + loginTenantAdmin(); + + User userA = new User(); + userA.setAuthority(Authority.TENANT_ADMIN); + userA.setFirstName("John"); + userA.setLastName("Doe"); + userA.setEmail("john.doe@tb.org"); + userA = doPost("/api/user", userA, User.class); + var aId = userA.getId(); + + User userB = new User(); + userB.setAuthority(Authority.TENANT_ADMIN); + userB.setFirstName("John"); + userB.setEmail("john@tb.org"); + userB = doPost("/api/user", userB, User.class); + var bId = userB.getId(); + + User userC = new User(); + userC.setAuthority(Authority.TENANT_ADMIN); + userC.setLastName("Doe"); + userC.setEmail("doe@tb.org"); + userC = doPost("/api/user", userC, User.class); + var cId = userC.getId(); + + User userD = new User(); + userD.setAuthority(Authority.TENANT_ADMIN); + userD.setEmail("noname@tb.org"); + userD = doPost("/api/user", userD, User.class); + var dId = userD.getId(); + + EntityTypeFilter filter = new EntityTypeFilter(); + filter.setEntityType(EntityType.USER); + + EntityDataSortOrder sortOrder = new EntityDataSortOrder( + new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName"), EntityDataSortOrder.Direction.ASC + ); + EntityDataPageLink pageLink = new EntityDataPageLink(10, 0, null, sortOrder); + List entityFields = List.of(new EntityKey(EntityKeyType.ENTITY_FIELD, "displayName")); + + EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "John Doe"))); + checkEntitiesByQuery(query, 1, (i, entity) -> { + Assert.assertEquals(aId, entity.getEntityId()); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("John Doe", displayName); + }); + query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "John"))); + checkEntitiesByQuery(query, 1, (i, entity) -> { + Assert.assertEquals(bId, entity.getEntityId()); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("John", displayName); + }); + query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "Doe"))); + checkEntitiesByQuery(query, 1, (i, entity) -> { + Assert.assertEquals(cId, entity.getEntityId()); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("Doe", displayName); + }); + query = new EntityDataQuery(filter, pageLink, entityFields, Collections.emptyList(), List.of(getEntityFieldEqualFilter("displayName", "noname@tb.org"))); + checkEntitiesByQuery(query, 1, (i, entity) -> { + Assert.assertEquals(dId, entity.getEntityId()); + String displayName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("displayName", new TsValue(0, "Invalid")).getValue(); + Assert.assertEquals("noname@tb.org", displayName); + }); + } + @Test public void testFindDevicesByOwnerNameAndOwnerType() throws Exception { loginTenantAdmin(); @@ -1105,19 +1219,30 @@ public class EntityQueryControllerTest extends AbstractControllerTest { // all devices with ownerName = TEST TENANT EntityDataQuery query = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, tenantOwnerNameFilter)); - checkEntitiesByQuery(query, numOfDevices, TEST_TENANT_NAME, "TENANT"); + BiConsumer checkFunction = (i, entity) -> { + String name = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("name", new TsValue(0, "Invalid")).getValue(); + String ownerName = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("ownerName", new TsValue(0, "Invalid")).getValue(); + String ownerType = entity.getLatest().get(EntityKeyType.ENTITY_FIELD).getOrDefault("ownerType", new TsValue(0, "Invalid")).getValue(); + String alarmActiveTime = entity.getLatest().get(EntityKeyType.ATTRIBUTE).getOrDefault("alarmActiveTime", new TsValue(0, "-1")).getValue(); + + Assert.assertEquals("Device" + i, name); + Assert.assertEquals(TEST_TENANT_NAME, ownerName); + Assert.assertEquals("TENANT", ownerType); + Assert.assertEquals("1" + i, alarmActiveTime); + }; + checkEntitiesByQuery(query, numOfDevices, checkFunction); // all devices with wrong ownerName EntityDataQuery wrongTenantNameQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, wrongOwnerNameFilter)); - checkEntitiesByQuery(wrongTenantNameQuery, 0, null, null); + checkEntitiesByQuery(wrongTenantNameQuery, 0, null); // all devices with owner type = TENANT EntityDataQuery tenantEntitiesQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, tenantOwnerTypeFilter)); - checkEntitiesByQuery(tenantEntitiesQuery, numOfDevices, TEST_TENANT_NAME, "TENANT"); + checkEntitiesByQuery(tenantEntitiesQuery, numOfDevices, checkFunction); // all devices with owner type = CUSTOMER EntityDataQuery customerEntitiesQuery = new EntityDataQuery(filter, pageLink, entityFields, latestValues, List.of(activeAlarmTimeFilter, customerOwnerTypeFilter)); - checkEntitiesByQuery(customerEntitiesQuery, 0, null, null); + checkEntitiesByQuery(customerEntitiesQuery, 0, null); } @Test @@ -1163,6 +1288,28 @@ public class EntityQueryControllerTest extends AbstractControllerTest { findByQueryAndCheck(query, 0); } + private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, BiConsumer checkFunction) throws Exception { + await() + .alias("data by query") + .atMost(30, TimeUnit.SECONDS) + .until(() -> { + var data = findByQuery(query); + var loadedEntities = new ArrayList<>(data.getData()); + return loadedEntities.size() == expectedNumOfDevices; + }); + if (expectedNumOfDevices == 0) { + return; + } + var data = findByQuery(query); + var loadedEntities = new ArrayList<>(data.getData()); + + Assert.assertEquals(expectedNumOfDevices, loadedEntities.size()); + + for (int i = 0; i < expectedNumOfDevices; i++) { + checkFunction.accept(i, loadedEntities.get(i)); + } + } + private void checkEntitiesByQuery(EntityDataQuery query, int expectedNumOfDevices, String expectedOwnerName, String expectedOwnerType) throws Exception { await() .alias("data by query") diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java index 9755201fe9..dda2cf127d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -65,6 +65,7 @@ public class EntityKeyMapping { public static final String NAME = "name"; public static final String TYPE = "type"; public static final String LABEL = "label"; + public static final String DISPLAY_NAME = "displayName"; public static final String FIRST_NAME = "firstName"; public static final String LAST_NAME = "lastName"; public static final String EMAIL = "email"; @@ -83,6 +84,8 @@ public class EntityKeyMapping { public static final String SERVICE_ID = "serviceId"; public static final String OWNER_NAME = "ownerName"; public static final String OWNER_TYPE = "ownerType"; + public static final String LABELED_ENTITY_DISPLAY_NAME_SELECT_QUERY = "COALESCE (e." + LABEL + ", e." + NAME+")"; + public static final String USER_DISPLAY_NAME_SELECT_QUERY = "COALESCE(NULLIF(TRIM(CONCAT_WS(' ', e.first_name, e.last_name)), ''), e.email)"; public static final String OWNER_NAME_SELECT_QUERY = "case when e.customer_id = '" + NULL_UUID + "' " + "then (select title from tenant where id = e.tenant_id) " + "else (select title from customer where id = e.customer_id) end"; @@ -94,6 +97,16 @@ public class EntityKeyMapping { OWNER_NAME, OWNER_NAME_SELECT_QUERY, OWNER_TYPE, OWNER_TYPE_SELECT_QUERY ); + public static final Map labeledPropertiesFunctions = Map.of( + OWNER_NAME, OWNER_NAME_SELECT_QUERY, + OWNER_TYPE, OWNER_TYPE_SELECT_QUERY, + DISPLAY_NAME, LABELED_ENTITY_DISPLAY_NAME_SELECT_QUERY + ); + public static final Map userPropertiesFunctions = Map.of( + OWNER_NAME, OWNER_NAME_SELECT_QUERY, + OWNER_TYPE, OWNER_TYPE_SELECT_QUERY, + DISPLAY_NAME, USER_DISPLAY_NAME_SELECT_QUERY + ); public static final Map queueStatsPropertiesFunctions = Map.of(NAME, QUEUE_STATS_NAME_QUERY); public static final List typedEntityFields = Arrays.asList(CREATED_TIME, ENTITY_TYPE, NAME, TYPE, ADDITIONAL_INFO); @@ -153,20 +166,24 @@ public class EntityKeyMapping { Map contactBasedAliases = new HashMap<>(); contactBasedAliases.put(NAME, TITLE); contactBasedAliases.put(LABEL, TITLE); + contactBasedAliases.put(DISPLAY_NAME, TITLE); aliases.put(EntityType.TENANT, contactBasedAliases); aliases.put(EntityType.CUSTOMER, contactBasedAliases); aliases.put(EntityType.DASHBOARD, contactBasedAliases); + Map deviceAndAssetAliases = new HashMap<>(); + deviceAndAssetAliases.put(TITLE, NAME); + aliases.put(EntityType.DEVICE, deviceAndAssetAliases); + aliases.put(EntityType.ASSET, deviceAndAssetAliases); Map commonEntityAliases = new HashMap<>(); commonEntityAliases.put(TITLE, NAME); - aliases.put(EntityType.DEVICE, commonEntityAliases); - aliases.put(EntityType.ASSET, commonEntityAliases); + commonEntityAliases.put(DISPLAY_NAME, NAME); aliases.put(EntityType.ENTITY_VIEW, commonEntityAliases); aliases.put(EntityType.WIDGETS_BUNDLE, commonEntityAliases); - propertiesFunctions.put(EntityType.DEVICE, ownerPropertiesFunctions); - propertiesFunctions.put(EntityType.ASSET, ownerPropertiesFunctions); + propertiesFunctions.put(EntityType.DEVICE, labeledPropertiesFunctions); + propertiesFunctions.put(EntityType.ASSET, labeledPropertiesFunctions); propertiesFunctions.put(EntityType.ENTITY_VIEW, ownerPropertiesFunctions); - propertiesFunctions.put(EntityType.USER, ownerPropertiesFunctions); + propertiesFunctions.put(EntityType.USER, userPropertiesFunctions); propertiesFunctions.put(EntityType.DASHBOARD, ownerPropertiesFunctions); propertiesFunctions.put(EntityType.QUEUE_STATS, queueStatsPropertiesFunctions); From 95cad64f50549e70faf1fc02e1476c35c03689b6 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Tue, 7 Oct 2025 17:29:08 +0300 Subject: [PATCH 203/216] Add support of displayName in EDQS --- .../server/edqs/data/BaseEntityData.java | 22 +++++++++++++++++++ .../server/edqs/data/DeviceData.java | 1 + 2 files changed, 23 insertions(+) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java index 21b9ff0c4f..487c534778 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java @@ -20,6 +20,7 @@ import lombok.Setter; import lombok.ToString; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edqs.fields.EntityFields; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.permission.QueryContext; @@ -139,11 +140,32 @@ public abstract class BaseEntityData implements EntityDa case "name" -> getEntityName(); case "ownerName" -> getOwnerName(); case "ownerType" -> getOwnerType(); + case "displayName" -> getDisplayName(); case "entityType" -> Optional.ofNullable(getEntityType()).map(EntityType::name).orElse(""); default -> fields.getAsString(name); }; } + public String getDisplayName(){ + return switch (getEntityType()) { + case DEVICE, ASSET -> StringUtils.isNotEmpty(fields.getLabel()) ? fields.getLabel() : fields.getName(); + case USER -> { + boolean firstNameSet = StringUtils.isNotEmpty(fields.getFirstName()); + boolean lastNameSet = StringUtils.isNotEmpty(fields.getLastName()); + if(firstNameSet && lastNameSet) { + yield fields.getFirstName() + " " + fields.getLastName(); + } else if(firstNameSet) { + yield fields.getFirstName(); + } else if (lastNameSet) { + yield fields.getLastName(); + } else { + yield fields.getEmail(); + } + } + default -> fields.getName(); + }; + } + public String getEntityName() { return getFields().getName(); } diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java index 3a3e5c5792..f47bfcb28a 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/DeviceData.java @@ -18,6 +18,7 @@ package org.thingsboard.server.edqs.data; import lombok.ToString; import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.edqs.fields.DeviceFields; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.edqs.DataPoint; From 494fb7abeb51b1d9b3d92e122bc435d974d3cbae Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 8 Oct 2025 11:46:45 +0300 Subject: [PATCH 204/216] UI: Add new entity field - displayName --- ui-ngx/src/app/core/http/entity.service.ts | 2 ++ ui-ngx/src/app/shared/models/entity.models.ts | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/ui-ngx/src/app/core/http/entity.service.ts b/ui-ngx/src/app/core/http/entity.service.ts index c652ba39ba..5cd4167afa 100644 --- a/ui-ngx/src/app/core/http/entity.service.ts +++ b/ui-ngx/src/app/core/http/entity.service.ts @@ -817,6 +817,7 @@ export class EntityService { switch (entityType) { case EntityType.USER: entityFieldKeys.push(entityFields.name.keyName); + entityFieldKeys.push(entityFields.displayName.keyName); entityFieldKeys.push(entityFields.email.keyName); entityFieldKeys.push(entityFields.firstName.keyName); entityFieldKeys.push(entityFields.lastName.keyName); @@ -846,6 +847,7 @@ export class EntityService { case EntityType.EDGE: case EntityType.ASSET: entityFieldKeys.push(entityFields.name.keyName); + entityFieldKeys.push(entityFields.displayName.keyName); entityFieldKeys.push(entityFields.type.keyName); entityFieldKeys.push(entityFields.label.keyName); entityFieldKeys.push(entityFields.ownerName.keyName); diff --git a/ui-ngx/src/app/shared/models/entity.models.ts b/ui-ngx/src/app/shared/models/entity.models.ts index 5aa526b583..ba171b05e5 100644 --- a/ui-ngx/src/app/shared/models/entity.models.ts +++ b/ui-ngx/src/app/shared/models/entity.models.ts @@ -163,6 +163,11 @@ export const entityFields: {[fieldName: string]: EntityField} = { name: 'entity-field.label', value: 'label' }, + displayName: { + keyName: 'displayName', + name: 'entity-field.name', + value: 'name' + }, queueName: { keyName: 'queueName', name: 'entity-field.queue-name', From a549340010362abe3bfd6ebc635f29bbe8d8edc4 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 8 Oct 2025 15:07:21 +0300 Subject: [PATCH 205/216] UI: Fixed Genaral resources --- .../resources-library.component.html | 20 +++++++++---------- .../resources/resources-library.component.ts | 4 ++++ .../resources-library-table-config.resolve.ts | 2 +- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html index 4737b75ef2..819753e105 100644 --- a/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html +++ b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.html @@ -30,7 +30,7 @@ @@ -48,16 +48,14 @@
    - @if (resourceTypes.length > 1) { - - resource.resource-type - - - {{ resourceTypesTranslationMap.get(resourceType) | translate }} - - - - } + + resource.resource-type + + + {{ resourceTypesTranslationMap.get(resourceType) | translate }} + + + resource.title diff --git a/ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts index e3ad0e15f3..977eb6873a 100644 --- a/ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts +++ b/ui-ngx/src/app/modules/home/components/resources/resources-library.component.ts @@ -112,6 +112,10 @@ export class ResourcesLibraryComponent extends EntityComponent impleme if (this.isEdit && this.entityForm && !this.isAdd) { this.entityForm.get('resourceType').disable({ emitEvent: false }); this.entityForm.get('fileName').disable({ emitEvent: false }); + this.entityForm.get('data').disable({ emitEvent: false }); + } + if (this.isAdd && this.resourceTypes.length === 1) { + this.entityForm.get('resourceType').disable({ emitEvent: false }); } } diff --git a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts index d92355fbac..1bce24f984 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/resource/resources-library-table-config.resolve.ts @@ -173,7 +173,7 @@ export class ResourcesLibraryTableConfigResolver { case 'downloadResource': this.downloadResource(action.event, action.entity); return true; - case 'deleteLibrary': + case 'deleteResource': this.deleteResource(action.event, action.entity); } return false; From bae703030276c8cd6d539c520d0bee21a479a212 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 8 Oct 2025 15:44:29 +0300 Subject: [PATCH 206/216] Fixed LABELED_ENTITY_DISPLAY_NAME_SELECT_QUERY in sql --- .../org/thingsboard/server/dao/sql/query/EntityKeyMapping.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java index dda2cf127d..ea5c3e8f9d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -84,7 +84,7 @@ public class EntityKeyMapping { public static final String SERVICE_ID = "serviceId"; public static final String OWNER_NAME = "ownerName"; public static final String OWNER_TYPE = "ownerType"; - public static final String LABELED_ENTITY_DISPLAY_NAME_SELECT_QUERY = "COALESCE (e." + LABEL + ", e." + NAME+")"; + public static final String LABELED_ENTITY_DISPLAY_NAME_SELECT_QUERY = "COALESCE(NULLIF(e." + LABEL + ", ''), e." + NAME + ")"; public static final String USER_DISPLAY_NAME_SELECT_QUERY = "COALESCE(NULLIF(TRIM(CONCAT_WS(' ', e.first_name, e.last_name)), ''), e.email)"; public static final String OWNER_NAME_SELECT_QUERY = "case when e.customer_id = '" + NULL_UUID + "' " + "then (select title from tenant where id = e.tenant_id) " + From e3967e31fc83ed1cb6032d7ab64760bfa15c351d Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 8 Oct 2025 15:52:33 +0300 Subject: [PATCH 207/216] Sync edqs and sql logics to display entity name --- .../org/thingsboard/server/edqs/data/BaseEntityData.java | 6 +++--- .../thingsboard/server/dao/sql/query/EntityKeyMapping.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java index 487c534778..b75ce6a315 100644 --- a/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java +++ b/common/edqs/src/main/java/org/thingsboard/server/edqs/data/BaseEntityData.java @@ -148,10 +148,10 @@ public abstract class BaseEntityData implements EntityDa public String getDisplayName(){ return switch (getEntityType()) { - case DEVICE, ASSET -> StringUtils.isNotEmpty(fields.getLabel()) ? fields.getLabel() : fields.getName(); + case DEVICE, ASSET -> StringUtils.isNotBlank(fields.getLabel()) ? fields.getLabel() : fields.getName(); case USER -> { - boolean firstNameSet = StringUtils.isNotEmpty(fields.getFirstName()); - boolean lastNameSet = StringUtils.isNotEmpty(fields.getLastName()); + boolean firstNameSet = StringUtils.isNotBlank(fields.getFirstName()); + boolean lastNameSet = StringUtils.isNotBlank(fields.getLastName()); if(firstNameSet && lastNameSet) { yield fields.getFirstName() + " " + fields.getLastName(); } else if(firstNameSet) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java index ea5c3e8f9d..34bc719639 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/EntityKeyMapping.java @@ -84,7 +84,7 @@ public class EntityKeyMapping { public static final String SERVICE_ID = "serviceId"; public static final String OWNER_NAME = "ownerName"; public static final String OWNER_TYPE = "ownerType"; - public static final String LABELED_ENTITY_DISPLAY_NAME_SELECT_QUERY = "COALESCE(NULLIF(e." + LABEL + ", ''), e." + NAME + ")"; + public static final String LABELED_ENTITY_DISPLAY_NAME_SELECT_QUERY = "COALESCE(NULLIF(TRIM(e." + LABEL + "), ''), e." + NAME + ")"; public static final String USER_DISPLAY_NAME_SELECT_QUERY = "COALESCE(NULLIF(TRIM(CONCAT_WS(' ', e.first_name, e.last_name)), ''), e.email)"; public static final String OWNER_NAME_SELECT_QUERY = "case when e.customer_id = '" + NULL_UUID + "' " + "then (select title from tenant where id = e.tenant_id) " + From 7ee879a9c1d5001706c551ec34c12b69195c6cc9 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 8 Oct 2025 16:30:40 +0300 Subject: [PATCH 208/216] UI: Add useEntityDisplayName input property in entity-autocomplete.component and entity-list.component --- .../entity/entity-autocomplete.component.html | 2 +- .../entity/entity-autocomplete.component.ts | 8 ++++++-- .../entity/entity-list-select.component.html | 1 + .../entity/entity-list-select.component.ts | 3 +++ .../components/entity/entity-list.component.html | 4 ++-- .../components/entity/entity-list.component.ts | 8 ++++++-- .../components/entity/entity-select.component.html | 1 + .../components/entity/entity-select.component.ts | 4 ++++ ui-ngx/src/app/shared/models/base-data.ts | 13 ++++++++++++- 9 files changed, 36 insertions(+), 8 deletions(-) diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html index db7300530e..512ae42921 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.html @@ -60,7 +60,7 @@ #entityAutocomplete="matAutocomplete" [displayWith]="displayEntityFn"> - +
    diff --git a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts index 9137a3b2d7..6f8b39c171 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-autocomplete.component.ts @@ -22,7 +22,7 @@ import { catchError, debounceTime, map, share, switchMap, tap } from 'rxjs/opera import { Store } from '@ngrx/store'; import { AppState } from '@app/core/core.state'; import { AliasEntityType, EntityType } from '@shared/models/entity-type.models'; -import { BaseData } from '@shared/models/base-data'; +import { BaseData, getEntityDisplayName } from '@shared/models/base-data'; import { EntityId } from '@shared/models/id/entity-id'; import { EntityService } from '@core/http/entity.service'; import { getCurrentAuthUser } from '@core/auth/auth.selectors'; @@ -138,6 +138,10 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit @coerceArray() additionalClasses: Array; + @Input() + @coerceBoolean() + useEntityDisplayName = false; + @Output() entityChanged = new EventEmitter>(); @@ -395,7 +399,7 @@ export class EntityAutocompleteComponent implements ControlValueAccessor, OnInit } displayEntityFn(entity?: BaseData): string | undefined { - return entity ? entity.name : undefined; + return entity ? (this.useEntityDisplayName ? getEntityDisplayName(entity) : entity.name) : undefined; } private fetchEntities(searchText?: string): Observable>> { diff --git a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html index f3983111bf..0ae793a1ac 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.html @@ -36,6 +36,7 @@ *ngIf="modelValue.entityType" [required]="required" [entityType]="modelValue.entityType" + [useEntityDisplayName]="useEntityDisplayName" formControlName="entityIds">
    diff --git a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.ts index e4bef51d15..7c2c4e7f2a 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-list-select.component.ts @@ -68,6 +68,9 @@ export class EntityListSelectComponent implements ControlValueAccessor, OnInit { @Input() additionEntityTypes: {[key in string]: string} = {}; + @Input({transform: booleanAttribute}) + useEntityDisplayName = false; + displayEntityTypeSelect: boolean; private defaultEntityType: EntityType | AliasEntityType = null; diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.html b/ui-ngx/src/app/shared/components/entity/entity-list.component.html index e0e7dfe3f7..33c684285e 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.html @@ -28,7 +28,7 @@ class="tb-chip-row-ellipsis" [removable]="!disabled" (removed)="remove(entity)"> - {{entity.name}} + {{ displayEntityFn(entity) }} close - +
    diff --git a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts index 9d1de180e9..f93172c9b0 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-list.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-list.component.ts @@ -39,7 +39,7 @@ import { Observable } from 'rxjs'; import { filter, map, mergeMap, share, tap } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { EntityType } from '@shared/models/entity-type.models'; -import { BaseData } from '@shared/models/base-data'; +import { BaseData, getEntityDisplayName } from '@shared/models/base-data'; import { EntityId } from '@shared/models/id/entity-id'; import { EntityService } from '@core/http/entity.service'; import { MatAutocomplete } from '@angular/material/autocomplete'; @@ -125,6 +125,10 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, OnChan @coerceBoolean() allowCreateNew: boolean; + @Input() + @coerceBoolean() + useEntityDisplayName = false; + @Output() createNew = new EventEmitter(); @@ -277,7 +281,7 @@ export class EntityListComponent implements ControlValueAccessor, OnInit, OnChan } public displayEntityFn(entity?: BaseData): string | undefined { - return entity ? entity.name : undefined; + return entity ? (this.useEntityDisplayName ? getEntityDisplayName(entity) : entity.name) : undefined; } private fetchEntities(searchText?: string): Observable>> { diff --git a/ui-ngx/src/app/shared/components/entity/entity-select.component.html b/ui-ngx/src/app/shared/components/entity/entity-select.component.html index 2b07af9e08..c0b4f1aafb 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-select.component.html +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.html @@ -33,6 +33,7 @@ [appearance]="appearance" [required]="required" [entityType]="modelValue.entityType" + [useEntityDisplayName]="useEntityDisplayName" formControlName="entityId">
    diff --git a/ui-ngx/src/app/shared/components/entity/entity-select.component.ts b/ui-ngx/src/app/shared/components/entity/entity-select.component.ts index 01b1f03388..5e75426952 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-select.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-select.component.ts @@ -62,6 +62,10 @@ export class EntitySelectComponent implements ControlValueAccessor, OnInit, Afte @Input() appearance: MatFormFieldAppearance = 'fill'; + @Input() + @coerceBoolean() + useEntityDisplayName = false; + displayEntityTypeSelect: boolean; AliasEntityType = AliasEntityType; diff --git a/ui-ngx/src/app/shared/models/base-data.ts b/ui-ngx/src/app/shared/models/base-data.ts index 972fa0b5a0..d9dc4b070d 100644 --- a/ui-ngx/src/app/shared/models/base-data.ts +++ b/ui-ngx/src/app/shared/models/base-data.ts @@ -16,7 +16,9 @@ import { EntityId } from '@shared/models/id/entity-id'; import { HasUUID } from '@shared/models/id/has-uuid'; -import { isDefinedAndNotNull } from '@core/utils'; +import { isDefinedAndNotNull, isNotEmptyStr } from '@core/utils'; +import { EntityType } from '@shared/models/entity-type.models'; +import { User } from '@shared/models/user.model'; export declare type HasId = EntityId | HasUUID; @@ -49,3 +51,12 @@ export function hasIdEquals(id1: HasId, id2: HasId): boolean { return id1 === id2; } } + +export function getEntityDisplayName(entity: BaseData): string { + if (entity?.id?.entityType === EntityType.USER) { + const user = entity as User; + const userName = (user?.firstName ?? '') + " " + (user?.lastName ?? ''); + return isNotEmptyStr(userName) ? userName.trim() : entity?.name; + } + return isNotEmptyStr(entity?.label) ? entity.label : entity?.name; +} From 60f51a01b9acf7321542f09ee5d6087b3849f716 Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 8 Oct 2025 19:11:58 +0300 Subject: [PATCH 209/216] UI: Add new alarm field - originatorDisplayName --- .../data/json/system/widget_types/alarms_table.json | 13 ++++++------- .../json/system/widget_types/entities_table.json | 13 ++++++------- ui-ngx/src/app/shared/models/alarm.models.ts | 7 +++++++ 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/application/src/main/data/json/system/widget_types/alarms_table.json b/application/src/main/data/json/system/widget_types/alarms_table.json index 4f86957f1c..446129972b 100644 --- a/application/src/main/data/json/system/widget_types/alarms_table.json +++ b/application/src/main/data/json/system/widget_types/alarms_table.json @@ -12,18 +12,12 @@ "templateHtml": "\n", "templateCss": "", "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.alarmsTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.alarmsTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n supportsUnitConversion: true\n };\n};\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", "settingsDirective": "tb-alarms-table-widget-settings", "dataKeySettingsDirective": "tb-alarms-table-key-settings", "hasBasicMode": true, "basicModeDirective": "tb-alarms-table-basic-config", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"allowAssign\":true,\"displayActivity\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true,\"entitiesTitle\":null,\"alarmsTitle\":\"Alarms\"},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418},{\"name\":\"assignee\",\"type\":\"alarm\",\"label\":\"Assignee\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.5008441077416634}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"warning\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false,\"configMode\":\"basic\",\"alarmFilterConfig\":null}" + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"allowAssign\":true,\"displayActivity\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true,\"entitiesTitle\":null,\"alarmsTitle\":\"Alarms\"},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originatorDisplayName\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418},{\"name\":\"assignee\",\"type\":\"alarm\",\"label\":\"Assignee\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.5008441077416634}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"warning\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false,\"configMode\":\"basic\",\"alarmFilterConfig\":null}" }, - "tags": [ - "alert", - "alerts" - ], "resources": [ { "link": "/api/images/system/alarms_table_system_widget_image.png", @@ -36,5 +30,10 @@ "data": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAIAAADGnbT+AAAABmJLR0QA/wD/AP+gvaeTAAAUUElEQVR42u2dh1sUVxeH/ctsUWMSSUETY/xSTDRKNNUYG4IgmhgVC7ZYA7FEERVUNAqoCIJdaeIConSwUAQEBO73zp5l3KAsEHcV3PN78vjcnXJ35s47554ZNr8zxBjz5MmTmpqakpKSOyrVCwiEAKmtrQ2ohkBVaWlpfX19e3u7UaleQCAESOAEVENAjA86KCpvCZyAagjhS2OVyrtxC6iGMDXqWKi8K6BSsFQKlkrBUilYCpZKwVIpWCoFS6V66WDxvisxMfGv3pSRkaEDquoHWJcuXZozZ87q1avX9qzly5ezTXV19aA47fT09JCQkObmZiXgVYJ15swZoPF8GfLz89nm9u3bg+K0+3JGqtcHrFWrVq34t3z0R8zBAhZ/zd2+ffuCBQsYGa+kwrt27Tp48CCNx48f+/T0BxZYe/bs4cy/+uqrKVOm7HLKz8H65ptvvvvuu3/++ee333575513Hjx48IIdXrx48fr163IPL1q0yL+mwiVLloSGhtox7Ny5c5LnMbi1tbXkRvv37//555+3bdsm2DHcZHgsYXlHR8drAxZn98YbbyQlJdHmvBiKW7du0T59+jQnGxYWVlxcfP/+fQaEf1nO6cfFxdE4f/48Z8dy2T4hIWHHjh1sf+XKFbbhI31+/PHH48eP52NMTMyhQ4fYjN7YpampyV/Amj17Ng3GheWVlZVDhw5lIUPz7rvv7t69m1XTpk1jLfn4hx9+yNPrc/ssKyvbu3evHf+6nVFFRQVr+W3aQGNrzZo1Y8eOXbduHT+ds0POm2++yelv3ryZ8+WYP/nkE1jp7OwMDAxkeW5u7ujRoxkHiAkICOAcV65cOWrUKLYHHQaWBKO8vHzu3LmzZs0qKiqKjY2dMGECu+/cuZOR9JeIdePGjZEjR9bV1b399tvciAIWlLBq06ZNP/744927d1kSHR3N4H7//fcQ9tw+2feXX37hxhW23M8IqhYvXsyXPnz4cKCBxfWW8DN8+HD4IG4tW7Zs6tSpnOyBAwc4cQacJGzevHkEpzFjxnBGUPjpp5+yQXx8PHsxgOxoz3oClvtUyFmzGbtD1d9//+0vYDGy3E+///47Nx9MuIPFHTZz5syCggKWcGdvduro0aM9dctrNthiAuUut89IqALHqqqqgUYVl5xZT+4Eh8MxbNiwq1evMjJffPHF5i5x/Fw+kKItNxXcEMPsDYhSgGWP57NgIeYEcgnw8uLbooEOFmJ0GFPYoi1gcTwcDHcY789aWlpgjjSfu5mbmwvgoWdhi1ucKYOjLSwsHLBUIX7dO2LEiMOHD8MWw8sgkCQRqMiNIKCxsZGMSrDjcYfpj2SA9okTJxgQ7j2GiA24i54LFrfiDz/8ICnp8ePH6ZyZ8TVP3hmIX3/91f7I5QcmxtQGa8aMGYw4M8K9e/dYyKpJkyZxw3GnkmF47pxHAdgKDg7maPkXiLl+AzZ/T01N5dQ4ZfIqbjDiNyQRXUY7Zb+OIQeXfEvS/KioKGIYeRVnx/8ww0d7PGnwkUZmZiYbMAK0SdhpSwrvRy9ICTOkpTKC9lT47GuI1tbWvncIWxztgI1V3URUfvaB0fOLGPDq9VmEHiCVBk8GgOXdFLNPYHGXcxlIBo/1LCYjtuEn9N4dU7oleScVlY/uOdYLwhoeHj4oqPK1iBqEt/Xr17+C91g8lDE9zfEoyV28/j6TNzTkp+43GY/KXvkWXj0rVaihocEXP2/Rn82ofCIFS6VgqRQslYKlYKkULJWCpVKwFCyVgqVSsFQKlkqlYKkULJVfg6VSeV0asVQ6FaoULJWCpWCpFCyVgqVSsFQqBUulYKkULJVKwVIpWCoFS6V6iWBhXYKfzgGPwoopKytLB1TVD7CuXbsmZqketHDhQnxBxGVVpfLTAgIq/wILg9DNbqI+j79fn+IY6z93Fe00t3c/f+OSOFOVrGA9R5g+Xr58GaM9HEdp4Pj7kodD7IcHEFipH5hTQ01Dl7FqbY71MW3i8zeuPGXuZypYPYpYJQbuorNnz+KliZO2WKURxniSoJgAPm84whHkWEsWyKqbN2/imrxlyxbW5uXlue754mIwjYyMFB9AHGDxQGcJH3EfxdOb3cUWli/Fk5JkkQHBxBCyjdOBcsOGDcZpa8iO+H9iBMeR4NkcERHByPgcrHPjTf5a18ebq6yPLrA6TflRkx1hHJtMW521oCzBVPxjNZ40W4EtK8zcjcWszjkKe0z5EZP7m3lcpWC5qKLmB3BwIcHFOMsFUIcMx1vcpCmvQAEPfFrfe+89Hl3xQsZRGM9q9mIJ2OGrieMt2F24cGHy5Mk4vQIoDTZ49OjRZ599BqMcM/1gFIhVLra5rMLzc+vWrfv27TNO41cqFcg0jX86a1nCV+NhyVEFBQWJ664PwboVZc4EmM4npr3VnHnb+ihgFe8y5z40VUnm0kyTFWItyV5s8lY4n7bmmotBpiLRnP/M3FxjLWEb9s1fbVruKViWKKBAiCJ44Br6wQcfCFhcURpgRLCRzT766CMYYoltD8x8CgR4Wc+fP/+yU3hWM80BFkzINlhOQiFm1BgwY+XNEkoQ5OTk0HguWOKvD5FvvfWW9Ekw87p7Z3ewyo+b9P+Z6rMWQ5lfWmFJwCIsNeRbISo7zKR98hSslofm1DBrFao+Z1LGms4OC6zCbToVPgXr22+/ZcaRXP6PP/5wByslJWXp0qWyGSGHijruYGGQz8wFQwQV+2kA710bLCIcnYMLJv0Eqr6Dhcc69ZLsPtndt2CVxlvZ+vX55trP1tRWEusCq/SQSR1v7uw12eEm3Q2s5gorD3tcaS2py7La7S1OsHYoWE/BooCHzQHTX69gkX1LKKJ+GIdEaSdmUrHJZ3fcp22wmPjwRhfn9OnTp0uahae+vOblAIRjSqp0A4vemHCZOqUT3xZKscA6bFrum+TRJmWMaa0zJftdYF2bY3KXWw0Hk+PHT8HqbLf2KolzrtpoMr5wTYUKljtY5EmUVqNneJK3D57BokFlLGY9smxZRQkGZjqq7jCrkjzZYIEUFFI+hLVQKFX8oIclJP5wA3Z8Lzt2A8s43bxJ1Jhk2ZEnBp+DZWE0z2SFOt8pdIFVddqijczpwlST9IaVpNs51v0LVlp25l2TGmjqchUsl7j83aoBEBXscmc0xI4bg3z7kKj/Ick7UyH7Un3OfXd2sSv9deuckENv9kJo41HR7l8qX5BUPbsj2zAn+qic4lO1N5qONmej1frP1WjsWttsxTCQetJgPSReX2DyVrlWEbdaalyPhNbJNLl2H1Bg8egENCdPnjzds6i6wTZMHK/w3Yl7juV34pVE0kgrwR9E77H4CyAPWXN6EyWTXm3JPwrdUrrCT8FiymtwDJzD6evPZphomnuTUan6C5ZKpWCpFCyVgqVSKVgqBUulYKlUCpZKwVIpWCqVgqVSsFSvJVgqlRYQUOlUqFKwVCoFS6VgqRQslUrBUilYKgVLpVKwVAqWSsHqSZ7/b1WsDXQ0Vf0DC0+OZcuW9fq/2GO76HOTDNXrBBb2m4PCFEQ1yMBSn/dXLOaBnqYCp8VS94376KPBvgkJxjemGwMLLDzWsEGbOHEi/mk06PAlX0EMI33r0NdfNTSY4GAzfLgZOdKEhJgukzCX2trMiBEmO9tqJyYacdrZssX0cdyYXoYONTU1/hKxutlxG6evn9g92rLt1/i3gdH/1x3b3vTvC8DHbk5unIu9O+ZH9nI8SHNzcz0cm4fv9YnCwsz06da1578pU0xUVPdYRY2Zzk6rMWYMPmZWg6co9wOrq3NtYMcz+3z9GSzc9PAF/emnn7DOlnoCWEUGBwdDAEagbEaDVeLUjfEacQ7LWiy4Mbe1e8Ni9MsvvxSHSKwi6YGI6HA4sIfE65H+cZcEFPxOca39+uuvMVd+rrmt7FhUVITXMo6SHBUGpz70bwIRAtXZs66P+M7HxroYmjbNDBuG6aBFBsMCc3xkeUaG2bTJzJ5tbeZwmIkTzdixZvx4I1URtm61Po4bZ4U09vVnsGgLEPQMHwIWlrU0KDnG1CnutAEBARweYIGIc4pogzb4wAUezmQbaMNBDrCEQvTnn3+KUxxssaXpzTWZsgPGaRUGXpQRkMOLlYvtC/EVXPhnK2UAELcN9RBssNwjlg3WjBnGWfrAYBjOsBDhVqywYCJiMYFevOjXYM2aNQuSfnRqDGPXHztuDNyBEgt42R34qCTg7vPOx9DQUEpREPz6bsddVVU1atQo6fPzzz9fsWKFr8DiknPhncfTHay0NKvhASwyB6Jdt+IGSUncQ2bqVCtpS0nxa7AoVZeWltbQpb6DFR4ezksQCghQxsLenbhlg8XpwJOY3lIWrxtY1FPZu3ev6aGAwPvvv09mJn36cCokHwoIMIcPuz5y2CEhfQULMeXJNIpFLwVguMpMlw6Ha5Wfg8XLM4IWxv9UFuHSmj7YcZM8sTHXnpQfY2NctfnIORLDOAUbLJgghrEK695x48bRm3EWwoiPj6dUzokTJ5hVeS1HqZVn7bg50+joaPxOWSLzsq+0Z48Fwf79BspJj3j06wmswEDDTcWDoQ0W0yWpWHKyCQqysn4GDbA4d/zradCVv4FFqRJK39gfk5OTmbCYccDLOIs0iUs2KVSiDLT1iL2F0CIFBKKioghX2fIQbkxBQQGe73ZMolCA3Tl27SynUAAwXSTnwII/O5snA3YhthG0WMtX2EWabIb4LtYyh8bExJDP+fbBkPkrNNRwCwlMxrKu56xcIY2J2Fl8ygpOvJjAdJ6GpH1gJ0EOOuU12MmT1jYs3L7d6o1oze6+ebZ9rV6Q+rUd92B88858ATT8ufDXnkWQIB+SWPKqRNRhptOLOmjAkqIPf3kUWZEU+FOpjP4eS6VgqRQslYKlYKkULJWCpVKwFCyVgqVSsFQKlg6ESsFSKVgqBUul8j5YKpX6vKt0KlQpWCqVgqVSsFQKlkqlYKkULJWCpVIpWCoFS6VgPVeY+5zuTWqZrOofWHl5eXP6oPnz5+McpGOqMv1ym8GAykNliqysLLXjVv0XsF6CjVF5efmLO4sUFhbmuNkrYpVTWlra307w4sJI0n0JnrZimMst9HKy0nvN9y5XX5X23YaSwroiaWfdyyl7VN7f3tLLM2qaa/wULGz1QsQN8QWEy97GjRvtjxEREdjz9bcTXN3O2nbFXVkmRpLG6cBGbmCc3rjuBnFeFzDNSw1ubGukvTM3OvLKGhodpmNRemjug7z+9naoIOFO/V0Fy7iHHGwagUMM1uPi4qTBBcYJUlJA27/PA1jYG8uObE/UMU7fZQDCUguGODvcuWGlxmmdSNiT2InjMruzHNNKAYvlrMUfEOPusLAw7P/YUfxRibj4PXnr2nR0doSkL7710NFhOhelhcLTo9ZH5Y8qbNpy7ueevHPqWvV1tuTjhYqLBXWFSXeSWzpaUkrOENVSSs9kVGayO2szKy9UNFY0tjWxqryxnM2uVl/rdK6it9OlZ9PK0h21BXkPbvoFWKdOnZoxYwZXMTIyUhxHMYMEC6431rTiJrpkyZLMzEx3sPCGLO8Sho4CFo6jmIvSwLUWo0dZQrcAivlxUFAQ3w5kM2fOZNXRo0fXrLEiBFvyUAJ5s2fPFrDWrl175MgRfFA5XywqAZEDYHvZGG9SL973W7K2Jt89TaQhXO3Iic6qyb5UeXnV5UhWJRafWHkpEiCWXljOvyxZeWnV4vPhO3NiHrU2zk1duPbqhuPFJ1gCMaxdduH3S5VXmF5ZteXG1mO3ExekBYMRI7n26voN1zcfLToGvn/n7/MLsIgKUioC87cJEybU1tZihMzl5+uIFvjesgoDd/fCE4A1efLkkC5hbusBLFmClSiOusZZb0JMv22wAgMDpQiKPRUKWDTAV+ITE6L4y1OOAJS9CNaJ4pPRubuIMfFFR86WpR4qiD9UGH/AcZBVJEwEnuL6O2ywMydawDpTmirTJfQU1lo5GaDE3orrBlZtSy1LgIm4VdpQxpKGVqvORawjzl/Aoq6OOGYjggqOyNiswxOTI5eTbyTFxuq4LzmWB7CIfLbH5OjRo93BEs48gwWOhE9GQEoceFH5D/IJSFuzthFamNqIW+uubrxcZbm3p5WnsQrg1l/bRAYmYDHf2WDdrr0trOzL398NrLoW663QHze2MpPmP7gVnO4a8wP+AxbTkLgdi996Y6OVW1DXBFNuikokJCTQOGw7oXsEi4AnsWe7U30Ei1lS3L8xOH0WLGZq2QurZqqqUFvPu2A1PWmal7qQ7Iq0iTkrPGMp8xdwsIoJ8VxZmhOdAzty/vzPYD14/JCkTR4zo3N2vbZgcevbsxi98ZBP8RKuJTDZl42Km8yDxlmhc/jw4ZVS9ao3sCiVQ+kbEjW84PsOFi7wkyZNWrlyJfGyG1h8ETO11ErhIWDYsGG+8PZdfWUtc5a0Y/J2w5Zk3MduHyclIj2CGLb5z2DROOg4TCq25sq68MyI1xMsnq1uuUmKeMnbV2ZAezPe79vzo4QTd0FbjZspPtjJ3wO44zlISCVREwJ4oJOiYvRmM+Fw1m6Q4gOyhN7I8zgYMZpnud0hT6MSBUmtBDuvi4jy8HGttOtb62uan7LLE2JlUyXPdDSsA2uuJm23Dsx0EoFa2q1iyqRTklHJlk/a21klT5FkaXRI41Gb9bBZ3VRD5DtSeOzlgcUzEdBgn3+jZ1G4hm388+/ZZHs8uqbZBv+DTRtvbCaNI1ELy4hwB9fnYJWVlS1YsKDXvxUyy/hnyXFedvi28ImPRRjjBRgv9CXgvTywjLMyUWlv6lZsUuXP0t9jqRQslYKlUrAULJWCpVKwVAqWgqVSsFQKlsqvweJvq/KbXZXKKwInoBrCX+/r6+t1OFTeEr/+AKohbW1t/KUPtjRuqV48VgESONEYwmd+jQlihC81kVa9iEAIkCRC/R9f3OEsEgi6eAAAAABJRU5ErkJggg==", "public": true } + ], + "scada": false, + "tags": [ + "alert", + "alerts" ] } \ No newline at end of file diff --git a/application/src/main/data/json/system/widget_types/entities_table.json b/application/src/main/data/json/system/widget_types/entities_table.json index 01034a1d1d..f58bd3f0ce 100644 --- a/application/src/main/data/json/system/widget_types/entities_table.json +++ b/application/src/main/data/json/system/widget_types/entities_table.json @@ -11,19 +11,13 @@ "resources": [], "templateHtml": "\n", "templateCss": "", - "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'name', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", - "settingsSchema": "", - "dataKeySettingsSchema": "", + "controllerScript": "self.onInit = function() {\n}\n\nself.onDataUpdated = function() {\n self.ctx.$scope.entitiesTableWidget.onDataUpdated();\n}\n\nself.onEditModeChanged = function() {\n self.ctx.$scope.entitiesTableWidget.onEditModeChanged();\n}\n\nself.typeParameters = function() {\n return {\n maxDatasources: 1,\n hasDataPageLink: true,\n warnOnPageDataOverflow: false,\n dataKeysOptional: true,\n supportsUnitConversion: true,\n defaultDataKeysFunction: function() {\n return [{ name: 'displayName', type: 'entityField' }];\n }\n };\n}\n\nself.actionSources = function() {\n return {\n 'actionCellButton': {\n name: 'widget-action.action-cell-button',\n multiple: true,\n hasShowCondition: true\n },\n 'rowClick': {\n name: 'widget-action.row-click',\n multiple: false\n },\n 'rowDoubleClick': {\n name: 'widget-action.row-double-click',\n multiple: false\n },\n 'cellClick': {\n name: 'widget-action.cell-click',\n multiple: true\n }\n };\n}\n\nself.onDestroy = function() {\n}\n", "settingsDirective": "tb-entities-table-widget-settings", "dataKeySettingsDirective": "tb-entities-table-key-settings", "hasBasicMode": true, "basicModeDirective": "tb-entities-table-basic-config", "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSearch\":true,\"enableSelectColumnDisplay\":true,\"enableStickyHeader\":true,\"enableStickyAction\":true,\"reserveSpaceForHiddenAction\":\"true\",\"displayEntityName\":false,\"displayEntityLabel\":false,\"displayEntityType\":false,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"name\",\"useRowStyleFunction\":false,\"entitiesTitle\":\"Entities\"},\"title\":\"Entities table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"datasources\":[{\"type\":\"function\",\"name\":\"Simulated\",\"entityAliasId\":null,\"filterId\":null,\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity name\",\"color\":\"#2196f3\",\"settings\":{\"columnWidth\":\"0px\",\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.472295003170325,\"funcBody\":\"return 'Simulated';\",\"aggregationType\":null,\"units\":null,\"decimals\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Entity type\",\"color\":\"#607d8b\",\"settings\":{},\"_hash\":0.782057645776538,\"funcBody\":\"return 'Device';\",\"decimals\":null,\"aggregationType\":null,\"usePostProcessing\":null,\"postFuncBody\":null},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Sin\",\"color\":\"#4caf50\",\"settings\":{},\"_hash\":0.904797781901171,\"funcBody\":\"return Math.round(1000*Math.sin(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Cos\",\"color\":\"#f44336\",\"settings\":{},\"_hash\":0.1961430898042078,\"funcBody\":\"return Math.round(1000*Math.cos(time/5000));\",\"decimals\":0},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Random\",\"color\":\"#ffc107\",\"settings\":{},\"_hash\":0.7678057538205878,\"funcBody\":\"var value = prevValue + Math.random() * 100 - 50;\\nvar multiplier = Math.pow(10, 2 || 0);\\nvar value = Math.round(value * multiplier) / multiplier;\\nif (value < -1000) {\\n\\tvalue = -1000;\\n} else if (value > 1000) {\\n\\tvalue = 1000;\\n}\\nreturn value;\",\"decimals\":2}],\"alarmFilterConfig\":{\"statusList\":[\"ACTIVE\"]}}],\"displayTimewindow\":false,\"configMode\":\"basic\",\"actions\":{},\"showTitleIcon\":false,\"titleIcon\":\"list\",\"iconColor\":null}" }, - "tags": [ - "administration", - "management" - ], "resources": [ { "link": "/api/images/system/entities_table_system_widget_image.png", @@ -36,5 +30,10 @@ "data": "iVBORw0KGgoAAAANSUhEUgAAAMgAAACgCAAAAABslHx1AAAAAmJLR0QA/4ePzL8AAAnhSURBVHja7d3rU5NXHsBx/7KAa8WuBRahaEAIiQRFQSAKXlrUpagYAmgBJbXKin3qKlaKYAOmtFlQLpWkgCgSRC6GiwZFQAi3J/nuC9Da2RGSTNtZmXNeJWfmOfl9Juc2c855zjoWnU8HPvD01LnAukXHpMwHnuRJx+I65yRrIE061z2V1wJEfrpugDWRBgREQAREQAREQATkQ4A4X/sBkXW34IHO7d9PfqXT6XS6Tr9DztXpsv/zP7mJZf5AFEEjNCn8nEZ2WE4HWSxjfkPUxyyXAxv/IEjoAZoUsnxxx24b5ScOaaqMquwFHqSoL3u8KK1mM0h6xrT9WYXalF7mC2MODnsPKYXdxfycFFdOR2qJKkfmB83hqDK601RFc2Sc1e61HlJVeQep/NjSpJDrEvuMERRHtF9UlHZs+nk6+N8PQhu9hLSuf10V4dakdx1RY4zvyU/2HlI43LKp9rmysUXR3xhQ3RpoHQysatlc5gr5qlt9lvAs++6/35M2LHoFuVuxxayQaSw5qJCLM+hRTBBXfjdQklTnvITIYebDxWiu0a14vf2AVBDodUVVb/xEUeKhtzQvoLnxI9hSW7UNEstaAxe5GU14DcZ99CvGvIO4E6MV8g8RTZVvIeprdwIlSWrxEkLRgaBHaK7Ro5iMPiRJ0oIPVSvtCI6NVY8Cmxs/gvCam9GQWGZbL1P1KeE1GHXeQ+hdr5CLdoycVcy/hYxvKnd++8RbSJciCjSZz/O2U6Ad6pR8aSMPA7qa1/fUBzQuQfoC7jwKKZsOufYs0eAbRGmDUqU8unvLKaXj8in6lVNk/IBVG3z4hReh1O8AUH4NmpStqk5mcsKjzV5DMq7DiVNydmhmgskWC4n1XI1IzqigPSHkn69JrOdKNkPK8b9qQBzd1A+aax/8yN4WkMeagMjjYq4lIAIiIAIiIAIiIAIiIALyZ0MG10QSVUtABERABERABERABERABOQDh3TchuYGuN3x62kZ6DeMAy3lUG8wGDrp0J9aYT+Ay2Aoaf1dTleVL7G4/vVFpZupkpNLy5XdBoOhDsau1PgOeZQIGamwy14R0ADkBzhgIiINci5YLCNPtj9sC3//hoDJTdb6/SXv5ox1+QLJPTeYcZMMqUfdCXAj22LpoU9Z3u87RA6enY/WuOZC5IqkDHB9qnbAcWMapPcCLT+Cru39kGBY3OrgSXZWtysXGuvuV/K6+OBVGXtW9uNVY0noozZ3PsRDTQHAVzUAGc1+tZH0tl9yC5vaM6goiRml6ssdDpqz7qeBWvtprgw83z63IoQTNRNRXfaomaQe0jvNeg5dGfvixliUvStqdrVYTJ/XJXe6wxZo+AzgpGrrgVeEHIk/NO075JtvCu+0nJEkKr68+jU7+2McM/Hj99OgdWp+fzXM7bGxMiTvpinFYtllqzo/Fesx62c3u3nZ9f1+i2XHqrttftpbstvOqayquFMA9mFPUcHMhl6KLvoO6Tiocc1pDndSUTChfJhClOPcLmN25A2A6jPIn1WyCmRva0WSJEmDU+rai5j1M1sArqRKkrTqppTQ19j2IdfVSpeXc7qT5kLgXqbvkIXwdMiIWKSigOPRdUQ5Hlut1+O7ZiLnOF1O7iVWgdSq5K74RboWyYxxYNYT20/9d22JMg9W3coR7qTxADC0fRgZVCNUnyDhAZLRj3Ek5TpcT4WKAjpDF4hyAJ1pUKlO3udqCtRqtRfeDwnURmc+hzJV0iEXdxLBrOd+3L7UF3yt2pM5t1osDap0zWM82lQb6NqxqXXxw9jVKUkTf+SAKLu870fl37VOF4A848Vznt82y7kBz1IxM/70WmKKIiACIiACIiACIiACIiAC8kFAxDq7qFoCIiACIiACIiACIiACIiDvJL8OlM+/+bDgPyRmnPH4jp4UAKKUSqW8WH/4KsB0dN1qZU2FaGOyfneAtD7fh1AcOp1ubyyt0QlZCwDlqp0Zs0D1Nj8gW164Ekx0awEWIgFqC7MlgNzIVdftJ4PBpJHB9e4C7tK5xJk5rzDVRs+2QU5WAcPRC2RVg1PziT+QZ/svswwZ3WVrdwOlEmDLzPMKwl4bl7QJ52fD5zh/zazHFpuhdXJ+Z7w3Lwpwq52z/4DG48DkY8irgc+b/YKkR8hvID0RZ5KOLkNmtU4vIfk37Qmye8dQTgOqF2a9RzmA6XzHHo8c+3z1aOqPg3KIsuVl3L74WX7KnfULcvLYuTcQQI4cXoKcvYmXkOOmGxE6Xdhd2xf9+zHrJ8MAvo3U6UKtq0ezuwcsW1N1ZwAY1zxmYsdr/yAvZmLuLEPsfUsll0rMRWu1IVvrvIAsRA7U5kxNTS26VedrMevlYDdz45X5U1NTq/dgHakAslx4C2Am8R7cjNHGB+7yB8KTiJHuGLvd/qptz0jztvnlNoI3/8jH9ub0LxmPbB06PYUxxIVZzxHJmVPpjPz1af7qq7MHmwDaL+ycp2l8Mc1ot/cDfv0jJdPQcHPUYDAY2rEczR0EGpd2HZlX3YLhMhiKmoD+3EwLOK5DVy2ui0eroVefeXfVWFzFHsBTZJoFaXDcYDAYSoHFIjGyC4iACIiACIiACIiACIiACMj/OUSss4uqJSACIiACIiACIiACIiAfOMQ6Bcz+eOsl4G7zqaxFi6W+782Xh8McAOCY65mXr5L3NFf2APS9Ws6YvMOoxWKx2PyAvNpwBRa0pd8px7msCvQJMhkkXdr55uxoXScbAQib7jZ59/zJE7diG/hp18Y3h8ezg+mTJCnzlB+Q8jNxMHIRjluwzHzkGyQY+mO4P4irjvuDbISe8s6w6aF2fh6+UedBNlU+6Xjv4/NaN7U5tIwlL0OaMoMBSO31A6IZzegEGIwbAnyEbB52nMunwIQzigITG7Fvu1UQOG3WE3uwZreJ7OxbcXkrFlFYASxDZuKfBQN07fOjjTxKoU4PnFFmy75D/nb0s6iGdyEFlRA6bdYT68BUMB0iY1oRYkuWf4Pk354NBjjS7AckP9l4ZpML8GT+6DskGCY/WXgHktkMYdNmPbGj1BqG1awM6Y1z8hbSEWm5HWQBh9rjO2QurMVq/dzUWwLnKvyCzATNF1fgWIaUXEUOfguZC3Xx/QqQUc0gv0HuS1JZ0BXIq/aj+zUfA2zJ84n5xrhx3yEbDLlxl7inNO5dhowqiw8HvYVwYU9x1AoQVbrRaHQvQQzdwGwwjEfO+wEZeA64bR75119cADafILLV2vES6G9+1cngGDaYaHZ2yi8H6JzjxQDuh7arKxyyt1mtVqsHumdgYA6Q2+DlE/yA/Mkp52KVsvdPKvsvhcw3Vo2IuZaACIiACIiACIiAeANZMxcEr40rmyec6xbWxiXa8rq1ca25zH8BTrZIsxZexqkAAAAASUVORK5CYII=", "public": true } + ], + "scada": false, + "tags": [ + "administration", + "management" ] } \ No newline at end of file diff --git a/ui-ngx/src/app/shared/models/alarm.models.ts b/ui-ngx/src/app/shared/models/alarm.models.ts index bcf0d7b75d..590e866465 100644 --- a/ui-ngx/src/app/shared/models/alarm.models.ts +++ b/ui-ngx/src/app/shared/models/alarm.models.ts @@ -140,6 +140,7 @@ export interface AlarmCommentInfo extends AlarmComment { export interface AlarmInfo extends Alarm { originatorName: string; originatorLabel: string; + originatorDisplayName?: string; assignee: AlarmAssignee; } @@ -172,6 +173,7 @@ export const simulatedAlarm: AlarmInfo = { clearTs: 0, assignTs: 0, originatorName: 'Simulated', + originatorDisplayName: 'Simulated', originatorLabel: 'Simulated', assignee: { firstName: '', @@ -242,6 +244,11 @@ export const alarmFields: {[fieldName: string]: AlarmField} = { value: 'originatorName', name: 'alarm.originator' }, + originatorDisplayName: { + keyName: 'originatorDisplayName', + value: 'originatorDisplayName', + name: 'alarm.originator' + }, originatorLabel: { keyName: 'originatorLabel', value: 'originatorLabel', From b6604d997bfbebeee058b25949ebccab9e4eee53 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 9 Oct 2025 13:11:00 +0300 Subject: [PATCH 210/216] added originatorDisplayName to alarmInfo object --- .../server/controller/EntityQueryControllerTest.java | 5 ++++- .../org/thingsboard/server/common/data/alarm/AlarmInfo.java | 5 +++++ .../thingsboard/server/dao/sql/query/AlarmDataAdapter.java | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java index 0c40c2f868..775df230b6 100644 --- a/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/EntityQueryControllerTest.java @@ -432,14 +432,17 @@ public class EntityQueryControllerTest extends AbstractControllerTest { List alarmFields = new ArrayList<>(); alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "type")); + alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, "originatorDisplayName")); EntityTypeFilter assetTypeFilter = new EntityTypeFilter(); assetTypeFilter.setEntityType(EntityType.ASSET); AlarmDataQuery assetAlarmQuery = new AlarmDataQuery(assetTypeFilter, pageLink, null, null, null, alarmFields); PageData alarmPageData = findAlarmsByQueryAndCheck(assetAlarmQuery, 10); - List retrievedAlarmTypes = alarmPageData.getData().stream().map(Alarm::getType).toList(); + List retrievedAlarmTypes = alarmPageData.getData().stream().map(AlarmData::getType).toList(); assertThat(retrievedAlarmTypes).containsExactlyInAnyOrderElementsOf(assetAlarmTypes); + List retrievedAlarmDisplayName = alarmPageData.getData().stream().map(AlarmData::getOriginatorDisplayName).toList(); + assertThat(retrievedAlarmDisplayName).containsExactlyInAnyOrderElementsOf(assets.stream().map(Asset::getLabel).toList()); KeyFilter nameFilter = buildStringKeyFilter(EntityKeyType.ENTITY_FIELD, "name", StringFilterPredicate.StringOperation.STARTS_WITH, "Asset1"); List keyFilters = Collections.singletonList(nameFilter); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java index 7a80a09891..b316a2ef50 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java @@ -38,6 +38,11 @@ public class AlarmInfo extends Alarm { @Schema(description = "Alarm originator label", example = "Thermostat label") private String originatorLabel; + @Getter + @Setter + @Schema(description = "Originator display name", example = "Thermostat") + private String originatorDisplayName; + @Getter @Setter @Schema(description = "Alarm assignee") diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java index b250fc7337..f3e2ba83a6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java @@ -121,6 +121,7 @@ public class AlarmDataAdapter { AlarmData alarmData = new AlarmData(alarm, entityId); alarmData.setOriginatorName(originatorName); alarmData.setOriginatorLabel(originatorLabel); + alarmData.setOriginatorDisplayName(StringUtils.isBlank(originatorLabel) ? originatorName : originatorLabel); if (alarm.getAssigneeId() != null) { alarmData.setAssignee(new AlarmAssignee(alarm.getAssigneeId(), assigneeFirstName, assigneeLastName, assigneeEmail)); } From aefb7dcb41b3cc43320cebf3f7ee02e4b4288a31 Mon Sep 17 00:00:00 2001 From: VIacheslavKlimov Date: Thu, 9 Oct 2025 14:50:03 +0300 Subject: [PATCH 211/216] Version set to 4.2.1-RC --- application/pom.xml | 2 +- common/actor/pom.xml | 2 +- common/cache/pom.xml | 2 +- common/cluster-api/pom.xml | 2 +- common/coap-server/pom.xml | 2 +- common/dao-api/pom.xml | 2 +- common/data/pom.xml | 2 +- common/discovery-api/pom.xml | 2 +- common/edge-api/pom.xml | 2 +- common/edqs/pom.xml | 2 +- common/message/pom.xml | 2 +- common/pom.xml | 2 +- common/proto/pom.xml | 2 +- common/queue/pom.xml | 2 +- common/script/pom.xml | 2 +- common/script/remote-js-client/pom.xml | 2 +- common/script/script-api/pom.xml | 2 +- common/stats/pom.xml | 2 +- common/transport/coap/pom.xml | 2 +- common/transport/http/pom.xml | 2 +- common/transport/lwm2m/pom.xml | 2 +- common/transport/mqtt/pom.xml | 2 +- common/transport/pom.xml | 2 +- common/transport/snmp/pom.xml | 2 +- common/transport/transport-api/pom.xml | 2 +- common/util/pom.xml | 2 +- common/version-control/pom.xml | 2 +- dao/pom.xml | 2 +- edqs/pom.xml | 2 +- monitoring/pom.xml | 2 +- msa/black-box-tests/pom.xml | 2 +- msa/edqs/pom.xml | 2 +- msa/js-executor/package.json | 2 +- msa/js-executor/pom.xml | 2 +- msa/monitoring/pom.xml | 2 +- msa/pom.xml | 2 +- msa/tb-node/pom.xml | 2 +- msa/tb/pom.xml | 2 +- msa/transport/coap/pom.xml | 2 +- msa/transport/http/pom.xml | 2 +- msa/transport/lwm2m/pom.xml | 2 +- msa/transport/mqtt/pom.xml | 2 +- msa/transport/pom.xml | 2 +- msa/transport/snmp/pom.xml | 2 +- msa/vc-executor-docker/pom.xml | 2 +- msa/vc-executor/pom.xml | 2 +- msa/web-ui/package.json | 2 +- msa/web-ui/pom.xml | 2 +- netty-mqtt/pom.xml | 4 ++-- pom.xml | 2 +- rest-client/pom.xml | 2 +- rule-engine/pom.xml | 2 +- rule-engine/rule-engine-api/pom.xml | 2 +- rule-engine/rule-engine-components/pom.xml | 2 +- tools/pom.xml | 2 +- transport/coap/pom.xml | 2 +- transport/http/pom.xml | 2 +- transport/lwm2m/pom.xml | 2 +- transport/mqtt/pom.xml | 2 +- transport/pom.xml | 2 +- transport/snmp/pom.xml | 2 +- ui-ngx/package.json | 2 +- ui-ngx/pom.xml | 2 +- 63 files changed, 64 insertions(+), 64 deletions(-) diff --git a/application/pom.xml b/application/pom.xml index 0413f7732c..b415ee08a6 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC thingsboard application diff --git a/common/actor/pom.xml b/common/actor/pom.xml index a6efdda77d..4149fe2ad9 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC common org.thingsboard.common diff --git a/common/cache/pom.xml b/common/cache/pom.xml index 3088dad098..6831156fde 100644 --- a/common/cache/pom.xml +++ b/common/cache/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC common org.thingsboard.common diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index 939dfb1e16..78ef7980e8 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC common org.thingsboard.common diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml index fbfbe78b55..b24fd98f5d 100644 --- a/common/coap-server/pom.xml +++ b/common/coap-server/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC common org.thingsboard.common diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index 636c7b9ef4..555b4a54bb 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC common org.thingsboard.common diff --git a/common/data/pom.xml b/common/data/pom.xml index da2a0970b1..f27df3687f 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC common org.thingsboard.common diff --git a/common/discovery-api/pom.xml b/common/discovery-api/pom.xml index 4f9ef70c5b..cb5238a664 100644 --- a/common/discovery-api/pom.xml +++ b/common/discovery-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC common org.thingsboard.common diff --git a/common/edge-api/pom.xml b/common/edge-api/pom.xml index 69a01be812..2b7db0cbd3 100644 --- a/common/edge-api/pom.xml +++ b/common/edge-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC common org.thingsboard.common diff --git a/common/edqs/pom.xml b/common/edqs/pom.xml index 140eabd270..cc5c5db14e 100644 --- a/common/edqs/pom.xml +++ b/common/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC common org.thingsboard.common diff --git a/common/message/pom.xml b/common/message/pom.xml index 834a041127..c84a4fde2d 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC common org.thingsboard.common diff --git a/common/pom.xml b/common/pom.xml index 563f1ec3ab..f45a2bdd28 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC thingsboard common diff --git a/common/proto/pom.xml b/common/proto/pom.xml index 6357cae711..30d4b0bc68 100644 --- a/common/proto/pom.xml +++ b/common/proto/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC common org.thingsboard.common diff --git a/common/queue/pom.xml b/common/queue/pom.xml index ccc0eadf1a..69adac81b4 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC common org.thingsboard.common diff --git a/common/script/pom.xml b/common/script/pom.xml index c203bfdbf2..ab5ac032c7 100644 --- a/common/script/pom.xml +++ b/common/script/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC common org.thingsboard.common diff --git a/common/script/remote-js-client/pom.xml b/common/script/remote-js-client/pom.xml index c2f5ee2da2..1a3e00a88c 100644 --- a/common/script/remote-js-client/pom.xml +++ b/common/script/remote-js-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-RC + 4.2.1-RC script org.thingsboard.common.script diff --git a/common/script/script-api/pom.xml b/common/script/script-api/pom.xml index e35dec1b1b..316f823a44 100644 --- a/common/script/script-api/pom.xml +++ b/common/script/script-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-RC + 4.2.1-RC script org.thingsboard.common.script diff --git a/common/stats/pom.xml b/common/stats/pom.xml index 090732d175..4bf24411dc 100644 --- a/common/stats/pom.xml +++ b/common/stats/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC common org.thingsboard.common diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index aaa9713e02..be3c62c057 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-RC + 4.2.1-RC transport org.thingsboard.common.transport diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index 29d633d83e..a136079cb5 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-RC + 4.2.1-RC transport org.thingsboard.common.transport diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index 4d7013a2bc..904e359bcc 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-RC + 4.2.1-RC transport org.thingsboard.common.transport diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index 2baee9bda2..45f9b76af7 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-RC + 4.2.1-RC transport org.thingsboard.common.transport diff --git a/common/transport/pom.xml b/common/transport/pom.xml index 2289b53aed..f786bcdd0e 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC common org.thingsboard.common diff --git a/common/transport/snmp/pom.xml b/common/transport/snmp/pom.xml index 71bd0c17fe..d3b2c46d69 100644 --- a/common/transport/snmp/pom.xml +++ b/common/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.common - 4.2.0-RC + 4.2.1-RC transport diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index 29ace644a9..a3fee460e1 100644 --- a/common/transport/transport-api/pom.xml +++ b/common/transport/transport-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.0-RC + 4.2.1-RC transport org.thingsboard.common.transport diff --git a/common/util/pom.xml b/common/util/pom.xml index 3f07b2c05b..ebedf5dea3 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC common org.thingsboard.common diff --git a/common/version-control/pom.xml b/common/version-control/pom.xml index 19278b8d13..b6a0ea1d2e 100644 --- a/common/version-control/pom.xml +++ b/common/version-control/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC common org.thingsboard.common diff --git a/dao/pom.xml b/dao/pom.xml index a32d0c4f6d..a1843959df 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC thingsboard dao diff --git a/edqs/pom.xml b/edqs/pom.xml index c29710490b..d648d9d786 100644 --- a/edqs/pom.xml +++ b/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC thingsboard edqs diff --git a/monitoring/pom.xml b/monitoring/pom.xml index cee8fc0a6b..77874c5ac9 100644 --- a/monitoring/pom.xml +++ b/monitoring/pom.xml @@ -21,7 +21,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC thingsboard diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index 84509ba48f..fed33f0002 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.0-RC + 4.2.1-RC msa org.thingsboard.msa diff --git a/msa/edqs/pom.xml b/msa/edqs/pom.xml index d1f8291e2e..c5be28e7bb 100644 --- a/msa/edqs/pom.xml +++ b/msa/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC msa org.thingsboard.msa diff --git a/msa/js-executor/package.json b/msa/js-executor/package.json index 6e5f590260..220561f6e7 100644 --- a/msa/js-executor/package.json +++ b/msa/js-executor/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-js-executor", "private": true, - "version": "4.2.0", + "version": "4.2.1", "description": "ThingsBoard JavaScript Executor Microservice", "main": "server.ts", "bin": "server.js", diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index 01ac469b39..651b142160 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC msa org.thingsboard.msa diff --git a/msa/monitoring/pom.xml b/msa/monitoring/pom.xml index 61f492dca8..abd12ed9e0 100644 --- a/msa/monitoring/pom.xml +++ b/msa/monitoring/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC msa diff --git a/msa/pom.xml b/msa/pom.xml index 58482a32cf..6fcf7c3523 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC thingsboard msa diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index 5f53d868c9..0103bf9352 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC msa org.thingsboard.msa diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index 646b03bf14..28e1366a73 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC msa org.thingsboard.msa diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml index 4f510f937b..8aefc94122 100644 --- a/msa/transport/coap/pom.xml +++ b/msa/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.0-RC + 4.2.1-RC transport org.thingsboard.msa.transport diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml index d9159cd0db..a1481203de 100644 --- a/msa/transport/http/pom.xml +++ b/msa/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.0-RC + 4.2.1-RC transport org.thingsboard.msa.transport diff --git a/msa/transport/lwm2m/pom.xml b/msa/transport/lwm2m/pom.xml index 9972d66a18..7af2b16588 100644 --- a/msa/transport/lwm2m/pom.xml +++ b/msa/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.0-RC + 4.2.1-RC transport org.thingsboard.msa.transport diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml index 185addeb3b..176dca19eb 100644 --- a/msa/transport/mqtt/pom.xml +++ b/msa/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.0-RC + 4.2.1-RC transport org.thingsboard.msa.transport diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml index 193d01ef06..afbab0c450 100644 --- a/msa/transport/pom.xml +++ b/msa/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC msa org.thingsboard.msa diff --git a/msa/transport/snmp/pom.xml b/msa/transport/snmp/pom.xml index ea3d6847c0..553ed8bb37 100644 --- a/msa/transport/snmp/pom.xml +++ b/msa/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.msa transport - 4.2.0-RC + 4.2.1-RC org.thingsboard.msa.transport diff --git a/msa/vc-executor-docker/pom.xml b/msa/vc-executor-docker/pom.xml index a77eb1d206..d0a291e246 100644 --- a/msa/vc-executor-docker/pom.xml +++ b/msa/vc-executor-docker/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC msa org.thingsboard.msa diff --git a/msa/vc-executor/pom.xml b/msa/vc-executor/pom.xml index 0e91749cbe..cf777c4c96 100644 --- a/msa/vc-executor/pom.xml +++ b/msa/vc-executor/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.0-RC + 4.2.1-RC msa org.thingsboard.msa diff --git a/msa/web-ui/package.json b/msa/web-ui/package.json index cd5f87338b..2fcf6ab914 100644 --- a/msa/web-ui/package.json +++ b/msa/web-ui/package.json @@ -1,7 +1,7 @@ { "name": "thingsboard-web-ui", "private": true, - "version": "4.2.0", + "version": "4.2.1", "description": "ThingsBoard Web UI Microservice", "main": "server.ts", "bin": "server.js", diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml index 8acd58b2b9..a56d0ca9d9 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC msa org.thingsboard.msa diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index bbcfcfea18..08f2a55f4a 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -19,11 +19,11 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC thingsboard netty-mqtt - 4.2.0-RC + 4.2.1-RC jar Netty MQTT Client diff --git a/pom.xml b/pom.xml index fd49af548f..1283dcc01c 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 4.2.0-RC + 4.2.1-RC pom Thingsboard diff --git a/rest-client/pom.xml b/rest-client/pom.xml index 07499f1129..e6e82da954 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC thingsboard rest-client diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml index 120d27ac89..8a9ad96d95 100644 --- a/rule-engine/pom.xml +++ b/rule-engine/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC thingsboard rule-engine diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index a6ee3492e5..8f449a5e64 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 3f2ee164df..4fc5a55c5e 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC rule-engine org.thingsboard.rule-engine diff --git a/tools/pom.xml b/tools/pom.xml index 1224e73777..cce1466971 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC thingsboard tools diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index c96f4fddf7..8ae86d381f 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC transport org.thingsboard.transport diff --git a/transport/http/pom.xml b/transport/http/pom.xml index e32915c51e..487e1de186 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC transport org.thingsboard.transport diff --git a/transport/lwm2m/pom.xml b/transport/lwm2m/pom.xml index af17bf703b..30effe6e42 100644 --- a/transport/lwm2m/pom.xml +++ b/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC transport org.thingsboard.transport diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index 87c98f9d78..a609e8d002 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC transport org.thingsboard.transport diff --git a/transport/pom.xml b/transport/pom.xml index 8ddcf992d8..c6a82349f5 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC thingsboard transport diff --git a/transport/snmp/pom.xml b/transport/snmp/pom.xml index 09901661a6..1894f09787 100644 --- a/transport/snmp/pom.xml +++ b/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.0-RC + 4.2.1-RC transport diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 4ba512a024..ea7d7c80fb 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -1,6 +1,6 @@ { "name": "thingsboard", - "version": "4.2.0", + "version": "4.2.1", "scripts": { "ng": "ng", "start": "node --max_old_space_size=8048 ./node_modules/@angular/cli/bin/ng serve --configuration development --host 0.0.0.0 --open", diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml index 2f5130014a..905a1a8aca 100644 --- a/ui-ngx/pom.xml +++ b/ui-ngx/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.0-RC + 4.2.1-RC thingsboard org.thingsboard From ca8b110dc1c22eec9ae2d8c202da777c9a5990d4 Mon Sep 17 00:00:00 2001 From: VIacheslavKlimov Date: Thu, 9 Oct 2025 15:00:13 +0300 Subject: [PATCH 212/216] Cleanup upgrade from 4.2.0 to 4.2.1 --- .../main/data/upgrade/basic/schema_update.sql | 30 ------------------- .../DefaultDatabaseSchemaSettingsService.java | 2 +- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/application/src/main/data/upgrade/basic/schema_update.sql b/application/src/main/data/upgrade/basic/schema_update.sql index add832ea6e..016e786776 100644 --- a/application/src/main/data/upgrade/basic/schema_update.sql +++ b/application/src/main/data/upgrade/basic/schema_update.sql @@ -14,33 +14,3 @@ -- limitations under the License. -- --- UPDATE OTA PACKAGE EXTERNAL ID START - -ALTER TABLE ota_package - ADD COLUMN IF NOT EXISTS external_id uuid; - -DO -$$ - BEGIN - IF NOT EXISTS(SELECT 1 FROM pg_constraint WHERE conname = 'ota_package_external_id_unq_key') THEN - ALTER TABLE ota_package ADD CONSTRAINT ota_package_external_id_unq_key UNIQUE (tenant_id, external_id); - END IF; - END; -$$; - --- UPDATE OTA PACKAGE EXTERNAL ID END - --- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT START - -DROP INDEX IF EXISTS idx_device_external_id; -DROP INDEX IF EXISTS idx_device_profile_external_id; -DROP INDEX IF EXISTS idx_asset_external_id; -DROP INDEX IF EXISTS idx_entity_view_external_id; -DROP INDEX IF EXISTS idx_rule_chain_external_id; -DROP INDEX IF EXISTS idx_dashboard_external_id; -DROP INDEX IF EXISTS idx_customer_external_id; -DROP INDEX IF EXISTS idx_widgets_bundle_external_id; - --- DROP INDEXES THAT DUPLICATE UNIQUE CONSTRAINT END - -ALTER TABLE mobile_app ADD COLUMN IF NOT EXISTS title varchar(255); \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java index e5bd026fb7..f41a530630 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java @@ -32,7 +32,7 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti // This list should include all versions which are compatible for the upgrade. // The compatibility cycle usually breaks when we have some scripts written in Java that may not work after new release. - private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.1.0"); + private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.2.0"); private final ProjectInfo projectInfo; private final JdbcTemplate jdbcTemplate; From ad6ab9cc536537884724954b210ed355d9520ca7 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 9 Oct 2025 15:04:51 +0300 Subject: [PATCH 213/216] AI node: fixed resources validation --- .../server/actors/ruleChain/DefaultTbContext.java | 14 ++++++-------- .../org/thingsboard/rule/engine/api/TbContext.java | 2 +- .../org/thingsboard/rule/engine/ai/TbAiNode.java | 2 +- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java index 88b04c7613..03830c1c4c 100644 --- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java +++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java @@ -1063,18 +1063,16 @@ public class DefaultTbContext implements TbContext { @Override public void checkTenantEntity(EntityId entityId) throws TbNodeException { TenantId actualTenantId = TenantIdLoader.findTenantId(this, entityId); - assertSameTenantId(actualTenantId, entityId); + if (!getTenantId().equals(actualTenantId)) { + throw new TbNodeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant.", true); + } } @Override - public & HasTenantId, I extends EntityId> void checkTenantEntity(E entity) throws TbNodeException { + public & HasTenantId, I extends EntityId> void checkTenantOrSystemEntity(E entity) throws TbNodeException { TenantId actualTenantId = entity.getTenantId(); - assertSameTenantId(actualTenantId, entity.getId()); - } - - private void assertSameTenantId(TenantId tenantId, EntityId entityId) throws TbNodeException { - if (!getTenantId().equals(tenantId)) { - throw new TbNodeException("Entity with id: '" + entityId + "' specified in the configuration doesn't belong to the current tenant.", true); + if (!getTenantId().equals(actualTenantId) && !actualTenantId.isSysTenantId()) { + throw new TbNodeException("Entity with id: '" + entity.getId() + "' specified in the configuration doesn't belong to the current or system tenant.", true); } } diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java index 920f00ed27..e703a7b256 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbContext.java @@ -255,7 +255,7 @@ public interface TbContext { void checkTenantEntity(EntityId entityId) throws TbNodeException; - & HasTenantId, I extends EntityId> void checkTenantEntity(E entity) throws TbNodeException; + & HasTenantId, I extends EntityId> void checkTenantOrSystemEntity(E entity) throws TbNodeException; boolean isLocalEntity(EntityId entityId); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java index 054b06fb54..654ed3f448 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/ai/TbAiNode.java @@ -261,7 +261,7 @@ public final class TbAiNode extends TbAbstractExternalNode implements TbNode { if (!ResourceType.GENERAL.equals(resource.getResourceType())) { throw new TbNodeException("[" + ctx.getTenantId() + "] Resource with ID: [" + tbResourceId + "] has unsupported resource type: " + resource.getResourceType(), true); } - ctx.checkTenantEntity(resource); + ctx.checkTenantOrSystemEntity(resource); } private ListenableFuture> loadResources(TbContext ctx) { From 1c2bdd4c14567a36a9bca74d6e2a23cd9e815bc9 Mon Sep 17 00:00:00 2001 From: VIacheslavKlimov Date: Thu, 9 Oct 2025 16:08:06 +0300 Subject: [PATCH 214/216] TMP version set to 4.2.1-RC --- application/pom.xml | 2 +- common/actor/pom.xml | 2 +- common/cache/pom.xml | 2 +- common/cluster-api/pom.xml | 2 +- common/coap-server/pom.xml | 2 +- common/dao-api/pom.xml | 2 +- common/data/pom.xml | 2 +- common/discovery-api/pom.xml | 2 +- common/edge-api/pom.xml | 2 +- common/edqs/pom.xml | 2 +- common/message/pom.xml | 2 +- common/pom.xml | 2 +- common/proto/pom.xml | 2 +- common/queue/pom.xml | 2 +- common/script/pom.xml | 2 +- common/script/remote-js-client/pom.xml | 2 +- common/script/script-api/pom.xml | 2 +- common/stats/pom.xml | 2 +- common/transport/coap/pom.xml | 2 +- common/transport/http/pom.xml | 2 +- common/transport/lwm2m/pom.xml | 2 +- common/transport/mqtt/pom.xml | 2 +- common/transport/pom.xml | 2 +- common/transport/snmp/pom.xml | 2 +- common/transport/transport-api/pom.xml | 2 +- common/util/pom.xml | 2 +- common/version-control/pom.xml | 2 +- dao/pom.xml | 2 +- edqs/pom.xml | 2 +- monitoring/pom.xml | 2 +- msa/black-box-tests/pom.xml | 2 +- msa/edqs/pom.xml | 2 +- msa/js-executor/pom.xml | 2 +- msa/monitoring/pom.xml | 2 +- msa/pom.xml | 2 +- msa/tb-node/pom.xml | 2 +- msa/tb/pom.xml | 2 +- msa/transport/coap/pom.xml | 2 +- msa/transport/http/pom.xml | 2 +- msa/transport/lwm2m/pom.xml | 2 +- msa/transport/mqtt/pom.xml | 2 +- msa/transport/pom.xml | 2 +- msa/transport/snmp/pom.xml | 2 +- msa/vc-executor-docker/pom.xml | 2 +- msa/vc-executor/pom.xml | 2 +- msa/web-ui/pom.xml | 2 +- netty-mqtt/pom.xml | 4 ++-- pom.xml | 2 +- rest-client/pom.xml | 2 +- rule-engine/pom.xml | 2 +- rule-engine/rule-engine-api/pom.xml | 2 +- rule-engine/rule-engine-components/pom.xml | 2 +- tools/pom.xml | 2 +- transport/coap/pom.xml | 2 +- transport/http/pom.xml | 2 +- transport/lwm2m/pom.xml | 2 +- transport/mqtt/pom.xml | 2 +- transport/pom.xml | 2 +- transport/snmp/pom.xml | 2 +- ui-ngx/pom.xml | 2 +- 60 files changed, 61 insertions(+), 61 deletions(-) diff --git a/application/pom.xml b/application/pom.xml index 4cbd9c3b64..b415ee08a6 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC thingsboard application diff --git a/common/actor/pom.xml b/common/actor/pom.xml index 92b174ea61..4149fe2ad9 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC common org.thingsboard.common diff --git a/common/cache/pom.xml b/common/cache/pom.xml index fcf52e01b7..6831156fde 100644 --- a/common/cache/pom.xml +++ b/common/cache/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC common org.thingsboard.common diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index 1f4b5ff41b..78ef7980e8 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC common org.thingsboard.common diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml index 27ff645a0f..b24fd98f5d 100644 --- a/common/coap-server/pom.xml +++ b/common/coap-server/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC common org.thingsboard.common diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index 160351d55d..555b4a54bb 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC common org.thingsboard.common diff --git a/common/data/pom.xml b/common/data/pom.xml index 779565ed8e..f27df3687f 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC common org.thingsboard.common diff --git a/common/discovery-api/pom.xml b/common/discovery-api/pom.xml index 3ae12c1bdb..cb5238a664 100644 --- a/common/discovery-api/pom.xml +++ b/common/discovery-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC common org.thingsboard.common diff --git a/common/edge-api/pom.xml b/common/edge-api/pom.xml index 7c2967cf3d..2b7db0cbd3 100644 --- a/common/edge-api/pom.xml +++ b/common/edge-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC common org.thingsboard.common diff --git a/common/edqs/pom.xml b/common/edqs/pom.xml index f7616d5b37..cc5c5db14e 100644 --- a/common/edqs/pom.xml +++ b/common/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC common org.thingsboard.common diff --git a/common/message/pom.xml b/common/message/pom.xml index 757c5de251..c84a4fde2d 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC common org.thingsboard.common diff --git a/common/pom.xml b/common/pom.xml index a9195e49d4..f45a2bdd28 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC thingsboard common diff --git a/common/proto/pom.xml b/common/proto/pom.xml index 09f9596d0c..30d4b0bc68 100644 --- a/common/proto/pom.xml +++ b/common/proto/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC common org.thingsboard.common diff --git a/common/queue/pom.xml b/common/queue/pom.xml index ffee41f8a9..69adac81b4 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC common org.thingsboard.common diff --git a/common/script/pom.xml b/common/script/pom.xml index d0a3b47218..ab5ac032c7 100644 --- a/common/script/pom.xml +++ b/common/script/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC common org.thingsboard.common diff --git a/common/script/remote-js-client/pom.xml b/common/script/remote-js-client/pom.xml index b1547d2e4b..1a3e00a88c 100644 --- a/common/script/remote-js-client/pom.xml +++ b/common/script/remote-js-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.3.0-SNAPSHOT + 4.2.1-RC script org.thingsboard.common.script diff --git a/common/script/script-api/pom.xml b/common/script/script-api/pom.xml index b0fcf1c4ae..316f823a44 100644 --- a/common/script/script-api/pom.xml +++ b/common/script/script-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.3.0-SNAPSHOT + 4.2.1-RC script org.thingsboard.common.script diff --git a/common/stats/pom.xml b/common/stats/pom.xml index e44e098a39..4bf24411dc 100644 --- a/common/stats/pom.xml +++ b/common/stats/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC common org.thingsboard.common diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index 37f33470b2..be3c62c057 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.3.0-SNAPSHOT + 4.2.1-RC transport org.thingsboard.common.transport diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index e49af5b0a2..a136079cb5 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.3.0-SNAPSHOT + 4.2.1-RC transport org.thingsboard.common.transport diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index 1787c851cd..904e359bcc 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.3.0-SNAPSHOT + 4.2.1-RC transport org.thingsboard.common.transport diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index 9df8cf918e..45f9b76af7 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.3.0-SNAPSHOT + 4.2.1-RC transport org.thingsboard.common.transport diff --git a/common/transport/pom.xml b/common/transport/pom.xml index 3bd21c96de..f786bcdd0e 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC common org.thingsboard.common diff --git a/common/transport/snmp/pom.xml b/common/transport/snmp/pom.xml index 1286073a5f..d3b2c46d69 100644 --- a/common/transport/snmp/pom.xml +++ b/common/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.common - 4.3.0-SNAPSHOT + 4.2.1-RC transport diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index 19b075cb61..a3fee460e1 100644 --- a/common/transport/transport-api/pom.xml +++ b/common/transport/transport-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.3.0-SNAPSHOT + 4.2.1-RC transport org.thingsboard.common.transport diff --git a/common/util/pom.xml b/common/util/pom.xml index 804ed0de38..ebedf5dea3 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC common org.thingsboard.common diff --git a/common/version-control/pom.xml b/common/version-control/pom.xml index 58c059915c..b6a0ea1d2e 100644 --- a/common/version-control/pom.xml +++ b/common/version-control/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC common org.thingsboard.common diff --git a/dao/pom.xml b/dao/pom.xml index fd6af62330..a1843959df 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC thingsboard dao diff --git a/edqs/pom.xml b/edqs/pom.xml index 4a4c05be71..d648d9d786 100644 --- a/edqs/pom.xml +++ b/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC thingsboard edqs diff --git a/monitoring/pom.xml b/monitoring/pom.xml index 6dbcf85f07..77874c5ac9 100644 --- a/monitoring/pom.xml +++ b/monitoring/pom.xml @@ -21,7 +21,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC thingsboard diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index 358c524e06..fed33f0002 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC msa org.thingsboard.msa diff --git a/msa/edqs/pom.xml b/msa/edqs/pom.xml index 2dd4a9a584..c5be28e7bb 100644 --- a/msa/edqs/pom.xml +++ b/msa/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC msa org.thingsboard.msa diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index 9730e7f33b..651b142160 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC msa org.thingsboard.msa diff --git a/msa/monitoring/pom.xml b/msa/monitoring/pom.xml index cb5b609e43..abd12ed9e0 100644 --- a/msa/monitoring/pom.xml +++ b/msa/monitoring/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC msa diff --git a/msa/pom.xml b/msa/pom.xml index 7fbc5d3c02..6fcf7c3523 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC thingsboard msa diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index 8df294260b..0103bf9352 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC msa org.thingsboard.msa diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index 1cbbf9d1ec..28e1366a73 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC msa org.thingsboard.msa diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml index 256220078d..8aefc94122 100644 --- a/msa/transport/coap/pom.xml +++ b/msa/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.3.0-SNAPSHOT + 4.2.1-RC transport org.thingsboard.msa.transport diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml index 7e5871729d..a1481203de 100644 --- a/msa/transport/http/pom.xml +++ b/msa/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.3.0-SNAPSHOT + 4.2.1-RC transport org.thingsboard.msa.transport diff --git a/msa/transport/lwm2m/pom.xml b/msa/transport/lwm2m/pom.xml index ca6977eac6..7af2b16588 100644 --- a/msa/transport/lwm2m/pom.xml +++ b/msa/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.3.0-SNAPSHOT + 4.2.1-RC transport org.thingsboard.msa.transport diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml index 172c4facb0..176dca19eb 100644 --- a/msa/transport/mqtt/pom.xml +++ b/msa/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.3.0-SNAPSHOT + 4.2.1-RC transport org.thingsboard.msa.transport diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml index a6cff4fd0b..afbab0c450 100644 --- a/msa/transport/pom.xml +++ b/msa/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC msa org.thingsboard.msa diff --git a/msa/transport/snmp/pom.xml b/msa/transport/snmp/pom.xml index 79734822aa..553ed8bb37 100644 --- a/msa/transport/snmp/pom.xml +++ b/msa/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.msa transport - 4.3.0-SNAPSHOT + 4.2.1-RC org.thingsboard.msa.transport diff --git a/msa/vc-executor-docker/pom.xml b/msa/vc-executor-docker/pom.xml index 85634d8688..d0a291e246 100644 --- a/msa/vc-executor-docker/pom.xml +++ b/msa/vc-executor-docker/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC msa org.thingsboard.msa diff --git a/msa/vc-executor/pom.xml b/msa/vc-executor/pom.xml index 0acd863612..cf777c4c96 100644 --- a/msa/vc-executor/pom.xml +++ b/msa/vc-executor/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC msa org.thingsboard.msa diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml index db7ed1424a..a56d0ca9d9 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC msa org.thingsboard.msa diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index a09d1e2409..08f2a55f4a 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -19,11 +19,11 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC thingsboard netty-mqtt - 4.3.0-SNAPSHOT + 4.2.1-RC jar Netty MQTT Client diff --git a/pom.xml b/pom.xml index 1676e3b56c..1283dcc01c 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC pom Thingsboard diff --git a/rest-client/pom.xml b/rest-client/pom.xml index c3cf2460dd..e6e82da954 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC thingsboard rest-client diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml index 7abb7be3a3..8a9ad96d95 100644 --- a/rule-engine/pom.xml +++ b/rule-engine/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC thingsboard rule-engine diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index 59397d4a92..8f449a5e64 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 98c7fde665..4fc5a55c5e 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC rule-engine org.thingsboard.rule-engine diff --git a/tools/pom.xml b/tools/pom.xml index 0b63d84b0a..cce1466971 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC thingsboard tools diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index 67f16917d3..8ae86d381f 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC transport org.thingsboard.transport diff --git a/transport/http/pom.xml b/transport/http/pom.xml index 371ecb84ca..487e1de186 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC transport org.thingsboard.transport diff --git a/transport/lwm2m/pom.xml b/transport/lwm2m/pom.xml index 66a3066a88..30effe6e42 100644 --- a/transport/lwm2m/pom.xml +++ b/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC transport org.thingsboard.transport diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index 57a73b1bbe..a609e8d002 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC transport org.thingsboard.transport diff --git a/transport/pom.xml b/transport/pom.xml index a8658bbf40..c6a82349f5 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC thingsboard transport diff --git a/transport/snmp/pom.xml b/transport/snmp/pom.xml index d5c3d1fe02..1894f09787 100644 --- a/transport/snmp/pom.xml +++ b/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC transport diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml index 56fc4ed628..905a1a8aca 100644 --- a/ui-ngx/pom.xml +++ b/ui-ngx/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.3.0-SNAPSHOT + 4.2.1-RC thingsboard org.thingsboard From 514812ae024ab346a8c6044570277440952b0ced Mon Sep 17 00:00:00 2001 From: VIacheslavKlimov Date: Thu, 9 Oct 2025 16:11:25 +0300 Subject: [PATCH 215/216] Version set to 4.3.0-SNAPSHOT --- application/pom.xml | 2 +- common/actor/pom.xml | 2 +- common/cache/pom.xml | 2 +- common/cluster-api/pom.xml | 2 +- common/coap-server/pom.xml | 2 +- common/dao-api/pom.xml | 2 +- common/data/pom.xml | 2 +- common/discovery-api/pom.xml | 2 +- common/edge-api/pom.xml | 2 +- common/edqs/pom.xml | 2 +- common/message/pom.xml | 2 +- common/pom.xml | 2 +- common/proto/pom.xml | 2 +- common/queue/pom.xml | 2 +- common/script/pom.xml | 2 +- common/script/remote-js-client/pom.xml | 2 +- common/script/script-api/pom.xml | 2 +- common/stats/pom.xml | 2 +- common/transport/coap/pom.xml | 2 +- common/transport/http/pom.xml | 2 +- common/transport/lwm2m/pom.xml | 2 +- common/transport/mqtt/pom.xml | 2 +- common/transport/pom.xml | 2 +- common/transport/snmp/pom.xml | 2 +- common/transport/transport-api/pom.xml | 2 +- common/util/pom.xml | 2 +- common/version-control/pom.xml | 2 +- dao/pom.xml | 2 +- edqs/pom.xml | 2 +- monitoring/pom.xml | 2 +- msa/black-box-tests/pom.xml | 2 +- msa/edqs/pom.xml | 2 +- msa/js-executor/pom.xml | 2 +- msa/monitoring/pom.xml | 2 +- msa/pom.xml | 2 +- msa/tb-node/pom.xml | 2 +- msa/tb/pom.xml | 2 +- msa/transport/coap/pom.xml | 2 +- msa/transport/http/pom.xml | 2 +- msa/transport/lwm2m/pom.xml | 2 +- msa/transport/mqtt/pom.xml | 2 +- msa/transport/pom.xml | 2 +- msa/transport/snmp/pom.xml | 2 +- msa/vc-executor-docker/pom.xml | 2 +- msa/vc-executor/pom.xml | 2 +- msa/web-ui/pom.xml | 2 +- netty-mqtt/pom.xml | 4 ++-- pom.xml | 2 +- rest-client/pom.xml | 2 +- rule-engine/pom.xml | 2 +- rule-engine/rule-engine-api/pom.xml | 2 +- rule-engine/rule-engine-components/pom.xml | 2 +- tools/pom.xml | 2 +- transport/coap/pom.xml | 2 +- transport/http/pom.xml | 2 +- transport/lwm2m/pom.xml | 2 +- transport/mqtt/pom.xml | 2 +- transport/pom.xml | 2 +- transport/snmp/pom.xml | 2 +- ui-ngx/pom.xml | 2 +- 60 files changed, 61 insertions(+), 61 deletions(-) diff --git a/application/pom.xml b/application/pom.xml index b415ee08a6..4cbd9c3b64 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT thingsboard application diff --git a/common/actor/pom.xml b/common/actor/pom.xml index 4149fe2ad9..92b174ea61 100644 --- a/common/actor/pom.xml +++ b/common/actor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/cache/pom.xml b/common/cache/pom.xml index 6831156fde..fcf52e01b7 100644 --- a/common/cache/pom.xml +++ b/common/cache/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/cluster-api/pom.xml b/common/cluster-api/pom.xml index 78ef7980e8..1f4b5ff41b 100644 --- a/common/cluster-api/pom.xml +++ b/common/cluster-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/coap-server/pom.xml b/common/coap-server/pom.xml index b24fd98f5d..27ff645a0f 100644 --- a/common/coap-server/pom.xml +++ b/common/coap-server/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/dao-api/pom.xml b/common/dao-api/pom.xml index 555b4a54bb..160351d55d 100644 --- a/common/dao-api/pom.xml +++ b/common/dao-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/data/pom.xml b/common/data/pom.xml index f27df3687f..779565ed8e 100644 --- a/common/data/pom.xml +++ b/common/data/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/discovery-api/pom.xml b/common/discovery-api/pom.xml index cb5238a664..3ae12c1bdb 100644 --- a/common/discovery-api/pom.xml +++ b/common/discovery-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/edge-api/pom.xml b/common/edge-api/pom.xml index 2b7db0cbd3..7c2967cf3d 100644 --- a/common/edge-api/pom.xml +++ b/common/edge-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/edqs/pom.xml b/common/edqs/pom.xml index cc5c5db14e..f7616d5b37 100644 --- a/common/edqs/pom.xml +++ b/common/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/message/pom.xml b/common/message/pom.xml index c84a4fde2d..757c5de251 100644 --- a/common/message/pom.xml +++ b/common/message/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/pom.xml b/common/pom.xml index f45a2bdd28..a9195e49d4 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT thingsboard common diff --git a/common/proto/pom.xml b/common/proto/pom.xml index 30d4b0bc68..09f9596d0c 100644 --- a/common/proto/pom.xml +++ b/common/proto/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/queue/pom.xml b/common/queue/pom.xml index 69adac81b4..ffee41f8a9 100644 --- a/common/queue/pom.xml +++ b/common/queue/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/script/pom.xml b/common/script/pom.xml index ab5ac032c7..d0a3b47218 100644 --- a/common/script/pom.xml +++ b/common/script/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/script/remote-js-client/pom.xml b/common/script/remote-js-client/pom.xml index 1a3e00a88c..b1547d2e4b 100644 --- a/common/script/remote-js-client/pom.xml +++ b/common/script/remote-js-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.1-RC + 4.3.0-SNAPSHOT script org.thingsboard.common.script diff --git a/common/script/script-api/pom.xml b/common/script/script-api/pom.xml index 316f823a44..b0fcf1c4ae 100644 --- a/common/script/script-api/pom.xml +++ b/common/script/script-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.1-RC + 4.3.0-SNAPSHOT script org.thingsboard.common.script diff --git a/common/stats/pom.xml b/common/stats/pom.xml index 4bf24411dc..e44e098a39 100644 --- a/common/stats/pom.xml +++ b/common/stats/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/coap/pom.xml b/common/transport/coap/pom.xml index be3c62c057..37f33470b2 100644 --- a/common/transport/coap/pom.xml +++ b/common/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.1-RC + 4.3.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/http/pom.xml b/common/transport/http/pom.xml index a136079cb5..e49af5b0a2 100644 --- a/common/transport/http/pom.xml +++ b/common/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.1-RC + 4.3.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/lwm2m/pom.xml b/common/transport/lwm2m/pom.xml index 904e359bcc..1787c851cd 100644 --- a/common/transport/lwm2m/pom.xml +++ b/common/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.1-RC + 4.3.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index 45f9b76af7..9df8cf918e 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.1-RC + 4.3.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/transport/pom.xml b/common/transport/pom.xml index f786bcdd0e..3bd21c96de 100644 --- a/common/transport/pom.xml +++ b/common/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/transport/snmp/pom.xml b/common/transport/snmp/pom.xml index d3b2c46d69..1286073a5f 100644 --- a/common/transport/snmp/pom.xml +++ b/common/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.common - 4.2.1-RC + 4.3.0-SNAPSHOT transport diff --git a/common/transport/transport-api/pom.xml b/common/transport/transport-api/pom.xml index a3fee460e1..19b075cb61 100644 --- a/common/transport/transport-api/pom.xml +++ b/common/transport/transport-api/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.common - 4.2.1-RC + 4.3.0-SNAPSHOT transport org.thingsboard.common.transport diff --git a/common/util/pom.xml b/common/util/pom.xml index ebedf5dea3..804ed0de38 100644 --- a/common/util/pom.xml +++ b/common/util/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/common/version-control/pom.xml b/common/version-control/pom.xml index b6a0ea1d2e..58c059915c 100644 --- a/common/version-control/pom.xml +++ b/common/version-control/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT common org.thingsboard.common diff --git a/dao/pom.xml b/dao/pom.xml index a1843959df..fd6af62330 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT thingsboard dao diff --git a/edqs/pom.xml b/edqs/pom.xml index d648d9d786..4a4c05be71 100644 --- a/edqs/pom.xml +++ b/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT thingsboard edqs diff --git a/monitoring/pom.xml b/monitoring/pom.xml index 77874c5ac9..6dbcf85f07 100644 --- a/monitoring/pom.xml +++ b/monitoring/pom.xml @@ -21,7 +21,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT thingsboard diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index fed33f0002..358c524e06 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/edqs/pom.xml b/msa/edqs/pom.xml index c5be28e7bb..2dd4a9a584 100644 --- a/msa/edqs/pom.xml +++ b/msa/edqs/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/js-executor/pom.xml b/msa/js-executor/pom.xml index 651b142160..9730e7f33b 100644 --- a/msa/js-executor/pom.xml +++ b/msa/js-executor/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/monitoring/pom.xml b/msa/monitoring/pom.xml index abd12ed9e0..cb5b609e43 100644 --- a/msa/monitoring/pom.xml +++ b/msa/monitoring/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT msa diff --git a/msa/pom.xml b/msa/pom.xml index 6fcf7c3523..7fbc5d3c02 100644 --- a/msa/pom.xml +++ b/msa/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT thingsboard msa diff --git a/msa/tb-node/pom.xml b/msa/tb-node/pom.xml index 0103bf9352..8df294260b 100644 --- a/msa/tb-node/pom.xml +++ b/msa/tb-node/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/tb/pom.xml b/msa/tb/pom.xml index 28e1366a73..1cbbf9d1ec 100644 --- a/msa/tb/pom.xml +++ b/msa/tb/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/coap/pom.xml b/msa/transport/coap/pom.xml index 8aefc94122..256220078d 100644 --- a/msa/transport/coap/pom.xml +++ b/msa/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.1-RC + 4.3.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/http/pom.xml b/msa/transport/http/pom.xml index a1481203de..7e5871729d 100644 --- a/msa/transport/http/pom.xml +++ b/msa/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.1-RC + 4.3.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/lwm2m/pom.xml b/msa/transport/lwm2m/pom.xml index 7af2b16588..ca6977eac6 100644 --- a/msa/transport/lwm2m/pom.xml +++ b/msa/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.1-RC + 4.3.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/mqtt/pom.xml b/msa/transport/mqtt/pom.xml index 176dca19eb..172c4facb0 100644 --- a/msa/transport/mqtt/pom.xml +++ b/msa/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard.msa - 4.2.1-RC + 4.3.0-SNAPSHOT transport org.thingsboard.msa.transport diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml index afbab0c450..a6cff4fd0b 100644 --- a/msa/transport/pom.xml +++ b/msa/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/transport/snmp/pom.xml b/msa/transport/snmp/pom.xml index 553ed8bb37..79734822aa 100644 --- a/msa/transport/snmp/pom.xml +++ b/msa/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard.msa transport - 4.2.1-RC + 4.3.0-SNAPSHOT org.thingsboard.msa.transport diff --git a/msa/vc-executor-docker/pom.xml b/msa/vc-executor-docker/pom.xml index d0a291e246..85634d8688 100644 --- a/msa/vc-executor-docker/pom.xml +++ b/msa/vc-executor-docker/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/vc-executor/pom.xml b/msa/vc-executor/pom.xml index cf777c4c96..0acd863612 100644 --- a/msa/vc-executor/pom.xml +++ b/msa/vc-executor/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/msa/web-ui/pom.xml b/msa/web-ui/pom.xml index a56d0ca9d9..db7ed1424a 100644 --- a/msa/web-ui/pom.xml +++ b/msa/web-ui/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT msa org.thingsboard.msa diff --git a/netty-mqtt/pom.xml b/netty-mqtt/pom.xml index 08f2a55f4a..a09d1e2409 100644 --- a/netty-mqtt/pom.xml +++ b/netty-mqtt/pom.xml @@ -19,11 +19,11 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT thingsboard netty-mqtt - 4.2.1-RC + 4.3.0-SNAPSHOT jar Netty MQTT Client diff --git a/pom.xml b/pom.xml index 1283dcc01c..1676e3b56c 100755 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT pom Thingsboard diff --git a/rest-client/pom.xml b/rest-client/pom.xml index e6e82da954..c3cf2460dd 100644 --- a/rest-client/pom.xml +++ b/rest-client/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT thingsboard rest-client diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml index 8a9ad96d95..7abb7be3a3 100644 --- a/rule-engine/pom.xml +++ b/rule-engine/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT thingsboard rule-engine diff --git a/rule-engine/rule-engine-api/pom.xml b/rule-engine/rule-engine-api/pom.xml index 8f449a5e64..59397d4a92 100644 --- a/rule-engine/rule-engine-api/pom.xml +++ b/rule-engine/rule-engine-api/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 4fc5a55c5e..98c7fde665 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -22,7 +22,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT rule-engine org.thingsboard.rule-engine diff --git a/tools/pom.xml b/tools/pom.xml index cce1466971..0b63d84b0a 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT thingsboard tools diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml index 8ae86d381f..67f16917d3 100644 --- a/transport/coap/pom.xml +++ b/transport/coap/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/http/pom.xml b/transport/http/pom.xml index 487e1de186..371ecb84ca 100644 --- a/transport/http/pom.xml +++ b/transport/http/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/lwm2m/pom.xml b/transport/lwm2m/pom.xml index 30effe6e42..66a3066a88 100644 --- a/transport/lwm2m/pom.xml +++ b/transport/lwm2m/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml index a609e8d002..57a73b1bbe 100644 --- a/transport/mqtt/pom.xml +++ b/transport/mqtt/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT transport org.thingsboard.transport diff --git a/transport/pom.xml b/transport/pom.xml index c6a82349f5..a8658bbf40 100644 --- a/transport/pom.xml +++ b/transport/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT thingsboard transport diff --git a/transport/snmp/pom.xml b/transport/snmp/pom.xml index 1894f09787..d5c3d1fe02 100644 --- a/transport/snmp/pom.xml +++ b/transport/snmp/pom.xml @@ -21,7 +21,7 @@ org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT transport diff --git a/ui-ngx/pom.xml b/ui-ngx/pom.xml index 905a1a8aca..56fc4ed628 100644 --- a/ui-ngx/pom.xml +++ b/ui-ngx/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.thingsboard - 4.2.1-RC + 4.3.0-SNAPSHOT thingsboard org.thingsboard From 2e37b53d096dee3017e5fc8ee5de6c2a91e077d7 Mon Sep 17 00:00:00 2001 From: VIacheslavKlimov Date: Thu, 9 Oct 2025 16:15:00 +0300 Subject: [PATCH 216/216] Update supported versions for upgrade --- .../service/install/DefaultDatabaseSchemaSettingsService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java index f41a530630..f48f4621ef 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultDatabaseSchemaSettingsService.java @@ -32,7 +32,7 @@ public class DefaultDatabaseSchemaSettingsService implements DatabaseSchemaSetti // This list should include all versions which are compatible for the upgrade. // The compatibility cycle usually breaks when we have some scripts written in Java that may not work after new release. - private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.2.0"); + private static final List SUPPORTED_VERSIONS_FOR_UPGRADE = List.of("4.2.1"); private final ProjectInfo projectInfo; private final JdbcTemplate jdbcTemplate;