Updated logic due to review comments
This commit is contained in:
parent
3abb23780d
commit
15c1035416
@ -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
|
||||
|
||||
@ -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<EntityId, TbCallback> 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 <M> void applyToTargetCfEntityActors(CalculatedFieldEntityCtxId link, TbCallback callback,
|
||||
Function<TbCallback, M> messageFactory, BiConsumer<EntityId, M> action) {
|
||||
if (isProfileEntity(link.entityId().getEntityType())) {
|
||||
var ids = entityProfileCache.getEntityIdsByProfileId(link.entityId());
|
||||
private void withTargetEntities(EntityId entityId, TbCallback parentCallback, BiConsumer<List<EntityId>, 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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<EntityId, GeofencingZoneState> toZones(Map<EntityId, KvEntry> 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<EntityId, GeofencingZoneState> zoneEntry) {
|
||||
|
||||
@ -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<GeofencingEvalResult> zoneResults = new ArrayList<>(argumentEntry.getZoneStates().size());
|
||||
|
||||
@ -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) {
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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: \"{}\"");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -38,5 +38,7 @@ public class SystemParams {
|
||||
String calculatedFieldDebugPerTenantLimitsConfiguration;
|
||||
long maxArgumentsPerCF;
|
||||
long maxDataPointsPerRollingArg;
|
||||
int minAllowedScheduledUpdateIntervalInSecForCF;
|
||||
int maxRelationLevelPerCfArgument;
|
||||
TrendzSettings trendzSettings;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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!");
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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!");
|
||||
}
|
||||
@ -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 { }
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
@ -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() {
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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();
|
||||
@ -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 {
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<CalculatedField> {
|
||||
|
||||
@ -38,10 +41,22 @@ public class CalculatedFieldDataValidator extends DataValidator<CalculatedField>
|
||||
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 void validateCreate(TenantId tenantId, CalculatedField calculatedField) {
|
||||
long maxCFsPerEntity = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxCalculatedFieldsPerEntity);
|
||||
if (maxCFsPerEntity <= 0) {
|
||||
return;
|
||||
}
|
||||
if (calculatedFieldDao.countCFByEntityId(tenantId, calculatedField.getEntityId()) >= maxCFsPerEntity) {
|
||||
throw new DataValidationException("Calculated fields per entity limit reached!");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -50,21 +65,9 @@ public class CalculatedFieldDataValidator extends DataValidator<CalculatedField>
|
||||
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) {
|
||||
long maxCFsPerEntity = apiLimitService.getLimit(tenantId, DefaultTenantProfileConfiguration::getMaxCalculatedFieldsPerEntity);
|
||||
if (maxCFsPerEntity <= 0) {
|
||||
return;
|
||||
}
|
||||
if (calculatedFieldDao.countCFByEntityId(tenantId, entityId) >= maxCFsPerEntity) {
|
||||
throw new DataValidationException("Calculated fields per entity limit reached!");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateNumberOfArgumentsPerCF(TenantId tenantId, CalculatedField calculatedField) {
|
||||
if (!(calculatedField instanceof ArgumentsBasedCalculatedFieldConfiguration argumentsBasedCfg)) {
|
||||
return;
|
||||
@ -79,8 +82,37 @@ public class CalculatedFieldDataValidator extends DataValidator<CalculatedField>
|
||||
}
|
||||
|
||||
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<String, RelationQueryDynamicSourceConfiguration> 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);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user