Merge branch 'master' into fix/5683-timeseries-table-export

This commit is contained in:
Max Petrov 2025-03-06 12:25:28 +02:00 committed by GitHub
commit fbf2b92d9d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
68 changed files with 951 additions and 143 deletions

View File

@ -32,14 +32,14 @@ on:
jobs:
build:
name: Check thingsboard.yml file
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Python 3.10
- name: Set up Python 3.13
uses: actions/setup-python@v3
with:
python-version: "3.10.2"
python-version: "3.13.2"
architecture: "x64"
env:
AGENT_TOOLSDIRECTORY: /opt/hostedtoolcache

View File

@ -63,4 +63,21 @@ $$;
-- UPDATE SAVE TIME SERIES NODES END
ALTER TABLE api_usage_state ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1;
ALTER TABLE api_usage_state ADD COLUMN IF NOT EXISTS version BIGINT DEFAULT 1;
-- UPDATE TENANT PROFILE CALCULATED FIELD LIMITS START
UPDATE tenant_profile
SET profile_data = profile_data
|| jsonb_build_object(
'configuration', profile_data->'configuration' || jsonb_build_object(
'maxCalculatedFieldsPerEntity', COALESCE(profile_data->'configuration'->>'maxCalculatedFieldsPerEntity', '5')::bigint,
'maxArgumentsPerCF', COALESCE(profile_data->'configuration'->>'maxArgumentsPerCF', '10')::bigint,
'maxDataPointsPerRollingArg', COALESCE(profile_data->'configuration'->>'maxDataPointsPerRollingArg', '1000')::bigint,
'maxStateSizeInKBytes', COALESCE(profile_data->'configuration'->>'maxStateSizeInKBytes', '32')::bigint,
'maxSingleValueArgumentSizeInKBytes', COALESCE(profile_data->'configuration'->>'maxSingleValueArgumentSizeInKBytes', '2')::bigint
)
)
WHERE profile_data->'configuration'->>'maxCalculatedFieldsPerEntity' IS NULL;
-- UPDATE TENANT PROFILE CALCULATED FIELD LIMITS END

View File

@ -644,6 +644,10 @@ public class ActorSystemContext {
@Getter
private String deviceStateNodeRateLimitConfig;
@Value("${actors.calculated_fields.calculation_timeout:5}")
@Getter
private long cfCalculationResultTimeout;
@Getter
@Setter
private TbActorSystem actorSystem;

View File

@ -274,32 +274,30 @@ public class CalculatedFieldEntityMessageProcessor extends AbstractContextAwareM
private void processStateIfReady(CalculatedFieldCtx ctx, List<CalculatedFieldId> cfIdList, CalculatedFieldState state, UUID tbMsgId, TbMsgType tbMsgType, TbCallback callback) throws CalculatedFieldException {
CalculatedFieldEntityCtxId ctxId = new CalculatedFieldEntityCtxId(tenantId, ctx.getCfId(), entityId);
boolean stateSizeOk;
if (ctx.isInitialized() && state.isReady()) {
try {
CalculatedFieldResult calculationResult = state.performCalculation(ctx).get(5, TimeUnit.SECONDS);
boolean stateSizeChecked = false;
try {
if (ctx.isInitialized() && state.isReady()) {
CalculatedFieldResult calculationResult = state.performCalculation(ctx).get(systemContext.getCfCalculationResultTimeout(), TimeUnit.SECONDS);
state.checkStateSize(ctxId, ctx.getMaxStateSize());
stateSizeOk = state.isSizeOk();
if (stateSizeOk) {
stateSizeChecked = true;
if (state.isSizeOk()) {
cfService.pushMsgToRuleEngine(tenantId, entityId, calculationResult, cfIdList, callback);
if (DebugModeUtil.isDebugAllAvailable(ctx.getCalculatedField())) {
systemContext.persistCalculatedFieldDebugEvent(tenantId, ctx.getCfId(), entityId, state.getArguments(), tbMsgId, tbMsgType, JacksonUtil.writeValueAsString(calculationResult.getResult()), null);
}
}
} catch (Exception e) {
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).msgId(tbMsgId).msgType(tbMsgType).arguments(state.getArguments()).cause(e).build();
}
} else {
state.checkStateSize(ctxId, ctx.getMaxStateSize());
stateSizeOk = state.isSizeOk();
if (stateSizeOk) {
callback.onSuccess(); // State was updated but no calculation performed;
} catch (Exception e) {
throw CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).msgId(tbMsgId).msgType(tbMsgType).arguments(state.getArguments()).cause(e).build();
} finally {
if (!stateSizeChecked) {
state.checkStateSize(ctxId, ctx.getMaxStateSize());
}
if (state.isSizeOk()) {
cfStateService.persistState(ctxId, state, callback);
} else {
removeStateAndRaiseSizeException(ctxId, CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(), callback);
}
}
if (stateSizeOk) {
cfStateService.persistState(ctxId, state, callback);
} else {
removeStateAndRaiseSizeException(ctxId, CalculatedFieldException.builder().ctx(ctx).eventEntity(entityId).errorMessage(ctx.getSizeExceedsLimitMessage()).build(), callback);
}
}

View File

@ -20,6 +20,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import lombok.Data;
import lombok.NoArgsConstructor;
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.kv.BasicKvEntry;
@ -64,7 +65,19 @@ public class SimpleCalculatedFieldState extends BaseCalculatedFieldState {
double expressionResult = expr.evaluate();
Output output = ctx.getOutput();
return Futures.immediateFuture(new CalculatedFieldResult(output.getType(), output.getScope(), JacksonUtil.valueToTree(Map.of(output.getName(), expressionResult))));
Object result;
Integer decimals = output.getDecimalsByDefault();
if (decimals != null) {
if (decimals.equals(0)) {
result = TbUtils.toInt(expressionResult);
} else {
result = TbUtils.toFixed(expressionResult, decimals);
}
} else {
result = expressionResult;
}
return Futures.immediateFuture(new CalculatedFieldResult(output.getType(), output.getScope(), JacksonUtil.valueToTree(Map.of(output.getName(), result))));
}
}

View File

@ -100,10 +100,6 @@ public class SingleValueArgumentEntry implements ArgumentEntry {
if (newVersion == null || this.version == null || newVersion > this.version) {
this.ts = singleValueEntry.getTs();
this.version = newVersion;
BasicKvEntry newValue = singleValueEntry.getKvEntryValue();
if (this.kvEntryValue != null && this.kvEntryValue.getValue().equals(newValue.getValue())) {
return false;
}
this.kvEntryValue = singleValueEntry.getKvEntryValue();
return true;
}

View File

@ -89,7 +89,7 @@ public class TsRollingArgumentEntry implements ArgumentEntry {
for (var e : tsRecords.entrySet()) {
values.add(new TbelCfTsDoubleVal(e.getKey(), e.getValue()));
}
return new TbelCfTsRollingArg(limit, timeWindow, values);
return new TbelCfTsRollingArg(timeWindow, values);
}
@Override

View File

@ -436,7 +436,9 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
ctx.sendWsMsg(update);
} else {
ctx.doFetchAlarmCount();
ctx.createAlarmSubscriptions();
if (entitiesIds != null) {
ctx.createAlarmSubscriptions();
}
TbAlarmCountSubCtx finalCtx = ctx;
ScheduledFuture<?> task = scheduler.scheduleWithFixedDelay(
() -> refreshDynamicQuery(finalCtx),

View File

@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
import org.thingsboard.server.common.data.util.ThrowingRunnable;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.queue.util.TbCoreComponent;
@ -61,6 +62,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
private final Map<EntityType, EntityImportService<?, ?, ?>> importServices = new HashMap<>();
private final RelationService relationService;
private final CalculatedFieldService calculatedFieldService;
private final RateLimitService rateLimitService;
private final TbLogEntityActionService logEntityActionService;
@ -72,7 +74,6 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
EntityType.NOTIFICATION_TEMPLATE, EntityType.NOTIFICATION_TARGET, EntityType.NOTIFICATION_RULE
);
@Override
public <E extends ExportableEntity<I>, I extends EntityId> EntityExportData<E> exportEntity(EntitiesExportCtx<?> ctx, I entityId) throws ThingsboardException {
if (!rateLimitService.checkRateLimit(LimitedApi.ENTITY_EXPORT, ctx.getTenantId())) {
@ -129,13 +130,11 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
}
}
@Override
public Comparator<EntityType> getEntityTypeComparatorForImport() {
return Comparator.comparing(SUPPORTED_ENTITY_TYPES::indexOf);
}
@SuppressWarnings("unchecked")
private <I extends EntityId, E extends ExportableEntity<I>, D extends EntityExportData<E>> EntityExportService<I, E, D> getExportService(EntityType entityType) {
EntityExportService<?, ?, ?> exportService = exportServices.get(entityType);

View File

@ -32,7 +32,6 @@ public interface EntitiesExportImportService {
<E extends ExportableEntity<I>, I extends EntityId> EntityImportResult<E> importEntity(EntitiesImportCtx ctx, EntityExportData<E> exportData) throws ThingsboardException;
void saveReferencesAndRelations(EntitiesImportCtx ctx) throws ThingsboardException;
Comparator<EntityType> getEntityTypeComparatorForImport();

View File

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.HasVersion;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
@ -31,6 +32,7 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.sync.ie.AttributeExportData;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.relation.RelationDao;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.ie.exporting.EntityExportService;
@ -59,6 +61,8 @@ public class DefaultEntityExportService<I extends EntityId, E extends Exportable
private RelationDao relationDao;
@Autowired
private AttributesService attributesService;
@Autowired
private CalculatedFieldService calculatedFieldService;
@Override
public final D getExportData(EntitiesExportCtx<?> ctx, I entityId) throws ThingsboardException {
@ -98,6 +102,10 @@ public class DefaultEntityExportService<I extends EntityId, E extends Exportable
Map<String, List<AttributeExportData>> attributes = exportAttributes(ctx, entity);
exportData.setAttributes(attributes);
}
if (ctx.getSettings().isExportCalculatedFields()) {
List<CalculatedField> calculatedFields = exportCalculatedFields(ctx, entity.getId());
exportData.setCalculatedFields(calculatedFields);
}
}
private List<EntityRelation> exportRelations(EntitiesExportCtx<?> ctx, E entity) throws ThingsboardException {
@ -141,6 +149,19 @@ public class DefaultEntityExportService<I extends EntityId, E extends Exportable
return attributes;
}
private List<CalculatedField> exportCalculatedFields(EntitiesExportCtx<?> ctx, EntityId entityId) {
List<CalculatedField> calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(ctx.getTenantId(), entityId);
calculatedFields.forEach(calculatedField -> {
calculatedField.setEntityId(getExternalIdOrElseInternal(ctx, entityId));
calculatedField.getConfiguration().getArguments().values().forEach(argument -> {
if (argument.getRefEntityId() != null) {
argument.setRefEntityId(getExternalIdOrElseInternal(ctx, argument.getRefEntityId()));
}
});
});
return calculatedFields;
}
protected <ID extends EntityId> ID getExternalIdOrElseInternal(EntitiesExportCtx<?> ctx, ID internalId) {
if (internalId == null || internalId.isNullUid()) return internalId;
var result = ctx.getExternalId(internalId);

View File

@ -47,7 +47,11 @@ public class AssetImportService extends BaseEntityImportService<AssetId, Asset,
@Override
protected Asset saveOrUpdate(EntitiesImportCtx ctx, Asset asset, EntityExportData<Asset> exportData, IdProvider idProvider) {
return assetService.saveAsset(asset);
Asset savedAsset = assetService.saveAsset(asset);
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
importCalculatedFields(ctx, savedAsset, exportData, idProvider);
}
return savedAsset;
}
@Override

View File

@ -50,7 +50,11 @@ public class AssetProfileImportService extends BaseEntityImportService<AssetProf
@Override
protected AssetProfile saveOrUpdate(EntitiesImportCtx ctx, AssetProfile assetProfile, EntityExportData<AssetProfile> exportData, IdProvider idProvider) {
return assetProfileService.saveAssetProfile(assetProfile);
AssetProfile saved = assetProfileService.saveAssetProfile(assetProfile);
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
importCalculatedFields(ctx, saved, exportData, idProvider);
}
return saved;
}
@Override

View File

@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.HasDefaultOption;
import org.thingsboard.server.common.data.HasVersion;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
@ -50,6 +51,7 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.sync.ie.AttributeExportData;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.relation.RelationDao;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.service.action.EntityActionService;
@ -78,6 +80,8 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
@Lazy
private ExportableEntitiesService entitiesService;
@Autowired
private CalculatedFieldService calculatedFieldService;
@Autowired
private RelationService relationService;
@Autowired
private RelationDao relationDao;
@ -134,7 +138,7 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
}
protected boolean updateRelatedEntitiesIfUnmodified(EntitiesImportCtx ctx, E prepared, D exportData, IdProvider idProvider) {
return false;
return importCalculatedFields(ctx, prepared, exportData, idProvider);
}
@Override
@ -278,12 +282,59 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
});
}
protected boolean importCalculatedFields(EntitiesImportCtx ctx, E savedEntity, D exportData, IdProvider idProvider) {
if (exportData.getCalculatedFields() == null || !ctx.isSaveCalculatedFields()) {
return false;
}
boolean updated = false;
List<CalculatedField> existing = calculatedFieldService.findCalculatedFieldsByEntityId(ctx.getTenantId(), savedEntity.getId());
List<CalculatedField> fieldsToSave = exportData.getCalculatedFields().stream()
.peek(calculatedField -> {
calculatedField.setTenantId(ctx.getTenantId());
calculatedField.setEntityId(savedEntity.getId());
calculatedField.getConfiguration().getArguments().values().forEach(argument -> {
if (argument.getRefEntityId() != null) {
argument.setRefEntityId(idProvider.getInternalId(argument.getRefEntityId(), ctx.isFinalImportAttempt()));
}
});
}).toList();
for (CalculatedField existingField : existing) {
boolean found = fieldsToSave.stream().anyMatch(importedField -> compareCalculatedFields(existingField, importedField));
if (!found) {
calculatedFieldService.deleteCalculatedField(ctx.getTenantId(), existingField.getId());
updated = true;
}
}
for (CalculatedField calculatedField : fieldsToSave) {
boolean found = existing.stream().anyMatch(existingField -> compareCalculatedFields(existingField, calculatedField));
if (!found) {
calculatedFieldService.save(calculatedField);
updated = true;
}
}
return updated;
}
private boolean compareCalculatedFields(CalculatedField existingField, CalculatedField newField) {
CalculatedField oldCopy = new CalculatedField(existingField);
CalculatedField newCopy = new CalculatedField(newField);
oldCopy.setId(null);
newCopy.setId(null);
oldCopy.setVersion(null);
newCopy.setVersion(null);
oldCopy.setCreatedTime(0);
newCopy.setCreatedTime(0);
return oldCopy.equals(newCopy);
}
protected void onEntitySaved(User user, E savedEntity, E oldEntity) throws ThingsboardException {
logEntityActionService.logEntityAction(user.getTenantId(), savedEntity.getId(), savedEntity, null,
oldEntity == null ? ActionType.ADDED : ActionType.UPDATED, user);
}
@SuppressWarnings("unchecked")
protected E findExistingEntity(EntitiesImportCtx ctx, E entity, IdProvider idProvider) {
return (E) Optional.ofNullable(entitiesService.findEntityByTenantIdAndExternalId(ctx.getTenantId(), entity.getId()))
@ -313,10 +364,10 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
.orElseThrow(() -> new MissingEntityException(externalId));
}
@SuppressWarnings("unchecked")
@RequiredArgsConstructor
protected class IdProvider {
private final EntitiesImportCtx ctx;
private final EntityImportResult<E> importResult;

View File

@ -64,13 +64,18 @@ public class DeviceImportService extends BaseEntityImportService<DeviceId, Devic
@Override
protected Device saveOrUpdate(EntitiesImportCtx ctx, Device device, DeviceExportData exportData, IdProvider idProvider) {
Device savedDevice;
if (exportData.getCredentials() != null && ctx.isSaveCredentials()) {
exportData.getCredentials().setId(null);
exportData.getCredentials().setDeviceId(null);
return deviceService.saveDeviceWithCredentials(device, exportData.getCredentials());
savedDevice = deviceService.saveDeviceWithCredentials(device, exportData.getCredentials());
} else {
return deviceService.saveDevice(device);
savedDevice = deviceService.saveDevice(device);
}
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
importCalculatedFields(ctx, savedDevice, exportData, idProvider);
}
return savedDevice;
}
@Override

