Merge branch 'master' into fix/5683-timeseries-table-export
This commit is contained in:
commit
fbf2b92d9d
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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))));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -26,5 +26,6 @@ public class Output {
|
||||
private String name;
|
||||
private OutputType type;
|
||||
private AttributeScope scope;
|
||||
private Integer decimalsByDefault;
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -38,4 +38,5 @@ public class DeviceExportData extends EntityExportData<Device> {
|
||||
public boolean hasCredentials() {
|
||||
return credentials != null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -25,7 +25,10 @@ import lombok.NoArgsConstructor;
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public class EntityExportSettings {
|
||||
|
||||
private boolean exportRelations;
|
||||
private boolean exportAttributes;
|
||||
private boolean exportCredentials;
|
||||
private boolean exportCalculatedFields;
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
}
|
||||
|
||||
@ -23,5 +23,6 @@ public class VersionLoadConfig {
|
||||
private boolean loadRelations;
|
||||
private boolean loadAttributes;
|
||||
private boolean loadCredentials;
|
||||
private boolean loadCalculatedFields;
|
||||
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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, []]
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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: []
|
||||
};
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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};
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user