added last records type of cf
This commit is contained in:
parent
2c7c6f0c5e
commit
c6d91c4ce8
@ -48,12 +48,16 @@ import org.thingsboard.server.common.data.id.DeviceProfileId;
|
|||||||
import org.thingsboard.server.common.data.id.EntityId;
|
import org.thingsboard.server.common.data.id.EntityId;
|
||||||
import org.thingsboard.server.common.data.id.EntityIdFactory;
|
import org.thingsboard.server.common.data.id.EntityIdFactory;
|
||||||
import org.thingsboard.server.common.data.id.TenantId;
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
|
import org.thingsboard.server.common.data.kv.Aggregation;
|
||||||
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
|
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
|
||||||
|
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
|
||||||
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
|
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
|
||||||
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
|
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
|
||||||
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
|
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
|
||||||
import org.thingsboard.server.common.data.kv.KvEntry;
|
import org.thingsboard.server.common.data.kv.KvEntry;
|
||||||
|
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
|
||||||
import org.thingsboard.server.common.data.kv.StringDataEntry;
|
import org.thingsboard.server.common.data.kv.StringDataEntry;
|
||||||
|
import org.thingsboard.server.common.data.kv.TsKvEntry;
|
||||||
import org.thingsboard.server.common.data.msg.TbMsgType;
|
import org.thingsboard.server.common.data.msg.TbMsgType;
|
||||||
import org.thingsboard.server.common.data.page.PageDataIterable;
|
import org.thingsboard.server.common.data.page.PageDataIterable;
|
||||||
import org.thingsboard.server.common.msg.TbMsg;
|
import org.thingsboard.server.common.msg.TbMsg;
|
||||||
@ -69,7 +73,10 @@ import org.thingsboard.server.gen.transport.TransportProtos;
|
|||||||
import org.thingsboard.server.queue.util.TbCoreComponent;
|
import org.thingsboard.server.queue.util.TbCoreComponent;
|
||||||
import org.thingsboard.server.service.cf.ctx.CalculatedFieldCtx;
|
import org.thingsboard.server.service.cf.ctx.CalculatedFieldCtx;
|
||||||
import org.thingsboard.server.service.cf.ctx.CalculatedFieldCtxId;
|
import org.thingsboard.server.service.cf.ctx.CalculatedFieldCtxId;
|
||||||
|
import org.thingsboard.server.service.cf.ctx.state.ArgumentEntry;
|
||||||
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
|
import org.thingsboard.server.service.cf.ctx.state.CalculatedFieldState;
|
||||||
|
import org.thingsboard.server.service.cf.ctx.state.CalculationContext;
|
||||||
|
import org.thingsboard.server.service.cf.ctx.state.LastRecordsCalculatedFieldState;
|
||||||
import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState;
|
import org.thingsboard.server.service.cf.ctx.state.ScriptCalculatedFieldState;
|
||||||
import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState;
|
import org.thingsboard.server.service.cf.ctx.state.SimpleCalculatedFieldState;
|
||||||
import org.thingsboard.server.service.partition.AbstractPartitionBasedService;
|
import org.thingsboard.server.service.partition.AbstractPartitionBasedService;
|
||||||
@ -84,6 +91,7 @@ import java.util.UUID;
|
|||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ConcurrentMap;
|
import java.util.concurrent.ConcurrentMap;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.thingsboard.server.common.data.DataConstants.SCOPE;
|
import static org.thingsboard.server.common.data.DataConstants.SCOPE;
|
||||||
|
|
||||||
@ -109,6 +117,8 @@ public class DefaultCalculatedFieldExecutionService extends AbstractPartitionBas
|
|||||||
private final ConcurrentMap<CalculatedFieldId, List<CalculatedFieldLink>> calculatedFieldLinks = new ConcurrentHashMap<>();
|
private final ConcurrentMap<CalculatedFieldId, List<CalculatedFieldLink>> calculatedFieldLinks = new ConcurrentHashMap<>();
|
||||||
private final ConcurrentMap<CalculatedFieldCtxId, CalculatedFieldCtx> states = new ConcurrentHashMap<>();
|
private final ConcurrentMap<CalculatedFieldCtxId, CalculatedFieldCtx> states = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private static final int MAX_LAST_RECORDS_VALUE = 1024;
|
||||||
|
|
||||||
@Value("${calculatedField.initFetchPackSize:50000}")
|
@Value("${calculatedField.initFetchPackSize:50000}")
|
||||||
@Getter
|
@Getter
|
||||||
private int initFetchPackSize;
|
private int initFetchPackSize;
|
||||||
@ -215,7 +225,19 @@ public class DefaultCalculatedFieldExecutionService extends AbstractPartitionBas
|
|||||||
public void onTelemetryUpdate(TenantId tenantId, CalculatedFieldId calculatedFieldId, Map<String, KvEntry> updatedTelemetry) {
|
public void onTelemetryUpdate(TenantId tenantId, CalculatedFieldId calculatedFieldId, Map<String, KvEntry> updatedTelemetry) {
|
||||||
try {
|
try {
|
||||||
CalculatedField calculatedField = calculatedFields.computeIfAbsent(calculatedFieldId, id -> calculatedFieldService.findById(tenantId, id));
|
CalculatedField calculatedField = calculatedFields.computeIfAbsent(calculatedFieldId, id -> calculatedFieldService.findById(tenantId, id));
|
||||||
updateOrInitializeState(calculatedField, calculatedField.getEntityId(), updatedTelemetry);
|
Map<String, ArgumentEntry> argumentValues = updatedTelemetry.entrySet().stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
Map.Entry::getKey,
|
||||||
|
entry -> {
|
||||||
|
ArgumentEntry argumentEntry = new ArgumentEntry();
|
||||||
|
argumentEntry.setKvEntry(entry.getValue());
|
||||||
|
if (entry.getValue() instanceof TsKvEntry) {
|
||||||
|
argumentEntry.setKvEntries(List.of((TsKvEntry) entry.getValue()));
|
||||||
|
}
|
||||||
|
return argumentEntry;
|
||||||
|
}
|
||||||
|
));
|
||||||
|
updateOrInitializeState(calculatedField, calculatedField.getEntityId(), argumentValues);
|
||||||
log.info("Successfully updated time series for calculatedFieldId: [{}]", calculatedFieldId);
|
log.info("Successfully updated time series for calculatedFieldId: [{}]", calculatedFieldId);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.trace("Failed to update telemetry for calculatedFieldId: [{}]", calculatedFieldId, e);
|
log.trace("Failed to update telemetry for calculatedFieldId: [{}]", calculatedFieldId, e);
|
||||||
@ -308,12 +330,12 @@ public class DefaultCalculatedFieldExecutionService extends AbstractPartitionBas
|
|||||||
|
|
||||||
private void initializeStateForEntity(TenantId tenantId, CalculatedField calculatedField, EntityId entityId, TbCallback callback) {
|
private void initializeStateForEntity(TenantId tenantId, CalculatedField calculatedField, EntityId entityId, TbCallback callback) {
|
||||||
Map<String, Argument> arguments = calculatedField.getConfiguration().getArguments();
|
Map<String, Argument> arguments = calculatedField.getConfiguration().getArguments();
|
||||||
Map<String, KvEntry> argumentValues = new HashMap<>();
|
Map<String, ArgumentEntry> argumentValues = new HashMap<>();
|
||||||
AtomicInteger remaining = new AtomicInteger(arguments.size());
|
AtomicInteger remaining = new AtomicInteger(arguments.size());
|
||||||
arguments.forEach((key, argument) -> Futures.addCallback(fetchArgumentValue(tenantId, argument, entityId), new FutureCallback<>() {
|
arguments.forEach((key, argument) -> Futures.addCallback(fetchArgumentValue(calculatedField, argument), new FutureCallback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Optional<? extends KvEntry> result) {
|
public void onSuccess(ArgumentEntry result) {
|
||||||
argumentValues.put(key, result.orElse(null));
|
argumentValues.put(key, result);
|
||||||
if (remaining.decrementAndGet() == 0) {
|
if (remaining.decrementAndGet() == 0) {
|
||||||
updateOrInitializeState(calculatedField, entityId, argumentValues);
|
updateOrInitializeState(calculatedField, entityId, argumentValues);
|
||||||
}
|
}
|
||||||
@ -327,10 +349,37 @@ public class DefaultCalculatedFieldExecutionService extends AbstractPartitionBas
|
|||||||
}, calculatedFieldCallbackExecutor));
|
}, calculatedFieldCallbackExecutor));
|
||||||
}
|
}
|
||||||
|
|
||||||
private ListenableFuture<Optional<? extends KvEntry>> fetchArgumentValue(TenantId tenantId, Argument argument, EntityId targetEntityId) {
|
private ListenableFuture<ArgumentEntry> fetchArgumentValue(CalculatedField calculatedField, Argument argument) {
|
||||||
|
TenantId tenantId = calculatedField.getTenantId();
|
||||||
|
EntityId cfEntityId = calculatedField.getEntityId();
|
||||||
EntityId argumentEntityId = argument.getEntityId();
|
EntityId argumentEntityId = argument.getEntityId();
|
||||||
EntityId entityId = EntityType.DEVICE_PROFILE.equals(argumentEntityId.getEntityType()) || EntityType.ASSET_PROFILE.equals(argumentEntityId.getEntityType()) ? targetEntityId : argumentEntityId;
|
EntityId entityId = EntityType.DEVICE_PROFILE.equals(argumentEntityId.getEntityType()) || EntityType.ASSET_PROFILE.equals(argumentEntityId.getEntityType())
|
||||||
return switch (argument.getType()) {
|
? cfEntityId
|
||||||
|
: argumentEntityId;
|
||||||
|
if (CalculatedFieldType.LAST_RECORDS.equals(calculatedField.getType())) {
|
||||||
|
return fetchLastRecords(tenantId, entityId, argument);
|
||||||
|
}
|
||||||
|
return fetchKvEntry(tenantId, entityId, argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListenableFuture<ArgumentEntry> fetchLastRecords(TenantId tenantId, EntityId entityId, Argument argument) {
|
||||||
|
long startTs = Math.max(argument.getStartTs(), 0);
|
||||||
|
long timeWindow = argument.getTimeWindow() == 0 ? System.currentTimeMillis() : argument.getTimeWindow();
|
||||||
|
long endTs = startTs + timeWindow;
|
||||||
|
int limit = argument.getLimit() == 0 ? MAX_LAST_RECORDS_VALUE : argument.getLimit();
|
||||||
|
|
||||||
|
ReadTsKvQuery query = new BaseReadTsKvQuery(argument.getKey(), startTs, endTs, 0, limit, Aggregation.NONE);
|
||||||
|
ListenableFuture<List<TsKvEntry>> lastRecordsFuture = timeseriesService.findAll(tenantId, entityId, List.of(query));
|
||||||
|
|
||||||
|
return Futures.transform(lastRecordsFuture, lastRecords -> {
|
||||||
|
ArgumentEntry argumentEntry = new ArgumentEntry();
|
||||||
|
argumentEntry.setKvEntries(lastRecords);
|
||||||
|
return argumentEntry;
|
||||||
|
}, calculatedFieldExecutor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListenableFuture<ArgumentEntry> fetchKvEntry(TenantId tenantId, EntityId entityId, Argument argument) {
|
||||||
|
ListenableFuture<Optional<? extends KvEntry>> kvEntryFuture = switch (argument.getType()) {
|
||||||
case "ATTRIBUTES" -> Futures.transform(
|
case "ATTRIBUTES" -> Futures.transform(
|
||||||
attributesService.find(tenantId, entityId, argument.getScope(), argument.getKey()),
|
attributesService.find(tenantId, entityId, argument.getScope(), argument.getKey()),
|
||||||
result -> result.or(() -> Optional.of(
|
result -> result.or(() -> Optional.of(
|
||||||
@ -342,9 +391,16 @@ public class DefaultCalculatedFieldExecutionService extends AbstractPartitionBas
|
|||||||
result -> result.or(() -> Optional.of(
|
result -> result.or(() -> Optional.of(
|
||||||
new BasicTsKvEntry(System.currentTimeMillis(), createDefaultKvEntry(argument))
|
new BasicTsKvEntry(System.currentTimeMillis(), createDefaultKvEntry(argument))
|
||||||
)),
|
)),
|
||||||
MoreExecutors.directExecutor());
|
calculatedFieldExecutor);
|
||||||
default -> throw new IllegalArgumentException("Invalid argument type '" + argument.getType() + "'.");
|
default -> throw new IllegalArgumentException("Invalid argument type '" + argument.getType() + "'.");
|
||||||
};
|
};
|
||||||
|
return Futures.transform(kvEntryFuture, kvEntry -> {
|
||||||
|
ArgumentEntry argumentEntry = new ArgumentEntry();
|
||||||
|
if (kvEntry.isPresent()) {
|
||||||
|
argumentEntry.setKvEntry(kvEntry.orElse(null));
|
||||||
|
}
|
||||||
|
return argumentEntry;
|
||||||
|
}, calculatedFieldExecutor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private KvEntry createDefaultKvEntry(Argument argument) {
|
private KvEntry createDefaultKvEntry(Argument argument) {
|
||||||
@ -359,7 +415,7 @@ public class DefaultCalculatedFieldExecutionService extends AbstractPartitionBas
|
|||||||
return new StringDataEntry(key, defaultValue);
|
return new StringDataEntry(key, defaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateOrInitializeState(CalculatedField calculatedField, EntityId entityId, Map<String, KvEntry> argumentValues) {
|
private void updateOrInitializeState(CalculatedField calculatedField, EntityId entityId, Map<String, ArgumentEntry> argumentValues) {
|
||||||
CalculatedFieldCtxId ctxId = new CalculatedFieldCtxId(calculatedField.getUuidId(), entityId.getId());
|
CalculatedFieldCtxId ctxId = new CalculatedFieldCtxId(calculatedField.getUuidId(), entityId.getId());
|
||||||
CalculatedFieldCtx calculatedFieldCtx = states.computeIfAbsent(ctxId, ctx -> new CalculatedFieldCtx(ctxId, null));
|
CalculatedFieldCtx calculatedFieldCtx = states.computeIfAbsent(ctxId, ctx -> new CalculatedFieldCtx(ctxId, null));
|
||||||
|
|
||||||
@ -373,7 +429,12 @@ public class DefaultCalculatedFieldExecutionService extends AbstractPartitionBas
|
|||||||
states.put(ctxId, calculatedFieldCtx);
|
states.put(ctxId, calculatedFieldCtx);
|
||||||
rocksDBService.put(JacksonUtil.writeValueAsString(ctxId), JacksonUtil.writeValueAsString(calculatedFieldCtx));
|
rocksDBService.put(JacksonUtil.writeValueAsString(ctxId), JacksonUtil.writeValueAsString(calculatedFieldCtx));
|
||||||
|
|
||||||
ListenableFuture<CalculatedFieldResult> resultFuture = state.performCalculation(calculatedField.getTenantId(), calculatedField.getConfiguration(), tbelInvokeService);
|
CalculationContext ctx = CalculationContext.builder()
|
||||||
|
.tenantId(calculatedField.getTenantId())
|
||||||
|
.configuration(calculatedField.getConfiguration())
|
||||||
|
.tbelInvokeService(tbelInvokeService)
|
||||||
|
.build();
|
||||||
|
ListenableFuture<CalculatedFieldResult> resultFuture = state.performCalculation(ctx);
|
||||||
Futures.addCallback(resultFuture, new FutureCallback<>() {
|
Futures.addCallback(resultFuture, new FutureCallback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(CalculatedFieldResult result) {
|
public void onSuccess(CalculatedFieldResult result) {
|
||||||
@ -414,6 +475,7 @@ public class DefaultCalculatedFieldExecutionService extends AbstractPartitionBas
|
|||||||
return switch (calculatedFieldType) {
|
return switch (calculatedFieldType) {
|
||||||
case SIMPLE -> new SimpleCalculatedFieldState();
|
case SIMPLE -> new SimpleCalculatedFieldState();
|
||||||
case SCRIPT -> new ScriptCalculatedFieldState();
|
case SCRIPT -> new ScriptCalculatedFieldState();
|
||||||
|
case LAST_RECORDS -> new LastRecordsCalculatedFieldState();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2024 The Thingsboard Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.thingsboard.server.service.cf.ctx.state;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.thingsboard.server.common.data.kv.KvEntry;
|
||||||
|
import org.thingsboard.server.common.data.kv.TsKvEntry;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ArgumentEntry {
|
||||||
|
|
||||||
|
private KvEntry kvEntry;
|
||||||
|
private List<TsKvEntry> kvEntries;
|
||||||
|
|
||||||
|
}
|
||||||
@ -19,11 +19,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
|
|||||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import org.thingsboard.script.api.tbel.TbelInvokeService;
|
|
||||||
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
|
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.Argument;
|
||||||
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
|
|
||||||
import org.thingsboard.server.common.data.id.TenantId;
|
|
||||||
import org.thingsboard.server.common.data.kv.KvEntry;
|
import org.thingsboard.server.common.data.kv.KvEntry;
|
||||||
import org.thingsboard.server.service.cf.CalculatedFieldResult;
|
import org.thingsboard.server.service.cf.CalculatedFieldResult;
|
||||||
|
|
||||||
@ -36,7 +33,8 @@ import java.util.Map;
|
|||||||
)
|
)
|
||||||
@JsonSubTypes({
|
@JsonSubTypes({
|
||||||
@JsonSubTypes.Type(value = SimpleCalculatedFieldState.class, name = "SIMPLE"),
|
@JsonSubTypes.Type(value = SimpleCalculatedFieldState.class, name = "SIMPLE"),
|
||||||
@JsonSubTypes.Type(value = ScriptCalculatedFieldState.class, name = "SCRIPT")
|
@JsonSubTypes.Type(value = ScriptCalculatedFieldState.class, name = "SCRIPT"),
|
||||||
|
@JsonSubTypes.Type(value = LastRecordsCalculatedFieldState.class, name = "LAST_RECORDS")
|
||||||
})
|
})
|
||||||
public interface CalculatedFieldState {
|
public interface CalculatedFieldState {
|
||||||
|
|
||||||
@ -47,8 +45,8 @@ public interface CalculatedFieldState {
|
|||||||
return argumentValues.keySet().containsAll(arguments.keySet());
|
return argumentValues.keySet().containsAll(arguments.keySet());
|
||||||
}
|
}
|
||||||
|
|
||||||
void initState(Map<String, KvEntry> argumentValues);
|
void initState(Map<String, ArgumentEntry> argumentValues);
|
||||||
|
|
||||||
ListenableFuture<CalculatedFieldResult> performCalculation(TenantId tenantId, CalculatedFieldConfiguration calculatedFieldConfiguration, TbelInvokeService tbelInvokeService);
|
ListenableFuture<CalculatedFieldResult> performCalculation(CalculationContext ctx);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2024 The Thingsboard Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.thingsboard.server.service.cf.ctx.state;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.thingsboard.script.api.tbel.TbelInvokeService;
|
||||||
|
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
|
||||||
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
|
import org.thingsboard.server.common.data.kv.KvEntry;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
public class CalculationContext {
|
||||||
|
|
||||||
|
private TenantId tenantId;
|
||||||
|
private CalculatedFieldConfiguration configuration;
|
||||||
|
private TbelInvokeService tbelInvokeService;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2024 The Thingsboard Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.thingsboard.server.service.cf.ctx.state;
|
||||||
|
|
||||||
|
import com.google.common.util.concurrent.Futures;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import lombok.Data;
|
||||||
|
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.CalculatedFieldConfiguration;
|
||||||
|
import org.thingsboard.server.common.data.cf.configuration.Output;
|
||||||
|
import org.thingsboard.server.common.data.kv.TsKvEntry;
|
||||||
|
import org.thingsboard.server.service.cf.CalculatedFieldResult;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class LastRecordsCalculatedFieldState implements CalculatedFieldState {
|
||||||
|
|
||||||
|
private Map<String, List<TsKvEntry>> arguments;
|
||||||
|
|
||||||
|
public LastRecordsCalculatedFieldState() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CalculatedFieldType getType() {
|
||||||
|
return CalculatedFieldType.LAST_RECORDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initState(Map<String, ArgumentEntry> argumentValues) {
|
||||||
|
if (arguments == null) {
|
||||||
|
arguments = new HashMap<>();
|
||||||
|
}
|
||||||
|
argumentValues.forEach((key, argumentEntry) -> {
|
||||||
|
List<TsKvEntry> tsKvEntryList = arguments.computeIfAbsent(key, k -> new ArrayList<>());
|
||||||
|
tsKvEntryList.addAll(argumentEntry.getKvEntries());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<CalculatedFieldResult> performCalculation(CalculationContext ctx) {
|
||||||
|
CalculatedFieldConfiguration configuration = ctx.getConfiguration();
|
||||||
|
Map<String, Argument> configArguments = configuration.getArguments();
|
||||||
|
Output output = configuration.getOutput();
|
||||||
|
|
||||||
|
Map<String, Object> resultMap = new HashMap<>();
|
||||||
|
|
||||||
|
arguments.replaceAll((key, entries) -> {
|
||||||
|
int limit = configArguments.get(key).getLimit();
|
||||||
|
List<TsKvEntry> limitedEntries = entries.stream()
|
||||||
|
.sorted(Comparator.comparingLong(TsKvEntry::getTs).reversed())
|
||||||
|
.limit(limit)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
Map<Long, Object> valueWithTs = limitedEntries.stream()
|
||||||
|
.collect(Collectors.toMap(TsKvEntry::getTs, TsKvEntry::getValue));
|
||||||
|
resultMap.put(key, valueWithTs);
|
||||||
|
|
||||||
|
return limitedEntries;
|
||||||
|
});
|
||||||
|
|
||||||
|
CalculatedFieldResult calculatedFieldResult = new CalculatedFieldResult();
|
||||||
|
calculatedFieldResult.setType(output.getType());
|
||||||
|
calculatedFieldResult.setScope(output.getScope());
|
||||||
|
calculatedFieldResult.setResultMap(resultMap);
|
||||||
|
|
||||||
|
return Futures.immediateFuture(calculatedFieldResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -40,7 +40,7 @@ public class ScriptCalculatedFieldState implements CalculatedFieldState {
|
|||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private CalculatedFieldScriptEngine calculatedFieldScriptEngine;
|
private CalculatedFieldScriptEngine calculatedFieldScriptEngine;
|
||||||
|
|
||||||
private Map<String, KvEntry> arguments = new HashMap<>();
|
private Map<String, KvEntry> arguments;
|
||||||
|
|
||||||
public ScriptCalculatedFieldState() {
|
public ScriptCalculatedFieldState() {
|
||||||
}
|
}
|
||||||
@ -50,22 +50,27 @@ public class ScriptCalculatedFieldState implements CalculatedFieldState {
|
|||||||
return CalculatedFieldType.SCRIPT;
|
return CalculatedFieldType.SCRIPT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void initState(Map<String, KvEntry> argumentValues) {
|
|
||||||
if (arguments == null) {
|
|
||||||
this.arguments = new HashMap<>();
|
|
||||||
}
|
|
||||||
this.arguments.putAll(argumentValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ListenableFuture<CalculatedFieldResult> performCalculation(TenantId tenantId, CalculatedFieldConfiguration calculatedFieldConfiguration, TbelInvokeService tbelInvokeService) {
|
public void initState(Map<String, ArgumentEntry> argumentValues) {
|
||||||
|
if (arguments == null) {
|
||||||
|
arguments = new HashMap<>();
|
||||||
|
}
|
||||||
|
argumentValues.forEach((key, value) -> arguments.put(key, value.getKvEntry()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<CalculatedFieldResult> performCalculation(CalculationContext ctx) {
|
||||||
|
CalculatedFieldConfiguration calculatedFieldConfiguration = ctx.getConfiguration();
|
||||||
|
TbelInvokeService tbelInvokeService = ctx.getTbelInvokeService();
|
||||||
|
|
||||||
if (tbelInvokeService == null) {
|
if (tbelInvokeService == null) {
|
||||||
throw new IllegalArgumentException("TBEL script engine is disabled!");
|
throw new IllegalArgumentException("TBEL script engine is disabled!");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (calculatedFieldScriptEngine == null) {
|
if (calculatedFieldScriptEngine == null) {
|
||||||
initEngine(tenantId, calculatedFieldConfiguration, tbelInvokeService);
|
initEngine(ctx.getTenantId(), calculatedFieldConfiguration, tbelInvokeService);
|
||||||
}
|
}
|
||||||
|
|
||||||
ListenableFuture<Object> resultFuture = calculatedFieldScriptEngine.executeScriptAsync(arguments);
|
ListenableFuture<Object> resultFuture = calculatedFieldScriptEngine.executeScriptAsync(arguments);
|
||||||
|
|||||||
@ -20,12 +20,10 @@ import com.google.common.util.concurrent.ListenableFuture;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import net.objecthunter.exp4j.Expression;
|
import net.objecthunter.exp4j.Expression;
|
||||||
import net.objecthunter.exp4j.ExpressionBuilder;
|
import net.objecthunter.exp4j.ExpressionBuilder;
|
||||||
import org.thingsboard.script.api.tbel.TbelInvokeService;
|
|
||||||
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
|
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.Argument;
|
||||||
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
|
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.Output;
|
||||||
import org.thingsboard.server.common.data.id.TenantId;
|
|
||||||
import org.thingsboard.server.common.data.kv.KvEntry;
|
import org.thingsboard.server.common.data.kv.KvEntry;
|
||||||
import org.thingsboard.server.service.cf.CalculatedFieldResult;
|
import org.thingsboard.server.service.cf.CalculatedFieldResult;
|
||||||
|
|
||||||
@ -37,21 +35,26 @@ public class SimpleCalculatedFieldState implements CalculatedFieldState {
|
|||||||
|
|
||||||
private Map<String, KvEntry> arguments;
|
private Map<String, KvEntry> arguments;
|
||||||
|
|
||||||
|
public SimpleCalculatedFieldState() {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CalculatedFieldType getType() {
|
public CalculatedFieldType getType() {
|
||||||
return CalculatedFieldType.SIMPLE;
|
return CalculatedFieldType.SIMPLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initState(Map<String, KvEntry> argumentValues) {
|
public void initState(Map<String, ArgumentEntry> argumentValues) {
|
||||||
if (arguments == null) {
|
if (arguments == null) {
|
||||||
arguments = new HashMap<>();
|
arguments = new HashMap<>();
|
||||||
}
|
}
|
||||||
arguments.putAll(argumentValues);
|
argumentValues.forEach((key, value) -> arguments.put(key, value.getKvEntry()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ListenableFuture<CalculatedFieldResult> performCalculation(TenantId tenantId, CalculatedFieldConfiguration calculatedFieldConfiguration, TbelInvokeService tbelInvokeService) {
|
public ListenableFuture<CalculatedFieldResult> performCalculation(CalculationContext ctx) {
|
||||||
|
CalculatedFieldConfiguration calculatedFieldConfiguration = ctx.getConfiguration();
|
||||||
|
|
||||||
Output output = calculatedFieldConfiguration.getOutput();
|
Output output = calculatedFieldConfiguration.getOutput();
|
||||||
Map<String, Argument> arguments = calculatedFieldConfiguration.getArguments();
|
Map<String, Argument> arguments = calculatedFieldConfiguration.getArguments();
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,6 @@ package org.thingsboard.server.common.data.cf;
|
|||||||
|
|
||||||
public enum CalculatedFieldType {
|
public enum CalculatedFieldType {
|
||||||
|
|
||||||
SIMPLE, SCRIPT
|
SIMPLE, SCRIPT, LAST_RECORDS
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,6 +29,7 @@ public class Argument {
|
|||||||
private String defaultValue;
|
private String defaultValue;
|
||||||
|
|
||||||
private int limit;
|
private int limit;
|
||||||
|
private long startTs;
|
||||||
private long timeWindow;
|
private long timeWindow;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -101,6 +101,9 @@ public abstract class BaseCalculatedFieldConfiguration implements CalculatedFiel
|
|||||||
argumentNode.put("type", argument.getType());
|
argumentNode.put("type", argument.getType());
|
||||||
argumentNode.put("scope", String.valueOf(argument.getScope()));
|
argumentNode.put("scope", String.valueOf(argument.getScope()));
|
||||||
argumentNode.put("defaultValue", argument.getDefaultValue());
|
argumentNode.put("defaultValue", argument.getDefaultValue());
|
||||||
|
argumentNode.put("limit", String.valueOf(argument.getLimit()));
|
||||||
|
argumentNode.put("startTs", String.valueOf(argument.getStartTs()));
|
||||||
|
argumentNode.put("timeWindow", String.valueOf(argument.getTimeWindow()));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (expression != null) {
|
if (expression != null) {
|
||||||
@ -144,7 +147,18 @@ public abstract class BaseCalculatedFieldConfiguration implements CalculatedFiel
|
|||||||
if (scope != null && !scope.isNull() && !scope.asText().equals("null")) {
|
if (scope != null && !scope.isNull() && !scope.asText().equals("null")) {
|
||||||
argument.setScope(AttributeScope.valueOf(scope.asText()));
|
argument.setScope(AttributeScope.valueOf(scope.asText()));
|
||||||
}
|
}
|
||||||
argument.setDefaultValue(argumentNode.get("defaultValue").asText());
|
if (argumentNode.hasNonNull("defaultValue")) {
|
||||||
|
argument.setDefaultValue(argumentNode.get("defaultValue").asText());
|
||||||
|
}
|
||||||
|
if (argumentNode.hasNonNull("limit")) {
|
||||||
|
argument.setLimit(argumentNode.get("limit").asInt());
|
||||||
|
}
|
||||||
|
if (argumentNode.hasNonNull("startTs")) {
|
||||||
|
argument.setStartTs(argumentNode.get("startTs").asLong());
|
||||||
|
}
|
||||||
|
if (argumentNode.hasNonNull("timeWindow")) {
|
||||||
|
argument.setTimeWindow(argumentNode.get("timeWindow").asInt());
|
||||||
|
}
|
||||||
arguments.put(key, argument);
|
arguments.put(key, argument);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,7 +35,8 @@ import java.util.UUID;
|
|||||||
)
|
)
|
||||||
@JsonSubTypes({
|
@JsonSubTypes({
|
||||||
@JsonSubTypes.Type(value = SimpleCalculatedFieldConfiguration.class, name = "SIMPLE"),
|
@JsonSubTypes.Type(value = SimpleCalculatedFieldConfiguration.class, name = "SIMPLE"),
|
||||||
@JsonSubTypes.Type(value = ScriptCalculatedFieldConfiguration.class, name = "SCRIPT")
|
@JsonSubTypes.Type(value = ScriptCalculatedFieldConfiguration.class, name = "SCRIPT"),
|
||||||
|
@JsonSubTypes.Type(value = LastRecordsCalculatedFieldConfiguration.class, name = "LAST_RECORDS")
|
||||||
})
|
})
|
||||||
public interface CalculatedFieldConfiguration {
|
public interface CalculatedFieldConfiguration {
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2024 The Thingsboard Authors
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.thingsboard.server.common.data.cf.configuration;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.thingsboard.server.common.data.EntityType;
|
||||||
|
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class LastRecordsCalculatedFieldConfiguration extends BaseCalculatedFieldConfiguration implements CalculatedFieldConfiguration {
|
||||||
|
|
||||||
|
public LastRecordsCalculatedFieldConfiguration() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public LastRecordsCalculatedFieldConfiguration(JsonNode config, EntityType entityType, UUID entityId) {
|
||||||
|
super(config, entityType, entityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CalculatedFieldType getType() {
|
||||||
|
return CalculatedFieldType.LAST_RECORDS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -24,8 +24,9 @@ import lombok.Data;
|
|||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import org.thingsboard.server.common.data.EntityType;
|
import org.thingsboard.server.common.data.EntityType;
|
||||||
import org.thingsboard.server.common.data.cf.CalculatedField;
|
import org.thingsboard.server.common.data.cf.CalculatedField;
|
||||||
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
|
|
||||||
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
|
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
|
||||||
|
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
|
||||||
|
import org.thingsboard.server.common.data.cf.configuration.LastRecordsCalculatedFieldConfiguration;
|
||||||
import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration;
|
import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration;
|
||||||
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
|
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
|
||||||
import org.thingsboard.server.common.data.id.CalculatedFieldId;
|
import org.thingsboard.server.common.data.id.CalculatedFieldId;
|
||||||
@ -120,14 +121,11 @@ public class CalculatedFieldEntity extends BaseSqlEntity<CalculatedField> implem
|
|||||||
}
|
}
|
||||||
|
|
||||||
private CalculatedFieldConfiguration readCalculatedFieldConfiguration(JsonNode config, EntityType entityType, UUID entityId) {
|
private CalculatedFieldConfiguration readCalculatedFieldConfiguration(JsonNode config, EntityType entityType, UUID entityId) {
|
||||||
switch (CalculatedFieldType.valueOf(type)) {
|
return switch (CalculatedFieldType.valueOf(type)) {
|
||||||
case SIMPLE:
|
case SIMPLE -> new SimpleCalculatedFieldConfiguration(config, entityType, entityId);
|
||||||
return new SimpleCalculatedFieldConfiguration(config, entityType, entityId);
|
case SCRIPT -> new ScriptCalculatedFieldConfiguration(config, entityType, entityId);
|
||||||
case SCRIPT:
|
case LAST_RECORDS -> new LastRecordsCalculatedFieldConfiguration(config, entityType, entityId);
|
||||||
return new ScriptCalculatedFieldConfiguration(config, entityType, entityId);
|
};
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Unsupported calculated field type: " + type + "!");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
|
|||||||
import org.thingsboard.server.common.data.cf.CalculatedFieldLinkConfiguration;
|
import org.thingsboard.server.common.data.cf.CalculatedFieldLinkConfiguration;
|
||||||
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
|
import org.thingsboard.server.common.data.cf.CalculatedFieldType;
|
||||||
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
|
import org.thingsboard.server.common.data.cf.configuration.CalculatedFieldConfiguration;
|
||||||
|
import org.thingsboard.server.common.data.cf.configuration.LastRecordsCalculatedFieldConfiguration;
|
||||||
import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration;
|
import org.thingsboard.server.common.data.cf.configuration.ScriptCalculatedFieldConfiguration;
|
||||||
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
|
import org.thingsboard.server.common.data.cf.configuration.SimpleCalculatedFieldConfiguration;
|
||||||
import org.thingsboard.server.common.data.id.CalculatedFieldId;
|
import org.thingsboard.server.common.data.id.CalculatedFieldId;
|
||||||
@ -136,14 +137,11 @@ public class DefaultNativeCalculatedFieldRepository implements NativeCalculatedF
|
|||||||
}
|
}
|
||||||
|
|
||||||
private CalculatedFieldConfiguration readCalculatedFieldConfiguration(CalculatedFieldType type, JsonNode config, EntityType entityType, UUID entityId) {
|
private CalculatedFieldConfiguration readCalculatedFieldConfiguration(CalculatedFieldType type, JsonNode config, EntityType entityType, UUID entityId) {
|
||||||
switch (type) {
|
return switch (type) {
|
||||||
case SIMPLE:
|
case SIMPLE -> new SimpleCalculatedFieldConfiguration(config, entityType, entityId);
|
||||||
return new SimpleCalculatedFieldConfiguration(config, entityType, entityId);
|
case SCRIPT -> new ScriptCalculatedFieldConfiguration(config, entityType, entityId);
|
||||||
case SCRIPT:
|
case LAST_RECORDS -> new LastRecordsCalculatedFieldConfiguration(config, entityType, entityId);
|
||||||
return new ScriptCalculatedFieldConfiguration(config, entityType, entityId);
|
};
|
||||||
default:
|
|
||||||
throw new IllegalArgumentException("Unsupported calculated field type: " + type + "!");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user