View File

@ -52,7 +52,11 @@ public class DeviceProfileImportService extends BaseEntityImportService<DevicePr
@Override
protected DeviceProfile saveOrUpdate(EntitiesImportCtx ctx, DeviceProfile deviceProfile, EntityExportData<DeviceProfile> exportData, IdProvider idProvider) {
return deviceProfileService.saveDeviceProfile(deviceProfile);
DeviceProfile saved = deviceProfileService.saveDeviceProfile(deviceProfile);
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
importCalculatedFields(ctx, saved, exportData, idProvider);
}
return saved;
}
@Override

View File

@ -94,7 +94,6 @@ import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
import java.util.stream.Collectors;
import static com.google.common.util.concurrent.Futures.transform;
import static org.thingsboard.server.common.data.sync.vc.VcUtils.checkBranchName;
@ -304,6 +303,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
.updateRelations(config.isLoadRelations())
.saveAttributes(config.isLoadAttributes())
.saveCredentials(config.isLoadCredentials())
.saveCalculatedFields(config.isLoadCalculatedFields())
.findExistingByName(false)
.build());
ctx.setFinalImportAttempt(true);
@ -327,7 +327,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
var sw = TbStopWatch.create("before");
List<EntityType> entityTypes = request.getEntityTypes().keySet().stream()
.sorted(exportImportService.getEntityTypeComparatorForImport()).collect(Collectors.toList());
.sorted(exportImportService.getEntityTypeComparatorForImport()).toList();
for (EntityType entityType : entityTypes) {
log.debug("[{}] Loading {} entities", ctx.getTenantId(), entityType);
sw.startNew("Entities " + entityType.name());
@ -362,6 +362,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
.updateRelations(config.isLoadRelations())
.saveAttributes(config.isLoadAttributes())
.saveCredentials(config.isLoadCredentials())
.saveCalculatedFields(config.isLoadCalculatedFields())
.findExistingByName(config.isFindExistingEntityByName())
.build();
}
@ -471,7 +472,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
}
@Override
public ListenableFuture<EntityDataDiff> compareEntityDataToVersion(User user, EntityId entityId, String versionId) throws Exception {
public ListenableFuture<EntityDataDiff> compareEntityDataToVersion(User user, EntityId entityId, String versionId) {
HasId<EntityId> entity = exportableEntitiesService.findEntityByTenantIdAndId(user.getTenantId(), entityId);
if (!(entity instanceof ExportableEntity)) throw new IllegalArgumentException("Unsupported entity type");
@ -484,6 +485,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
.exportRelations(otherVersion.hasRelations())
.exportAttributes(otherVersion.hasAttributes())
.exportCredentials(otherVersion.hasCredentials())
.exportCalculatedFields(otherVersion.hasCalculatedFields())
.build());
EntityExportData<?> currentVersion;
try {
@ -503,7 +505,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
@Override
public ListenableFuture<List<BranchInfo>> listBranches(TenantId tenantId) throws Exception {
public ListenableFuture<List<BranchInfo>> listBranches(TenantId tenantId) {
return gitServiceQueue.listBranches(tenantId);
}

View File

@ -69,6 +69,7 @@ public abstract class EntitiesExportCtx<R extends VersionCreateRequest> {
.exportRelations(config.isSaveRelations())
.exportAttributes(config.isSaveAttributes())
.exportCredentials(config.isSaveCredentials())
.exportCalculatedFields(config.isSaveCalculatedFields())
.build();
}
@ -85,4 +86,5 @@ public abstract class EntitiesExportCtx<R extends VersionCreateRequest> {
log.debug("[{}][{}] Local cache put: {}", internalId.getEntityType(), internalId.getId(), externalId);
externalIdMap.put(internalId, externalId != null ? externalId : internalId);
}
}

View File

@ -91,6 +91,10 @@ public class EntitiesImportCtx {
return getSettings().isSaveCredentials();
}
public boolean isSaveCalculatedFields() {
return getSettings().isSaveCalculatedFields();
}
public EntityId getInternalId(EntityId externalId) {
var result = externalToInternalIdMap.get(externalId);
log.debug("[{}][{}] Local cache {} for id", externalId.getEntityType(), externalId.getId(), result != null ? "hit" : "miss");
@ -140,5 +144,4 @@ public class EntitiesImportCtx {
return notFoundIds.contains(externalId);
}
}

View File

@ -39,6 +39,7 @@ public class EntityTypeExportCtx extends EntitiesExportCtx<VersionCreateRequest>
.exportRelations(config.isSaveRelations())
.exportAttributes(config.isSaveAttributes())
.exportCredentials(config.isSaveCredentials())
.exportCalculatedFields(config.isSaveCalculatedFields())
.build();
this.overwrite = ObjectUtils.defaultIfNull(config.getSyncStrategy(), defaultSyncStrategy) == SyncStrategy.OVERWRITE;
}

View File

@ -512,6 +512,8 @@ actors:
enabled: "${ACTORS_CALCULATED_FIELD_DEBUG_MODE_RATE_LIMITS_PER_TENANT_ENABLED:true}"
# The value of DEBUG mode rate limit. By default, no more than 50 thousand events per hour
configuration: "${ACTORS_CALCULATED_FIELD_DEBUG_MODE_RATE_LIMITS_PER_TENANT_CONFIGURATION:50000:3600}"
# Time in seconds to receive calculation result.
calculation_timeout: "${ACTORS_CALCULATION_TIMEOUT_SEC:5}"
debug:
settings:

View File

@ -83,7 +83,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@Slf4j
@DaoSqlTest
@TestPropertySource(properties = {
"server.ws.alarms_per_alarm_status_subscription_cache_size=5"
"server.ws.alarms_per_alarm_status_subscription_cache_size=5",
"server.ws.dynamic_page_link.refresh_interval=15"
})
public class WebsocketApiTest extends AbstractControllerTest {
@Autowired

View File

@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedField
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.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.dao.usagerecord.ApiLimitService;
@ -138,7 +139,7 @@ public class SimpleCalculatedFieldStateTest {
Output output = getCalculatedFieldConfig().getOutput();
assertThat(result.getType()).isEqualTo(output.getType());
assertThat(result.getScope()).isEqualTo(output.getScope());
assertThat(result.getResult()).isEqualTo(JacksonUtil.valueToTree(Map.of("output", 49.0)));
assertThat(result.getResult()).isEqualTo(JacksonUtil.valueToTree(Map.of("output", 49)));
}
@Test
@ -154,6 +155,26 @@ public class SimpleCalculatedFieldStateTest {
.hasMessage("Argument 'key2' is not a number.");
}
@Test
void testPerformCalculationWhenDecimalsByDefault() throws ExecutionException, InterruptedException {
state.arguments = new HashMap<>(Map.of(
"key1", new SingleValueArgumentEntry(System.currentTimeMillis() - 10, new DoubleDataEntry("key1", 11.3456), 145L),
"key2", new SingleValueArgumentEntry(System.currentTimeMillis() - 6, new DoubleDataEntry("key2", 15.1), 165L),
"key3", new SingleValueArgumentEntry(System.currentTimeMillis() - 3, new DoubleDataEntry("key3", 23.1), 184L)
));
Output output = getCalculatedFieldConfig().getOutput();
output.setDecimalsByDefault(3);
ctx.setOutput(output);
CalculatedFieldResult result = state.performCalculation(ctx).get();
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)));
}
@Test
void testIsReadyWhenNotAllArgPresent() {
assertThat(state.isReady()).isFalse();
@ -219,6 +240,7 @@ public class SimpleCalculatedFieldStateTest {
output.setName("output");
output.setType(OutputType.ATTRIBUTES);
output.setScope(AttributeScope.SERVER_SCOPE);
output.setDecimalsByDefault(0);
config.setOutput(output);

View File

@ -71,6 +71,6 @@ public class SingleValueArgumentEntryTest {
@Test
void testUpdateEntryWhenValueWasNotChanged() {
assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 11L), 237L))).isFalse();
assertThat(entry.updateEntry(new SingleValueArgumentEntry(ts + 18, new LongDataEntry("key", 11L), 364L))).isTrue();
}
}

View File

@ -43,6 +43,15 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.audit.ActionType;
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.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.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.DeviceData;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
@ -70,6 +79,7 @@ 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.sync.ie.DeviceExportData;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.EntityExportSettings;
import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
@ -79,6 +89,7 @@ import org.thingsboard.server.common.data.util.ThrowingRunnable;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.cf.CalculatedFieldService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceProfileService;
@ -145,6 +156,8 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
protected TenantService tenantService;
@Autowired
protected EntityViewService entityViewService;
@Autowired
protected CalculatedFieldService calculatedFieldService;
protected TenantId tenantId1;
protected User tenantAdmin1;
@ -191,6 +204,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
Asset asset = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1");
DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1");
Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1");
CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), asset.getId());
Map<EntityType, EntityExportData> entitiesExportData = Stream.of(customer.getId(), asset.getId(), device.getId(),
ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId())
@ -198,6 +212,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
try {
return exportEntity(tenantAdmin1, entityId, EntityExportSettings.builder()
.exportCredentials(false)
.exportCalculatedFields(true)
.build());
} catch (Exception e) {
throw new RuntimeException(e);
@ -245,7 +260,6 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
verify(entityActionService, Mockito.never()).logEntityAction(any(), eq(importedAsset.getId()), eq(importedAsset),
any(), eq(ActionType.UPDATED), isNull());
EntityExportData<Asset> updatedAssetEntity = getAndClone(entitiesExportData, EntityType.ASSET);
updatedAssetEntity.getEntity().setLabel("t" + updatedAssetEntity.getEntity().getLabel());
Asset updatedAsset = importEntity(tenantAdmin2, updatedAssetEntity).getSavedEntity();
@ -268,10 +282,27 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE));
verify(tbClusterService, Mockito.never()).onDeviceUpdated(eq(importedDevice), eq(importedDevice));
// calculated field of imported device:
List<CalculatedField> calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(tenantId2, importedDevice.getId());
assertThat(calculatedFields.size()).isOne();
var importedCalculatedField = calculatedFields.get(0);
assertThat(importedCalculatedField.getName()).isEqualTo(calculatedField.getName());
verify(tbClusterService).onCalculatedFieldUpdated(eq(importedCalculatedField), isNull(), any());
EntityExportData<Device> updatedDeviceEntity = getAndClone(entitiesExportData, EntityType.DEVICE);
updatedDeviceEntity.getEntity().setLabel("t" + updatedDeviceEntity.getEntity().getLabel());
Device updatedDevice = importEntity(tenantAdmin2, updatedDeviceEntity).getSavedEntity();
verify(tbClusterService).onDeviceUpdated(eq(updatedDevice), eq(importedDevice));
// update calculated field:
DeviceExportData deviceExportData = (DeviceExportData) getAndClone(entitiesExportData, EntityType.DEVICE);
deviceExportData.setCalculatedFields(deviceExportData.getCalculatedFields().stream().peek(field -> field.setName("t_" + field.getName())).toList());
importEntity(tenantAdmin2, deviceExportData).getSavedEntity();
calculatedFields = calculatedFieldService.findCalculatedFieldsByEntityId(tenantId2, importedDevice.getId());
assertThat(calculatedFields.size()).isOne();
importedCalculatedField = calculatedFields.get(0);
assertThat(importedCalculatedField.getName()).startsWith("t_");
}
@Test
@ -290,12 +321,15 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
Device device = createDevice(tenantId1, customer.getId(), deviceProfile.getId(), "Device 1");
EntityView entityView = createEntityView(tenantId1, customer.getId(), device.getId(), "Entity view 1");
CalculatedField calculatedField = createCalculatedField(tenantId1, device.getId(), device.getId());
Map<EntityId, EntityId> ids = new HashMap<>();
for (EntityId entityId : List.of(customer.getId(), ruleChain.getId(), dashboard.getId(), assetProfile.getId(), asset.getId(),
deviceProfile.getId(), device.getId(), entityView.getId(), ruleChain.getId(), dashboard.getId())) {
EntityExportData exportData = exportEntity(getSecurityUser(tenantAdmin1), entityId);
EntityImportResult importResult = importEntity(getSecurityUser(tenantAdmin2), exportData, EntityImportSettings.builder()
.saveCredentials(false)
.saveCalculatedFields(true)
.build());
ids.put(entityId, (EntityId) importResult.getSavedEntity().getId());
}
@ -325,10 +359,16 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
assertThat(exportedDeviceProfile.getDefaultRuleChainId()).isEqualTo(ruleChain.getId());
assertThat(exportedDeviceProfile.getDefaultDashboardId()).isEqualTo(dashboard.getId());
Device exportedDevice = (Device) exportEntity(tenantAdmin2, (DeviceId) ids.get(device.getId())).getEntity();
EntityExportData<Device> entityExportData = exportEntity(tenantAdmin2, (DeviceId) ids.get(device.getId()));
Device exportedDevice = entityExportData.getEntity();
assertThat(exportedDevice.getCustomerId()).isEqualTo(customer.getId());
assertThat(exportedDevice.getDeviceProfileId()).isEqualTo(deviceProfile.getId());
List<CalculatedField> calculatedFields = ((DeviceExportData) entityExportData).getCalculatedFields();
assertThat(calculatedFields.size()).isOne();
CalculatedField field = calculatedFields.get(0);
assertThat(field.getName()).isEqualTo(calculatedField.getName());
EntityView exportedEntityView = (EntityView) exportEntity(tenantAdmin2, (EntityViewId) ids.get(entityView.getId())).getEntity();
assertThat(exportedEntityView.getCustomerId()).isEqualTo(customer.getId());
assertThat(exportedEntityView.getEntityId()).isEqualTo(device.getId());
@ -340,7 +380,6 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
deviceProfileService.saveDeviceProfile(importedDeviceProfile);
}
protected Device createDevice(TenantId tenantId, CustomerId customerId, DeviceProfileId deviceProfileId, String name) {
Device device = new Device();
device.setTenantId(tenantId);
@ -549,9 +588,43 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
return relation;
}
private CalculatedField createCalculatedField(TenantId tenantId, EntityId entityId, EntityId referencedEntityId) {
CalculatedField calculatedField = new CalculatedField();
calculatedField.setTenantId(tenantId);
calculatedField.setEntityId(entityId);
calculatedField.setType(CalculatedFieldType.SIMPLE);
calculatedField.setName("Test Calculated Field");
calculatedField.setConfigurationVersion(1);
calculatedField.setConfiguration(getCalculatedFieldConfig(referencedEntityId));
calculatedField.setVersion(1L);
return calculatedFieldService.save(calculatedField);
}
private CalculatedFieldConfiguration getCalculatedFieldConfig(EntityId referencedEntityId) {
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
Argument argument = new Argument();
argument.setRefEntityId(referencedEntityId);
ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null);
argument.setRefEntityKey(refEntityKey);
config.setArguments(Map.of("T", argument));
config.setExpression("T - (100 - H) / 5");
Output output = new Output();
output.setName("output");
output.setType(OutputType.TIME_SERIES);
config.setOutput(output);
return config;
}
protected <E extends ExportableEntity<I>, I extends EntityId> EntityExportData<E> exportEntity(User user, I entityId) throws Exception {
return exportEntity(user, entityId, EntityExportSettings.builder()
.exportCredentials(true)
.exportCalculatedFields(true)
.build());
}
@ -562,6 +635,7 @@ public class ExportImportServiceSqlTest extends AbstractControllerTest {
protected <E extends ExportableEntity<I>, I extends EntityId> EntityImportResult<E> importEntity(User user, EntityExportData<E> exportData) throws Exception {
return importEntity(user, exportData, EntityImportSettings.builder()
.saveCredentials(true)
.saveCalculatedFields(true)
.build());
}

View File

@ -44,6 +44,15 @@ import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
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.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.ReferencedEntityKey;
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
import org.thingsboard.server.common.data.debug.DebugSettings;
import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.DeviceData;
@ -562,6 +571,81 @@ public class VersionControlTest extends AbstractControllerTest {
checkImportedEntity(tenantId1, defaultDeviceProfile, tenantId2, importedDeviceProfile);
}
@Test
public void testVcWithCalculatedFields_betweenTenants() throws Exception {
Asset asset = createAsset(null, null, "Asset 1");
Device device = createDevice(null, null, "Device 1", "test1");
CalculatedField calculatedField = createCalculatedField("CalculatedField1", device.getId(), asset.getId());
String versionId = createVersion("calculated fields of asset and device", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE);
loginTenant2();
loadVersion(versionId, config -> {
config.setLoadCredentials(false);
}, EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE);
Asset importedAsset = findAsset(asset.getName());
Device importedDevice = findDevice(device.getName());
checkImportedEntity(tenantId1, device, tenantId2, importedDevice);
checkImportedEntity(tenantId1, asset, tenantId2, importedAsset);
List<CalculatedField> importedCalculatedFields = findCalculatedFieldsByEntityId(importedDevice.getId());
assertThat(importedCalculatedFields).size().isOne();
assertThat(importedCalculatedFields.get(0)).satisfies(importedField -> {
assertThat(importedField.getName()).isEqualTo(calculatedField.getName());
assertThat(importedField.getType()).isEqualTo(calculatedField.getType());
assertThat(importedField.getId()).isNotEqualTo(calculatedField.getId());
});
}
@Test
public void testVcWithReferencedCalculatedFields_betweenTenants() throws Exception {
Asset asset = createAsset(null, null, "Asset 1");
Device device = createDevice(null, null, "Device 1", "test1");
CalculatedField deviceCalculatedField = createCalculatedField("CalculatedField1", device.getId(), asset.getId());
CalculatedField assetCalculatedField = createCalculatedField("CalculatedField2", asset.getId(), device.getId());
String versionId = createVersion("calculated fields of asset and device", EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE);
loginTenant2();
loadVersion(versionId, config -> {
config.setLoadCredentials(false);
}, EntityType.ASSET, EntityType.DEVICE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE);
Asset importedAsset = findAsset(asset.getName());
Device importedDevice = findDevice(device.getName());
checkImportedEntity(tenantId1, device, tenantId2, importedDevice);
checkImportedEntity(tenantId1, asset, tenantId2, importedAsset);
List<CalculatedField> importedDeviceCalculatedFields = findCalculatedFieldsByEntityId(importedDevice.getId());
assertThat(importedDeviceCalculatedFields).size().isOne();
assertThat(importedDeviceCalculatedFields.get(0)).satisfies(importedField -> {
assertThat(importedField.getName()).isEqualTo(deviceCalculatedField.getName());
assertThat(importedField.getType()).isEqualTo(deviceCalculatedField.getType());
assertThat(importedField.getId()).isNotEqualTo(deviceCalculatedField.getId());
});
List<CalculatedField> importedAssetCalculatedFields = findCalculatedFieldsByEntityId(importedAsset.getId());
assertThat(importedAssetCalculatedFields).size().isOne();
assertThat(importedAssetCalculatedFields.get(0)).satisfies(importedField -> {
assertThat(importedField.getName()).isEqualTo(assetCalculatedField.getName());
assertThat(importedField.getType()).isEqualTo(assetCalculatedField.getType());
assertThat(importedField.getId()).isNotEqualTo(assetCalculatedField.getId());
});
}
@Test
public void testVcWithCalculatedFields_sameTenant() throws Exception {
Asset asset = createAsset(null, null, "Asset 1");
CalculatedField calculatedField = createCalculatedField("CalculatedField", asset.getId(), asset.getId());
String versionId = createVersion("asset and field", EntityType.ASSET);
loadVersion(versionId, EntityType.ASSET);
CalculatedField importedCalculatedField = findCalculatedFieldByEntityId(asset.getId());
assertThat(importedCalculatedField.getId()).isEqualTo(calculatedField.getId());
assertThat(importedCalculatedField.getName()).isEqualTo(calculatedField.getName());
assertThat(importedCalculatedField.getConfiguration()).isEqualTo(calculatedField.getConfiguration());
assertThat(importedCalculatedField.getType()).isEqualTo(calculatedField.getType());
}
private <E extends ExportableEntity<?> & HasTenantId> void checkImportedEntity(TenantId tenantId1, E initialEntity, TenantId tenantId2, E importedEntity) {
assertThat(initialEntity.getTenantId()).isEqualTo(tenantId1);
assertThat(importedEntity.getTenantId()).isEqualTo(tenantId2);
@ -626,10 +710,10 @@ public class VersionControlTest extends AbstractControllerTest {
request.setEntityTypes(Arrays.stream(entityTypes).collect(Collectors.toMap(t -> t, entityType -> {
EntityTypeVersionCreateConfig config = new EntityTypeVersionCreateConfig();
config.setAllEntities(true);
config.setSaveRelations(true);
config.setSaveAttributes(true);
config.setSaveCredentials(true);
config.setSaveCalculatedFields(true);
return config;
})));
@ -695,6 +779,7 @@ public class VersionControlTest extends AbstractControllerTest {
config.setLoadAttributes(true);
config.setLoadRelations(true);
config.setLoadCredentials(true);
config.setLoadCalculatedFields(true);
config.setRemoveOtherEntities(false);
config.setFindExistingEntityByName(true);
configModifier.accept(config);
@ -941,6 +1026,38 @@ public class VersionControlTest extends AbstractControllerTest {
return doPost("/api/v2/relation", relation, EntityRelation.class);
}
private CalculatedField createCalculatedField(String name, EntityId entityId, EntityId referencedEntityId) {
CalculatedField calculatedField = new CalculatedField();
calculatedField.setEntityId(entityId);
calculatedField.setType(CalculatedFieldType.SIMPLE);
calculatedField.setName(name);
calculatedField.setConfigurationVersion(1);
calculatedField.setConfiguration(getCalculatedFieldConfig(referencedEntityId));
calculatedField.setVersion(1L);
return doPost("/api/calculatedField", calculatedField, CalculatedField.class);
}
private CalculatedFieldConfiguration getCalculatedFieldConfig(EntityId referencedEntityId) {
SimpleCalculatedFieldConfiguration config = new SimpleCalculatedFieldConfiguration();
Argument argument = new Argument();
argument.setRefEntityId(referencedEntityId);
ReferencedEntityKey refEntityKey = new ReferencedEntityKey("temperature", ArgumentType.TS_LATEST, null);
argument.setRefEntityKey(refEntityKey);
config.setArguments(Map.of("T", argument));
config.setExpression("T - (100 - H) / 5");
Output output = new Output();
output.setName("output");
output.setType(OutputType.TIME_SERIES);
config.setOutput(output);
return config;
}
protected void checkImportedRuleChainData(RuleChain initialRuleChain, RuleChainMetaData initialMetaData, RuleChain importedRuleChain, RuleChainMetaData importedMetaData) {
assertThat(importedRuleChain.getType()).isEqualTo(initialRuleChain.getType());
assertThat(importedRuleChain.getName()).isEqualTo(initialRuleChain.getName());
@ -995,11 +1112,18 @@ public class VersionControlTest extends AbstractControllerTest {
private RuleChain findRuleChain(String name) throws Exception {
return doGetTypedWithPageLink("/api/ruleChains?", new TypeReference<PageData<RuleChain>>() {}, new PageLink(100, 0, name)).getData().get(0);
}
private RuleChainMetaData findRuleChainMetaData(RuleChainId ruleChainId) throws Exception {
return doGet("/api/ruleChain/" + ruleChainId + "/metadata", RuleChainMetaData.class);
}
private CalculatedField findCalculatedFieldByEntityId(EntityId entityId) throws Exception {
return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields?", new TypeReference<PageData<CalculatedField>>() {}, new PageLink(100, 0)).getData().get(0);
}
private List<CalculatedField> findCalculatedFieldsByEntityId(EntityId entityId) throws Exception {
return doGetTypedWithPageLink("/api/" + entityId.getEntityType() + "/" + entityId.getId() + "/calculatedFields?", new TypeReference<PageData<CalculatedField>>() {}, new PageLink(100, 0)).getData();
}
}

View File

@ -23,7 +23,6 @@ import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.HasDebugSettings;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
@ -37,11 +36,14 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import java.io.Serial;
@Schema
@Data
@EqualsAndHashCode(callSuper = true)
public class CalculatedField extends BaseData<CalculatedFieldId> implements HasName, HasTenantId, HasVersion, ExportableEntity<CalculatedFieldId>, HasDebugSettings {
public class CalculatedField extends BaseData<CalculatedFieldId> implements HasName, HasTenantId, HasVersion, HasDebugSettings {
@Serial
private static final long serialVersionUID = 4491966747773381420L;
private TenantId tenantId;
@ -66,9 +68,6 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
@Getter
@Setter
private Long version;
@Getter
@Setter
private CalculatedFieldId externalId;
public CalculatedField() {
super();
@ -78,7 +77,7 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
super(id);
}
public CalculatedField(TenantId tenantId, EntityId entityId, CalculatedFieldType type, String name, int configurationVersion, CalculatedFieldConfiguration configuration, Long version, CalculatedFieldId externalId) {
public CalculatedField(TenantId tenantId, EntityId entityId, CalculatedFieldType type, String name, int configurationVersion, CalculatedFieldConfiguration configuration, Long version) {
this.tenantId = tenantId;
this.entityId = entityId;
this.type = type;
@ -86,7 +85,19 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
this.configurationVersion = configurationVersion;
this.configuration = configuration;
this.version = version;
this.externalId = externalId;
}
public CalculatedField(CalculatedField calculatedField) {
super(calculatedField);
this.tenantId = calculatedField.tenantId;
this.entityId = calculatedField.entityId;
this.type = calculatedField.type;
this.name = calculatedField.name;
this.debugMode = calculatedField.debugMode;
this.debugSettings = calculatedField.debugSettings;
this.configurationVersion = calculatedField.configurationVersion;
this.configuration = calculatedField.configuration;
this.version = calculatedField.version;
}
@Schema(description = "JSON object with the Calculated Field Id. Referencing non-existing Calculated Field Id will cause error.")
@ -112,7 +123,6 @@ public class CalculatedField extends BaseData<CalculatedFieldId> implements HasN
.append(", configurationVersion=").append(configurationVersion)
.append(", configuration=").append(configuration)
.append(", version=").append(version)
.append(", externalId=").append(externalId)
.append(", createdTime=").append(createdTime)
.append(", id=").append(id).append(']')
.toString();

View File

@ -26,5 +26,6 @@ public class Output {
private String name;
private OutputType type;
private AttributeScope scope;
private Integer decimalsByDefault;
}

View File

@ -20,11 +20,13 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import org.thingsboard.server.common.data.EntityType;
import java.io.Serial;
import java.util.UUID;
@Schema
public class CalculatedFieldId extends UUIDBased implements EntityId {
@Serial
private static final long serialVersionUID = 1L;
@JsonCreator
@ -41,4 +43,5 @@ public class CalculatedFieldId extends UUIDBased implements EntityId {
public EntityType getEntityType() {
return EntityType.CALCULATED_FIELD;
}
}

View File

@ -38,4 +38,5 @@ public class DeviceExportData extends EntityExportData<Device> {
public boolean hasCredentials() {
return credentials != null;
}
}

View File

@ -26,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import lombok.Data;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.cf.CalculatedField;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.sync.JsonTbEntity;
@ -55,6 +56,8 @@ public class EntityExportData<E extends ExportableEntity<? extends EntityId>> {
public static final Comparator<AttributeExportData> attrComparator = Comparator
.comparing(AttributeExportData::getKey).thenComparing(AttributeExportData::getLastUpdateTs);
public static final Comparator<CalculatedField> calculatedFieldsComparator = Comparator.comparing(CalculatedField::getName);
@JsonProperty(index = 2)
@JsonTbEntity
private E entity;
@ -65,6 +68,9 @@ public class EntityExportData<E extends ExportableEntity<? extends EntityId>> {
private List<EntityRelation> relations;
@JsonProperty(index = 101)
private Map<String, List<AttributeExportData>> attributes;
@JsonProperty(index = 102)
@JsonIgnoreProperties({"id", "entityId", "createdTime", "version"})
private List<CalculatedField> calculatedFields;
public EntityExportData<E> sort() {
if (relations != null && !relations.isEmpty()) {
@ -73,6 +79,9 @@ public class EntityExportData<E extends ExportableEntity<? extends EntityId>> {
if (attributes != null && !attributes.isEmpty()) {
attributes.values().forEach(list -> list.sort(attrComparator));
}
if (calculatedFields != null && !calculatedFields.isEmpty()) {
calculatedFields.sort(calculatedFieldsComparator);
}
return this;
}
@ -96,4 +105,9 @@ public class EntityExportData<E extends ExportableEntity<? extends EntityId>> {
return relations != null;
}
@JsonIgnore
public boolean hasCalculatedFields() {
return calculatedFields != null;
}
}

View File

@ -25,7 +25,10 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
@Builder
public class EntityExportSettings {
private boolean exportRelations;
private boolean exportAttributes;
private boolean exportCredentials;
private boolean exportCalculatedFields;
}

View File

@ -25,8 +25,11 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
@Builder
public class EntityImportSettings {
private boolean findExistingByName;
private boolean updateRelations;
private boolean saveAttributes;
private boolean saveCredentials;
private boolean saveCalculatedFields;
}

View File

@ -17,13 +17,18 @@ package org.thingsboard.server.common.data.sync.vc.request.create;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
@Data
public class VersionCreateConfig implements Serializable {
@Serial
private static final long serialVersionUID = 1223723167716612772L;
private boolean saveRelations;
private boolean saveAttributes;
private boolean saveCredentials;
private boolean saveCalculatedFields;
}

View File

@ -23,5 +23,6 @@ public class VersionLoadConfig {
private boolean loadRelations;
private boolean loadAttributes;
private boolean loadCredentials;
private boolean loadCalculatedFields;
}

View File

@ -135,11 +135,11 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private double warnThreshold;
private long maxCalculatedFieldsPerEntity;
private long maxArgumentsPerCF;
private long maxDataPointsPerRollingArg;
private long maxStateSizeInKBytes;
private long maxSingleValueArgumentSizeInKBytes;
private long maxCalculatedFieldsPerEntity = 5;
private long maxArgumentsPerCF = 10;
private long maxDataPointsPerRollingArg = 1000;
private long maxStateSizeInKBytes = 32;
private long maxSingleValueArgumentSizeInKBytes = 2;
@Override
public long getProfileThreshold(ApiUsageRecordKey key) {

View File

@ -26,6 +26,7 @@ import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.aware.TenantAwareMsg;
import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
import java.io.Serial;
import java.util.Optional;
/**
@ -33,6 +34,8 @@ import java.util.Optional;
*/
@Data
public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg {
@Serial
private static final long serialVersionUID = -5303421482781273062L;
private final TenantId tenantId;
@ -66,4 +69,5 @@ public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg {
public MsgType getMsgType() {
return MsgType.COMPONENT_LIFE_CYCLE_MSG;
}
}

View File

@ -135,7 +135,7 @@ public class ProtoUtils {
builder.setName(msg.getName());
}
if (msg.getOldName() != null) {
builder.setName(msg.getOldName());
builder.setOldName(msg.getOldName());
}
return builder.build();
}
@ -171,7 +171,6 @@ public class ProtoUtils {
return entityTypeByProtoNumber[entityType.getNumber()];
}
public static TransportProtos.ToEdgeSyncRequestMsgProto toProto(ToEdgeSyncRequest request) {
return TransportProtos.ToEdgeSyncRequestMsgProto.newBuilder()
.setTenantIdMSB(request.getTenantId().getId().getMostSignificantBits())

View File

@ -136,7 +136,10 @@ public class DefaultTbelInvokeService extends AbstractScriptInvokeService implem
parserConfig.registerDataType("TbelCfSingleValueArg", TbelCfSingleValueArg.class, TbelCfSingleValueArg::memorySize);
parserConfig.registerDataType("TbelCfTsRollingArg", TbelCfTsRollingArg.class, TbelCfTsRollingArg::memorySize);
parserConfig.registerDataType("TbelCfTsDoubleVal", TbelCfTsDoubleVal.class, TbelCfTsDoubleVal::memorySize);
parserConfig.registerDataType("TbelCfTsRollingData", TbelCfTsRollingData.class, TbelCfTsRollingData::memorySize);
parserConfig.registerDataType("TbTimeWindow", TbTimeWindow.class, TbTimeWindow::memorySize);
parserConfig.registerDataType("TbelCfTsDoubleVal", TbelCfTsMultiDoubleVal.class, TbelCfTsMultiDoubleVal::memorySize);
TbUtils.register(parserConfig);
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(threadPoolSize, "tbel-executor"));
try {

View File

@ -17,20 +17,24 @@ package org.thingsboard.script.api.tbel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TbTimeWindow implements TbelCfObject {
public static final long OBJ_SIZE = 32L;
private long startTs;
private long endTs;
private int limit;
@Override
public long memorySize() {
return OBJ_SIZE;
}
public boolean matches(long ts) {
return ts >= startTs && ts < endTs;
}
}

View File

@ -255,6 +255,8 @@ public class TbUtils {
double.class, int.class)));
parserConfig.addImport("toFixed", new MethodStub(TbUtils.class.getMethod("toFixed",
float.class, int.class)));
parserConfig.addImport("toInt", new MethodStub(TbUtils.class.getMethod("toInt",
double.class)));
parserConfig.addImport("hexToBytes", new MethodStub(TbUtils.class.getMethod("hexToBytes",
ExecutionContext.class, String.class)));
parserConfig.addImport("hexToBytesArray", new MethodStub(TbUtils.class.getMethod("hexToBytesArray",
@ -1155,6 +1157,10 @@ public class TbUtils {
return BigDecimal.valueOf(value).setScale(precision, RoundingMode.HALF_UP).floatValue();
}
public static int toInt(double value) {
return BigDecimal.valueOf(value).setScale(0, RoundingMode.HALF_UP).intValue();
}
public static ExecutionHashMap<String, Object> toFlatMap(ExecutionContext ctx, Map<String, Object> json) {
return toFlatMap(ctx, json, new ArrayList<>(), true);
}
@ -1506,5 +1512,6 @@ public class TbUtils {
}
return hex;
}
}

View File

@ -0,0 +1,66 @@
/**
* 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.script.api.tbel;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
@Data
public class TbelCfTsMultiDoubleVal implements TbelCfObject {
public static final long OBJ_SIZE = 32L; // Approximate calculation;
private final long ts;
private final double[] values;
@JsonIgnore
public double getV1() {
return getV(0);
}
@JsonIgnore
public double getV2() {
return getV(1);
}
@JsonIgnore
public double getV3() {
return getV(2);
}
@JsonIgnore
public double getV4() {
return getV(3);
}
@JsonIgnore
public double getV5() {
return getV(4);
}
private double getV(int idx) {
if (values.length < idx + 1) {
throw new IllegalArgumentException("Can't get value at index " + idx + ". There are " + values.length + " values present.");
} else {
return values[idx];
}
}
@Override
public long memorySize() {
return OBJ_SIZE + values.length * 8L;
}
}

View File

@ -19,11 +19,15 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import org.thingsboard.common.util.JacksonUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.function.Consumer;
import static org.thingsboard.script.api.tbel.TbelCfTsDoubleVal.OBJ_SIZE;
@ -44,9 +48,9 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable<TbelCfTsDoubleVal
this.values = Collections.unmodifiableList(values);
}
public TbelCfTsRollingArg(int limit, long timeWindow, List<TbelCfTsDoubleVal> values) {
public TbelCfTsRollingArg(long timeWindow, List<TbelCfTsDoubleVal> values) {
long ts = System.currentTimeMillis();
this.timeWindow = new TbTimeWindow(ts - timeWindow, ts, limit);
this.timeWindow = new TbTimeWindow(ts - timeWindow, ts);
this.values = Collections.unmodifiableList(values);
}
@ -104,6 +108,14 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable<TbelCfTsDoubleVal
return min;
}
public double avg() {
return avg(true);
}
public double avg(boolean ignoreNaN) {
return mean(ignoreNaN);
}
public double mean() {
return mean(true);
}
@ -256,6 +268,88 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable<TbelCfTsDoubleVal
return sum;
}
public TbelCfTsRollingData merge(TbelCfTsRollingArg other) {
return mergeAll(Collections.singletonList(other), null);
}
public TbelCfTsRollingData merge(TbelCfTsRollingArg other, Map<String, Object> settings) {
return mergeAll(Collections.singletonList(other), settings);
}
public TbelCfTsRollingData mergeAll(List<TbelCfTsRollingArg> others) {
return mergeAll(others, null);
}
public TbelCfTsRollingData mergeAll(List<TbelCfTsRollingArg> others, Map<String, Object> settings) {
List<TbelCfTsRollingArg> args = new ArrayList<>(others.size() + 1);
args.add(this);
args.addAll(others);
boolean ignoreNaN = true;
if (settings != null && settings.containsKey("ignoreNaN")) {
ignoreNaN = Boolean.parseBoolean(settings.get("ignoreNaN").toString());
}
TbTimeWindow timeWindow = null;
if (settings != null && settings.containsKey("timeWindow")) {
var twVar = settings.get("timeWindow");
if (twVar instanceof TbTimeWindow) {
timeWindow = (TbTimeWindow) settings.get("timeWindow");
} else if (twVar instanceof Map twMap) {
timeWindow = new TbTimeWindow(Long.valueOf(twMap.get("startTs").toString()), Long.valueOf(twMap.get("endTs").toString()));
} else {
timeWindow = JacksonUtil.fromString(settings.get("timeWindow").toString(), TbTimeWindow.class);
}
}
TreeSet<Long> allTimestamps = new TreeSet<>();
long startTs = Long.MAX_VALUE;
long endTs = Long.MIN_VALUE;
for (TbelCfTsRollingArg arg : args) {
for (TbelCfTsDoubleVal val : arg.getValues()) {
allTimestamps.add(val.getTs());
}
startTs = Math.min(startTs, arg.getTimeWindow().getStartTs());
endTs = Math.max(endTs, arg.getTimeWindow().getEndTs());
}
List<TbelCfTsMultiDoubleVal> data = new ArrayList<>();
int[] lastIndex = new int[args.size()];
double[] result = new double[args.size()];
Arrays.fill(result, Double.NaN);
for (long ts : allTimestamps) {
for (int i = 0; i < args.size(); i++) {
var arg = args.get(i);
var values = arg.getValues();
while (lastIndex[i] < values.size() && values.get(lastIndex[i]).getTs() <= ts) {
result[i] = values.get(lastIndex[i]).getValue();
lastIndex[i]++;
}
}
if (timeWindow == null || timeWindow.matches(ts)) {
if (ignoreNaN) {
boolean skip = false;
for (int i = 0; i < args.size(); i++) {
if (Double.isNaN(result[i])) {
skip = true;
break;
}
}
if (!skip) {
data.add(new TbelCfTsMultiDoubleVal(ts, Arrays.copyOf(result, result.length)));
}
} else {
data.add(new TbelCfTsMultiDoubleVal(ts, Arrays.copyOf(result, result.length)));
}
}
}
return new TbelCfTsRollingData(timeWindow != null ? timeWindow : new TbTimeWindow(startTs, endTs), data);
}
@JsonIgnore
public int getSize() {
return values.size();
@ -266,11 +360,6 @@ public class TbelCfTsRollingArg implements TbelCfArg, Iterable<TbelCfTsDoubleVal
return values.iterator();
}
@Override
public void forEach(Consumer<? super TbelCfTsDoubleVal> action) {
values.forEach(action);
}
@Override
public String getType() {
return "TS_ROLLING";

View File

@ -0,0 +1,61 @@
/**
* 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.script.api.tbel;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;
import static org.thingsboard.script.api.tbel.TbelCfTsDoubleVal.OBJ_SIZE;
public class TbelCfTsRollingData implements TbelCfObject, Iterable<TbelCfTsMultiDoubleVal> {
@Getter
private final TbTimeWindow timeWindow;
@Getter
private final List<TbelCfTsMultiDoubleVal> values;
public TbelCfTsRollingData(TbTimeWindow timeWindow, List<TbelCfTsMultiDoubleVal> values) {
this.timeWindow = timeWindow;
this.values = Collections.unmodifiableList(values);
}
@Override
public long memorySize() {
return 12 + values.size() * OBJ_SIZE;
}
@JsonIgnore
public List<TbelCfTsMultiDoubleVal> getValue() {
return values;
}
@JsonIgnore
public int getSize() {
return values.size();
}
@Override
public Iterator<TbelCfTsMultiDoubleVal> iterator() {
return values.iterator();
}
}

View File

@ -1109,7 +1109,7 @@ public class TbUtilsTest {
String validInput = Base64.getEncoder().encodeToString(new byte[]{1, 2, 3, 4, 5});
ExecutionArrayList<Byte> actual = TbUtils.base64ToBytesList(ctx, validInput);
ExecutionArrayList<Byte> expected = new ExecutionArrayList<>(ctx);
expected.addAll(List.of((byte) 1, (byte)2, (byte)3, (byte)4, (byte)5));
expected.addAll(List.of((byte) 1, (byte) 2, (byte) 3, (byte) 4, (byte) 5));
Assertions.assertEquals(expected, actual);
String emptyInput = Base64.getEncoder().encodeToString(new byte[]{});
@ -1123,6 +1123,7 @@ public class TbUtilsTest {
TbUtils.base64ToBytesList(ctx, null);
});
}
@Test
public void bytesToHex_Test() {
byte[] bb = {(byte) 0xBB, (byte) 0xAA};
@ -1136,6 +1137,13 @@ public class TbUtilsTest {
Assertions.assertEquals(expected, actual);
}
@Test
void toInt() {
Assertions.assertEquals(1729, TbUtils.toInt(doubleVal));
Assertions.assertEquals(13, TbUtils.toInt(12.8));
Assertions.assertEquals(28, TbUtils.toInt(28.0));
}
private static List<Byte> toList(byte[] data) {
List<Byte> result = new ArrayList<>(data.length);
for (Byte b : data) {

View File

@ -15,10 +15,15 @@
*/
package org.thingsboard.script.api.tbel;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.thingsboard.common.util.JacksonUtil;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ -33,7 +38,7 @@ public class TbelCfTsRollingArgTest {
@BeforeEach
void setUp() {
rollingArg = new TbelCfTsRollingArg(
new TbTimeWindow(ts - 30000, ts - 10, 10),
new TbTimeWindow(ts - 30000, ts - 10),
List.of(
new TbelCfTsDoubleVal(ts - 10, Double.NaN),
new TbelCfTsDoubleVal(ts - 20, 2.0),
@ -98,7 +103,7 @@ public class TbelCfTsRollingArgTest {
void testFirstAndLastWhenOnlyNaNAndIgnoreNaNIsFalse() {
assertThat(rollingArg.first()).isEqualTo(2.0);
rollingArg = new TbelCfTsRollingArg(
new TbTimeWindow(ts - 30000, ts - 10, 10),
new TbTimeWindow(ts - 30000, ts - 10),
List.of(
new TbelCfTsDoubleVal(ts - 10, Double.NaN),
new TbelCfTsDoubleVal(ts - 40, Double.NaN),
@ -117,7 +122,7 @@ public class TbelCfTsRollingArgTest {
@Test
void testEmptyValues() {
rollingArg = new TbelCfTsRollingArg(new TbTimeWindow(0, 10, 10), List.of());
rollingArg = new TbelCfTsRollingArg(new TbTimeWindow(0, 10), List.of());
assertThatThrownBy(rollingArg::sum).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
assertThatThrownBy(rollingArg::max).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
assertThatThrownBy(rollingArg::min).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
@ -128,4 +133,81 @@ public class TbelCfTsRollingArgTest {
assertThatThrownBy(rollingArg::last).isInstanceOf(IllegalArgumentException.class).hasMessage("Rolling argument values are empty.");
}
@Test
public void merge_two_rolling_args_ts_match_test() {
TbTimeWindow tw = new TbTimeWindow(0, 60000);
TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3)));
TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13)));
var result = arg1.merge(arg2);
Assertions.assertEquals(3, result.getSize());
Assertions.assertNotNull(result.getValues());
Assertions.assertNotNull(result.getValues().get(0));
Assertions.assertEquals(1000L, result.getValues().get(0).getTs());
Assertions.assertEquals(1, result.getValues().get(0).getValues()[0]);
Assertions.assertEquals(11, result.getValues().get(0).getValues()[1]);
}
@Test
public void merge_two_rolling_args_with_timewindow_test() {
TbTimeWindow tw = new TbTimeWindow(0, 60000);
TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3)));
TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(1000, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13)));
var result = arg1.merge(arg2, Collections.singletonMap("timeWindow", new TbTimeWindow(0, 10000)));
Assertions.assertEquals(2, result.getSize());
Assertions.assertNotNull(result.getValues());
Assertions.assertNotNull(result.getValues().get(0));
Assertions.assertEquals(1000L, result.getValues().get(0).getTs());
Assertions.assertEquals(1, result.getValues().get(0).getValues()[0]);
Assertions.assertEquals(11, result.getValues().get(0).getValues()[1]);
result = arg1.merge(arg2, Collections.singletonMap("timeWindow", Map.of("startTs", 0L, "endTs", 10000)));
Assertions.assertEquals(2, result.getSize());
Assertions.assertNotNull(result.getValues());
Assertions.assertNotNull(result.getValues().get(0));
Assertions.assertEquals(1000L, result.getValues().get(0).getTs());
Assertions.assertEquals(1, result.getValues().get(0).getValues()[0]);
Assertions.assertEquals(11, result.getValues().get(0).getValues()[1]);
}
@Test
public void merge_two_rolling_args_ts_mismatch_default_test() {
TbTimeWindow tw = new TbTimeWindow(0, 60000);
TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(100, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3)));
TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(200, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13)));
var result = arg1.merge(arg2);
Assertions.assertEquals(3, result.getSize());
Assertions.assertNotNull(result.getValues());
TbelCfTsMultiDoubleVal item0 = result.getValues().get(0);
Assertions.assertNotNull(item0);
Assertions.assertEquals(200L, item0.getTs());
Assertions.assertEquals(1, item0.getValues()[0]);
Assertions.assertEquals(11, item0.getValues()[1]);
}
@Test
public void merge_two_rolling_args_ts_mismatch_ignore_nan_disabled_test() {
TbTimeWindow tw = new TbTimeWindow(0, 60000);
TbelCfTsRollingArg arg1 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(100, 1), new TbelCfTsDoubleVal(5000, 2), new TbelCfTsDoubleVal(15000, 3)));
TbelCfTsRollingArg arg2 = new TbelCfTsRollingArg(tw, Arrays.asList(new TbelCfTsDoubleVal(200, 11), new TbelCfTsDoubleVal(5000, 12), new TbelCfTsDoubleVal(15000, 13)));
var result = arg1.merge(arg2, Collections.singletonMap("ignoreNaN", false));
Assertions.assertEquals(4, result.getSize());
Assertions.assertNotNull(result.getValues());
TbelCfTsMultiDoubleVal item0 = result.getValues().get(0);
Assertions.assertNotNull(item0);
Assertions.assertEquals(100L, item0.getTs());
Assertions.assertEquals(1, item0.getValues()[0]);
Assertions.assertEquals(Double.NaN, item0.getValues()[1]);
TbelCfTsMultiDoubleVal item1 = result.getValues().get(1);
Assertions.assertEquals(200L, item1.getTs());
Assertions.assertEquals(1, item1.getValues()[0]);
Assertions.assertEquals(11, item1.getValues()[1]);
}
}

View File

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.dao.cf;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

View File

@ -729,7 +729,6 @@ public class ModelConstants {
public static final String CALCULATED_FIELD_CONFIGURATION_VERSION = "configuration_version";
public static final String CALCULATED_FIELD_CONFIGURATION = "configuration";
public static final String CALCULATED_FIELD_VERSION = "version";
public static final String CALCULATED_FIELD_EXTERNAL_ID = "external_id";
/**
* Calculated field links constants.

View File

@ -40,7 +40,6 @@ import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_C
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_CONFIGURATION_VERSION;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_ENTITY_ID;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_ENTITY_TYPE;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_EXTERNAL_ID;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_TABLE_NAME;
import static org.thingsboard.server.dao.model.ModelConstants.CALCULATED_FIELD_TENANT_ID_COLUMN;
@ -82,9 +81,6 @@ public class CalculatedFieldEntity extends BaseVersionedEntity<CalculatedField>
@Column(name = DEBUG_SETTINGS)
private String debugSettings;
@Column(name = CALCULATED_FIELD_EXTERNAL_ID)
private UUID externalId;
public CalculatedFieldEntity() {
super();
}
@ -101,9 +97,6 @@ public class CalculatedFieldEntity extends BaseVersionedEntity<CalculatedField>
this.configuration = JacksonUtil.valueToTree(calculatedField.getConfiguration());
this.version = calculatedField.getVersion();
this.debugSettings = JacksonUtil.toString(calculatedField.getDebugSettings());
if (calculatedField.getExternalId() != null) {
this.externalId = calculatedField.getExternalId().getId();
}
}
@Override
@ -118,9 +111,6 @@ public class CalculatedFieldEntity extends BaseVersionedEntity<CalculatedField>
calculatedField.setConfiguration(JacksonUtil.treeToValue(configuration, CalculatedFieldConfiguration.class));
calculatedField.setVersion(version);
calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class));
if (externalId != null) {
calculatedField.setExternalId(new CalculatedFieldId(externalId));
}
return calculatedField;
}

View File

@ -92,7 +92,6 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF
calculatedField.setConfiguration(JacksonUtil.treeToValue(configuration, CalculatedFieldConfiguration.class));
calculatedField.setVersion(version);
calculatedField.setDebugSettings(JacksonUtil.fromString(debugSettings, DebugSettings.class));
calculatedField.setExternalId(externalIdObj != null ? new CalculatedFieldId(UUID.fromString((String) externalIdObj)) : null);
return calculatedField;
}).collect(Collectors.toList());

View File

@ -936,9 +936,7 @@ CREATE TABLE IF NOT EXISTS calculated_field (
configuration varchar(1000000),
version BIGINT DEFAULT 1,
debug_settings varchar(1024),
external_id UUID,
CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, name),
CONSTRAINT calculated_field_external_id_unq_key UNIQUE (tenant_id, external_id)
CONSTRAINT calculated_field_unq_key UNIQUE (entity_id, name)
);
CREATE TABLE IF NOT EXISTS calculated_field_link (

View File

@ -101,20 +101,6 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
.hasMessage("Calculated Field with such name is already in exists!");
}
@Test
public void testSaveCalculatedFieldWithExistingExternalId() {
Device device = createTestDevice();
CalculatedField calculatedField = getCalculatedField(device.getId(), device.getId());
calculatedField.setExternalId(new CalculatedFieldId(UUID.fromString("2ef69d0a-89cf-4868-86f8-c50551d87ebe")));
calculatedFieldService.save(calculatedField);
calculatedField.setName("Test 2");
assertThatThrownBy(() -> calculatedFieldService.save(calculatedField))
.isInstanceOf(DataValidationException.class)
.hasMessage("Calculated Field with such external id already exists!");
}
@Test
public void testFindCalculatedFieldById() {
CalculatedField savedCalculatedField = saveValidCalculatedField();

View File

@ -23,6 +23,7 @@ import { CalculatedField, CalculatedFieldTestScriptInputParams } from '@shared/m
import { PageLink } from '@shared/models/page/page-link';
import { EntityId } from '@shared/models/id/entity-id';
import { EntityTestScriptResult } from '@shared/models/entity.models';
import { CalculatedFieldEventBody } from '@shared/models/event.models';
@Injectable({
providedIn: 'root'
@ -53,4 +54,8 @@ export class CalculatedFieldsService {
public testScript(inputParams: CalculatedFieldTestScriptInputParams, config?: RequestConfig): Observable<EntityTestScriptResult> {
return this.http.post<EntityTestScriptResult>('/api/calculatedField/testScript', inputParams, defaultHttpOptionsFromConfig(config));
}
public getLatestCalculatedFieldDebugEvent(id: string, config?: RequestConfig): Observable<CalculatedFieldEventBody> {
return this.http.get<CalculatedFieldEventBody>(`/api/calculatedField/${id}/debug`, defaultHttpOptionsFromConfig(config));
}
}

View File

@ -35,10 +35,11 @@ import {
import { noLeadTrailSpacesRegex } from '@shared/models/regex.constants';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { EntityType } from '@shared/models/entity-type.models';
import { map, startWith } from 'rxjs/operators';
import { map, startWith, switchMap } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { ScriptLanguage } from '@shared/models/rule-node.models';
import { CalculatedFieldsService } from '@core/http/calculated-fields.service';
import { Observable } from 'rxjs';
@Component({
selector: 'tb-calculated-field-dialog',
@ -136,7 +137,23 @@ export class CalculatedFieldDialogComponent extends DialogComponent<CalculatedFi
}
onTestScript(): void {
this.data.getTestScriptDialogFn(this.fromGroupValue, null, false).subscribe(expression => {
const calculatedFieldId = this.data.value?.id?.id;
let testScriptDialogResult$: Observable<string>;
if (calculatedFieldId) {
testScriptDialogResult$ = this.calculatedFieldsService.getLatestCalculatedFieldDebugEvent(calculatedFieldId)
.pipe(
switchMap(event => {
const args = event?.arguments ? JSON.parse(event.arguments) : null;
return this.data.getTestScriptDialogFn(this.fromGroupValue, args, false);
}),
takeUntilDestroyed(this.destroyRef)
)
} else {
testScriptDialogResult$ = this.data.getTestScriptDialogFn(this.fromGroupValue, null, false);
}
testScriptDialogResult$.subscribe(expression => {
this.configFormGroup.get('expressionSCRIPT').setValue(expression);
this.configFormGroup.get('expressionSCRIPT').markAsDirty();
});

View File

@ -84,6 +84,9 @@
<mat-checkbox formControlName="saveRelations">
{{ 'version-control.export-relations' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveCalculatedFields">
{{ 'version-control.export-calculated-fields' | translate }}
</mat-checkbox>
</div>
</div>
</div>

View File

@ -25,7 +25,11 @@ import { TranslateService } from '@ngx-translate/core';
import { DialogService } from '@core/services/dialog.service';
import { catchError, map, mergeMap } from 'rxjs/operators';
import { Observable, of } from 'rxjs';
import { EntityTypeVersionCreateConfig, exportableEntityTypes } from '@shared/models/vc.models';
import {
EntityTypeVersionCreateConfig,
exportableEntityTypes,
typesWithCalculatedFields
} from '@shared/models/vc.models';
import { EntityType, entityTypeTranslations } from '@shared/models/entity-type.models';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@ -43,6 +47,8 @@ export class AutoCommitSettingsComponent extends PageComponent implements OnInit
isReadOnly: Observable<boolean>;
readonly typesWithCalculatedFields = typesWithCalculatedFields;
constructor(protected store: Store<AppState>,
private adminService: AdminService,
private dialogService: DialogService,
@ -104,7 +110,8 @@ export class AutoCommitSettingsComponent extends PageComponent implements OnInit
branch: null,
saveAttributes: true,
saveRelations: false,
saveCredentials: true
saveCredentials: true,
saveCalculatedFields: true,
};
const allowed = this.allowedEntityTypes();
let entityType: EntityType = null;
@ -206,7 +213,8 @@ export class AutoCommitSettingsComponent extends PageComponent implements OnInit
branch: [config.branch, []],
saveRelations: [config.saveRelations, []],
saveAttributes: [config.saveAttributes, []],
saveCredentials: [config.saveCredentials, []]
saveCredentials: [config.saveCredentials, []],
saveCalculatedFields: [config.saveCalculatedFields, []]
})
}
);

View File

@ -72,6 +72,9 @@
<mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveRelations">
{{ 'version-control.export-relations' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)" formControlName="saveCalculatedFields">
{{ 'version-control.export-calculated-fields' | translate }}
</mat-checkbox>
</div>
</div>
</div>

View File

@ -33,7 +33,8 @@ import {
EntityTypeVersionCreateConfig,
exportableEntityTypes,
SyncStrategy,
syncStrategyTranslationMap
syncStrategyTranslationMap,
typesWithCalculatedFields
} from '@shared/models/vc.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -79,6 +80,8 @@ export class EntityTypesVersionCreateComponent extends PageComponent implements
loading = true;
readonly typesWithCalculatedFields = typesWithCalculatedFields;
constructor(protected store: Store<AppState>,
private translate: TranslateService,
private fb: UntypedFormBuilder,
@ -150,6 +153,7 @@ export class EntityTypesVersionCreateComponent extends PageComponent implements
saveRelations: [config.saveRelations, []],
saveAttributes: [config.saveAttributes, []],
saveCredentials: [config.saveCredentials, []],
saveCalculatedFields: [config.saveCalculatedFields, []],
allEntities: [config.allEntities, []],
entityIds: [config.entityIds, [Validators.required]]
})
@ -202,6 +206,7 @@ export class EntityTypesVersionCreateComponent extends PageComponent implements
saveAttributes: true,
saveRelations: true,
saveCredentials: true,
saveCalculatedFields: true,
allEntities: true,
entityIds: []
};

View File

@ -72,6 +72,9 @@
<mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityTypeFormGroup.get('entityType').value)" formControlName="loadRelations">
{{ 'version-control.load-relations' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="typesWithCalculatedFields.has(entityTypeFormGroup.get('entityType').value)" formControlName="loadCalculatedFields">
{{ 'version-control.load-calculated-fields' | translate }}
</mat-checkbox>
</div>
</div>
</div>

View File

@ -31,7 +31,8 @@ import { PageComponent } from '@shared/components/page.component';
import {
entityTypesWithoutRelatedData,
EntityTypeVersionLoadConfig,
exportableEntityTypes
exportableEntityTypes,
typesWithCalculatedFields
} from '@shared/models/vc.models';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -75,6 +76,8 @@ export class EntityTypesVersionLoadComponent extends PageComponent implements On
loading = true;
readonly typesWithCalculatedFields = typesWithCalculatedFields;
constructor(protected store: Store<AppState>,
private translate: TranslateService,
private popoverService: TbPopoverService,
@ -145,6 +148,7 @@ export class EntityTypesVersionLoadComponent extends PageComponent implements On
loadRelations: [config.loadRelations, []],
loadAttributes: [config.loadAttributes, []],
loadCredentials: [config.loadCredentials, []],
loadCalculatedFields: [config.loadCalculatedFields, []],
removeOtherEntities: [config.removeOtherEntities, []],
findExistingEntityByName: [config.findExistingEntityByName, []]
})
@ -180,6 +184,7 @@ export class EntityTypesVersionLoadComponent extends PageComponent implements On
loadAttributes: true,
loadRelations: true,
loadCredentials: true,
loadCalculatedFields: true,
removeOtherEntities: false,
findExistingEntityByName: true
};

View File

@ -47,6 +47,9 @@
<mat-checkbox *ngIf="!entityTypesWithoutRelatedData.has(entityId.entityType)" formControlName="saveRelations" style="margin-bottom: 16px;">
{{ 'version-control.export-relations' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="typesWithCalculatedFields.has(entityId.entityType)" formControlName="saveCalculatedFields" class="mb-4">
{{ 'version-control.export-calculated-fields' | translate }}
</mat-checkbox>
</div>
</fieldset>
</form>

View File

@ -20,6 +20,7 @@ import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms
import {
entityTypesWithoutRelatedData,
SingleEntityVersionCreateRequest,
typesWithCalculatedFields,
VersionCreateRequestType,
VersionCreationResult
} from '@shared/models/vc.models';
@ -71,6 +72,8 @@ export class EntityVersionCreateComponent extends PageComponent implements OnIni
private versionCreateResultSubscription: Subscription;
readonly typesWithCalculatedFields = typesWithCalculatedFields;
constructor(protected store: Store<AppState>,
private entitiesVersionControlService: EntitiesVersionControlService,
private cd: ChangeDetectorRef,
@ -115,7 +118,8 @@ export class EntityVersionCreateComponent extends PageComponent implements OnIni
? this.createVersionFormGroup.get('saveRelations').value : false,
saveAttributes: !entityTypesWithoutRelatedData.has(this.entityId.entityType)
? this.createVersionFormGroup.get('saveAttributes').value : false,
saveCredentials: this.entityId.entityType === EntityType.DEVICE ? this.createVersionFormGroup.get('saveCredentials').value : false
saveCredentials: this.entityId.entityType === EntityType.DEVICE ? this.createVersionFormGroup.get('saveCredentials').value : false,
saveCalculatedFields: typesWithCalculatedFields.has(this.entityId.entityType) ? this.createVersionFormGroup.get('saveCalculatedFields').value : false,
},
type: VersionCreateRequestType.SINGLE_ENTITY
};

View File

@ -36,6 +36,9 @@
<mat-checkbox *ngIf="entityDataInfo.hasRelations" formControlName="loadRelations" style="margin-bottom: 16px;">
{{ 'version-control.load-relations' | translate }}
</mat-checkbox>
<mat-checkbox *ngIf="entityDataInfo.hasCalculatedFields" formControlName="loadCalculatedFields" class="mb-4">
{{ 'version-control.load-calculated-fields' | translate }}
</mat-checkbox>
</div>
</fieldset>
</form>

View File

@ -79,7 +79,8 @@ export class EntityVersionRestoreComponent extends PageComponent implements OnIn
this.restoreFormGroup = this.fb.group({
loadAttributes: [true, []],
loadRelations: [true, []],
loadCredentials: [true, []]
loadCredentials: [true, []],
loadCalculatedFields: [true, []]
});
this.entitiesVersionControlService.getEntityDataInfo(this.externalEntityId, this.versionId).subscribe((data) => {
this.entityDataInfo = data;
@ -110,7 +111,8 @@ export class EntityVersionRestoreComponent extends PageComponent implements OnIn
config: {
loadRelations: this.entityDataInfo.hasRelations ? this.restoreFormGroup.get('loadRelations').value : false,
loadAttributes: this.entityDataInfo.hasAttributes ? this.restoreFormGroup.get('loadAttributes').value : false,
loadCredentials: this.entityDataInfo.hasCredentials ? this.restoreFormGroup.get('loadCredentials').value : false
loadCredentials: this.entityDataInfo.hasCredentials ? this.restoreFormGroup.get('loadCredentials').value : false,
loadCalculatedFields: this.entityDataInfo.hasCalculatedFields ? this.restoreFormGroup.get('loadCalculatedFields').value : false
},
type: VersionLoadRequestType.SINGLE_ENTITY
};

View File

@ -272,7 +272,7 @@ export const CalculatedFieldAttributeValueArgumentAutocomplete = {
export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
max: {
meta: 'function',
description: 'Computes the maximum value in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
description: 'Returns the maximum value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
args: [
{
name: 'ignoreNaN',
@ -288,7 +288,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
},
min: {
meta: 'function',
description: 'Computes the minimum value in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
description: 'Returns the minimum value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
args: [
{
name: 'ignoreNaN',
@ -304,7 +304,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
},
mean: {
meta: 'function',
description: 'Computes the mean value of the rolling argument values list. Returns NaN if any value is NaN and ignoreNaN is false.',
description: 'Computes the mean value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
args: [
{
name: 'ignoreNaN',
@ -318,9 +318,25 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
type: 'number'
}
},
avg: {
meta: 'function',
description: 'Computes the average value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
args: [
{
name: 'ignoreNaN',
description: 'Whether to ignore NaN values. Equals true by default.',
type: 'boolean',
optional: true,
}
],
return: {
description: 'The average value, or NaN if applicable',
type: 'number'
}
},
std: {
meta: 'function',
description: 'Computes the standard deviation in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
description: 'Computes the standard deviation of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
args: [
{
name: 'ignoreNaN',
@ -336,7 +352,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
},
median: {
meta: 'function',
description: 'Computes the median value of the rolling argument values list. Returns NaN if any value is NaN and ignoreNaN is false.',
description: 'Computes the median value of the rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
args: [
{
name: 'ignoreNaN',
@ -352,7 +368,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
},
count: {
meta: 'function',
description: 'Counts values in the list of rolling argument values. Counts non-NaN values if ignoreNaN is true, otherwise - total size.',
description: 'Counts values of the rolling argument. Counts non-NaN values if ignoreNaN is true, otherwise - total size.',
args: [
{
name: 'ignoreNaN',
@ -368,7 +384,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
},
last: {
meta: 'function',
description: 'Returns the last non-NaN value in the list of rolling argument values if ignoreNaN is true, otherwise - the last value.',
description: 'Returns the last non-NaN value of the rolling argument values if ignoreNaN is true, otherwise - the last value.',
args: [
{
name: 'ignoreNaN',
@ -384,7 +400,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
},
first: {
meta: 'function',
description: 'Returns the first non-NaN value in the list of rolling argument values if ignoreNaN is true, otherwise - the first value.',
description: 'Returns the first non-NaN value of the rolling argument values if ignoreNaN is true, otherwise - the first value.',
args: [
{
name: 'ignoreNaN',
@ -400,7 +416,7 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
},
sum: {
meta: 'function',
description: 'Computes the sum of values in the list of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
description: 'Computes the sum of rolling argument values. Returns NaN if any value is NaN and ignoreNaN is false.',
args: [
{
name: 'ignoreNaN',
@ -413,12 +429,56 @@ export const CalculatedFieldRollingValueArgumentFunctionsAutocomplete = {
description: 'The sum of values, or NaN if applicable',
type: 'number'
}
},
merge: {
meta: 'function',
description: 'Merges current object with other time series rolling argument into a single object by aligning their timestamped values. Supports optional configurable settings.',
args: [
{
name: 'other',
description: "A time series rolling argument to be merged with the current object.",
type: "object",
optional: true
},
{
name: "settings",
description: "Optional settings controlling the merging process. Supported keys: 'ignoreNaN' (boolean, equals true by default) to determine whether NaN values should be ignored; 'timeWindow' (object, empty by default) to apply time window filtering.",
type: "object",
optional: true
}
],
return: {
description: 'A new object containing merged timestamped values from all provided arguments, aligned based on timestamps and filtered according to settings.',
type: '{ values: { ts: number; values: number[]; }[]; timeWindow: { startTs: number; endTs: number } }; }',
}
},
mergeAll: {
meta: 'function',
description: 'Merges current object with other time series rolling arguments into a single object by aligning their timestamped values. Supports optional configurable settings.',
args: [
{
name: 'others',
description: "A list of time series rolling arguments to be merged with the current object.",
type: "object[]",
optional: true
},
{
name: "settings",
description: "Optional settings controlling the merging process. Supported keys: 'ignoreNaN' (boolean, equals true by default) to determine whether NaN values should be ignored; 'timeWindow' (object, empty by default) to apply time window filtering.",
type: "object",
optional: true
}
],
return: {
description: 'A new object containing merged timestamped values from all provided arguments, aligned based on timestamps and filtered according to settings.',
type: '{ values: { ts: number; values: number[]; }[]; timeWindow: { startTs: number; endTs: number } }; }',
}
}
};
export const CalculatedFieldRollingValueArgumentAutocomplete = {
meta: 'object',
type: '{ values: { ts: number; value: any; }[]; timeWindow: { startTs: number; endTs: number; limit: number } }; }',
type: '{ values: { ts: number; value: number; }[]; timeWindow: { startTs: number; endTs: number } }; }',
description: 'Calculated field rolling value argument.',
children: {
...CalculatedFieldRollingValueArgumentFunctionsAutocomplete,
@ -429,7 +489,7 @@ export const CalculatedFieldRollingValueArgumentAutocomplete = {
},
timeWindow: {
meta: 'object',
type: '{ startTs: number; endTs: number; limit: number }',
type: '{ startTs: number; endTs: number }',
description: 'Time window configuration',
children: {
startTs: {
@ -441,11 +501,6 @@ export const CalculatedFieldRollingValueArgumentAutocomplete = {
meta: 'number',
type: 'number',
description: 'End time stamp',
},
limit: {
meta: 'number',
type: 'number',
description: 'Limit',
}
}
}
@ -504,7 +559,7 @@ const calculatedFieldSingleArgumentValueHighlightRules: AceHighlightRules = {
}
const calculatedFieldRollingArgumentValueFunctionsHighlightRules: Array<AceHighlightRule> =
['max', 'min', 'mean', 'std', 'median', 'count', 'last', 'first', 'sum'].map(funcName => ({
['max', 'min', 'avg', 'mean', 'std', 'median', 'count', 'last', 'first', 'sum', 'merge', 'mergeAll'].map(funcName => ({
token: 'tb.calculated-field-func',
regex: `\\b${funcName}\\b`,
next: 'no_regex'

View File

@ -156,11 +156,11 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan
rpcTtlDays: 0,
queueStatsTtlDays: 0,
ruleEngineExceptionsTtlDays: 0,
maxCalculatedFieldsPerEntity: 0,
maxArgumentsPerCF: 0,
maxDataPointsPerRollingArg: 0,
maxStateSizeInKBytes: 0,
maxSingleValueArgumentSizeInKBytes: 0,
maxCalculatedFieldsPerEntity: 5,
maxArgumentsPerCF: 10,
maxDataPointsPerRollingArg: 1000,
maxStateSizeInKBytes: 32,
maxSingleValueArgumentSizeInKBytes: 2,
calculatedFieldDebugEventsRateLimit: ''
};
configuration = {...defaultConfiguration, type: TenantProfileType.DEFAULT};

View File

@ -49,6 +49,7 @@ export interface VersionCreateConfig {
saveRelations: boolean;
saveAttributes: boolean;
saveCredentials: boolean;
saveCalculatedFields: boolean;
}
export enum VersionCreateRequestType {
@ -106,6 +107,7 @@ export function createDefaultEntityTypesVersionCreate(): {[entityType: string]:
syncStrategy: null,
saveAttributes: !entityTypesWithoutRelatedData.has(entityType),
saveRelations: !entityTypesWithoutRelatedData.has(entityType),
saveCalculatedFields: typesWithCalculatedFields.has(entityType),
saveCredentials: true,
allEntities: true,
entityIds: []
@ -118,6 +120,7 @@ export interface VersionLoadConfig {
loadRelations: boolean;
loadAttributes: boolean;
loadCredentials: boolean;
loadCalculatedFields: boolean;
}
export enum VersionLoadRequestType {
@ -154,6 +157,7 @@ export function createDefaultEntityTypesVersionLoad(): {[entityType: string]: En
loadAttributes: !entityTypesWithoutRelatedData.has(entityType),
loadRelations: !entityTypesWithoutRelatedData.has(entityType),
loadCredentials: true,
loadCalculatedFields: typesWithCalculatedFields.has(entityType),
removeOtherEntities: false,
findExistingEntityByName: true
};
@ -254,4 +258,7 @@ export interface EntityDataInfo {
hasRelations: boolean;
hasAttributes: boolean;
hasCredentials: boolean;
hasCalculatedFields: boolean;
}
export const typesWithCalculatedFields = new Set<EntityType | AliasEntityType>([EntityType.DEVICE, EntityType.ASSET, EntityType.ASSET_PROFILE, EntityType.DEVICE_PROFILE]);

View File

@ -6320,6 +6320,7 @@
"export-relations": "Export relations",
"export-attributes": "Export attributes",
"export-credentials": "Export credentials",
"export-calculated-fields": "Export calculated fields",
"entity-versions": "Entity versions",
"versions": "Versions",
"created-time": "Created time",
@ -6336,6 +6337,7 @@
"load-relations": "Load relations",
"load-attributes": "Load attributes",
"load-credentials": "Load credentials",
"load-calculated-fields": "Load calculated fields",
"compare-with-current": "Compare with current",
"diff-entity-with-version": "Diff with entity version '{{versionName}}'",
"previous-difference": "Previous Difference",