added implementation for script type
This commit is contained in:
parent
40299cab1a
commit
c75603f57c
@ -17,6 +17,7 @@ package org.thingsboard.server.service.cf;
|
|||||||
|
|
||||||
import org.thingsboard.server.common.data.id.CalculatedFieldId;
|
import org.thingsboard.server.common.data.id.CalculatedFieldId;
|
||||||
import org.thingsboard.server.common.data.id.TenantId;
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
|
import org.thingsboard.server.common.data.kv.KvEntry;
|
||||||
import org.thingsboard.server.common.msg.queue.TbCallback;
|
import org.thingsboard.server.common.msg.queue.TbCallback;
|
||||||
import org.thingsboard.server.gen.transport.TransportProtos;
|
import org.thingsboard.server.gen.transport.TransportProtos;
|
||||||
|
|
||||||
@ -26,8 +27,6 @@ public interface CalculatedFieldExecutionService {
|
|||||||
|
|
||||||
void onCalculatedFieldMsg(TransportProtos.CalculatedFieldMsgProto proto, TbCallback callback);
|
void onCalculatedFieldMsg(TransportProtos.CalculatedFieldMsgProto proto, TbCallback callback);
|
||||||
|
|
||||||
void onTelemetryUpdate(TenantId tenantId, CalculatedFieldId calculatedFieldId, Map<String, String> updatedTelemetry);
|
void onTelemetryUpdate(TenantId tenantId, CalculatedFieldId calculatedFieldId, Map<String, KvEntry> updatedTelemetry);
|
||||||
|
|
||||||
// void onEntityProfileUpdate(TransportProtos.CalculatedFieldEntityProfileUpdateMsgProto proto, TbCallback callback);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,7 +25,7 @@ public class CalculatedFieldResult {
|
|||||||
|
|
||||||
private String type;
|
private String type;
|
||||||
private AttributeScope scope;
|
private AttributeScope scope;
|
||||||
private Map<String, String> resultMap;
|
private Map<String, Object> resultMap;
|
||||||
|
|
||||||
public CalculatedFieldResult() {
|
public CalculatedFieldResult() {
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.thingsboard.server.service.cf;
|
package org.thingsboard.server.service.cf;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import com.google.common.util.concurrent.FutureCallback;
|
import com.google.common.util.concurrent.FutureCallback;
|
||||||
import com.google.common.util.concurrent.Futures;
|
import com.google.common.util.concurrent.Futures;
|
||||||
@ -30,6 +31,7 @@ import org.springframework.beans.factory.annotation.Value;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.thingsboard.common.util.JacksonUtil;
|
import org.thingsboard.common.util.JacksonUtil;
|
||||||
import org.thingsboard.common.util.ThingsBoardExecutors;
|
import org.thingsboard.common.util.ThingsBoardExecutors;
|
||||||
|
import org.thingsboard.script.api.tbel.TbelInvokeService;
|
||||||
import org.thingsboard.server.cluster.TbClusterService;
|
import org.thingsboard.server.cluster.TbClusterService;
|
||||||
import org.thingsboard.server.common.data.cf.CalculatedField;
|
import org.thingsboard.server.common.data.cf.CalculatedField;
|
||||||
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
|
import org.thingsboard.server.common.data.cf.CalculatedFieldLink;
|
||||||
@ -90,6 +92,7 @@ public class DefaultCalculatedFieldExecutionService extends AbstractPartitionBas
|
|||||||
private final TimeseriesService timeseriesService;
|
private final TimeseriesService timeseriesService;
|
||||||
private final RocksDBService rocksDBService;
|
private final RocksDBService rocksDBService;
|
||||||
private final TbClusterService clusterService;
|
private final TbClusterService clusterService;
|
||||||
|
private final TbelInvokeService tbelInvokeService;
|
||||||
|
|
||||||
private ListeningExecutorService calculatedFieldExecutor;
|
private ListeningExecutorService calculatedFieldExecutor;
|
||||||
private ListeningExecutorService calculatedFieldCallbackExecutor;
|
private ListeningExecutorService calculatedFieldCallbackExecutor;
|
||||||
@ -201,7 +204,7 @@ public class DefaultCalculatedFieldExecutionService extends AbstractPartitionBas
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTelemetryUpdate(TenantId tenantId, CalculatedFieldId calculatedFieldId, Map<String, String> 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);
|
updateOrInitializeState(calculatedField, calculatedField.getEntityId(), updatedTelemetry);
|
||||||
@ -266,13 +269,13 @@ 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, String> argumentValues = new HashMap<>();
|
Map<String, KvEntry> argumentValues = new HashMap<>();
|
||||||
AtomicInteger remaining = new AtomicInteger(arguments.size());
|
AtomicInteger remaining = new AtomicInteger(arguments.size());
|
||||||
arguments.forEach((key, argument) -> Futures.addCallback(fetchArgumentValue(tenantId, argument), new FutureCallback<>() {
|
arguments.forEach((key, argument) -> Futures.addCallback(fetchArgumentValue(tenantId, argument), new FutureCallback<>() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Optional<? extends KvEntry> result) {
|
public void onSuccess(Optional<? extends KvEntry> result) {
|
||||||
String value = result.map(KvEntry::getValueAsString).orElse(argument.getDefaultValue());
|
// todo: should be rewritten implementation for default value
|
||||||
argumentValues.put(key, value);
|
argumentValues.put(key, result.orElse(null));
|
||||||
if (remaining.decrementAndGet() == 0) {
|
if (remaining.decrementAndGet() == 0) {
|
||||||
updateOrInitializeState(calculatedField, entityId, argumentValues);
|
updateOrInitializeState(calculatedField, entityId, argumentValues);
|
||||||
}
|
}
|
||||||
@ -300,7 +303,7 @@ public class DefaultCalculatedFieldExecutionService extends AbstractPartitionBas
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateOrInitializeState(CalculatedField calculatedField, EntityId entityId, Map<String, String> argumentValues) {
|
private void updateOrInitializeState(CalculatedField calculatedField, EntityId entityId, Map<String, KvEntry> 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));
|
||||||
|
|
||||||
@ -314,19 +317,30 @@ 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));
|
||||||
|
|
||||||
CalculatedFieldResult result = state.performCalculation(calculatedField.getConfiguration());
|
ListenableFuture<CalculatedFieldResult> resultFuture = state.performCalculation(calculatedField.getTenantId(), calculatedField.getConfiguration(), tbelInvokeService);
|
||||||
|
Futures.addCallback(resultFuture, new FutureCallback<>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(CalculatedFieldResult result) {
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
pushMsgToRuleEngine(calculatedField.getTenantId(), calculatedField.getEntityId(), result);
|
pushMsgToRuleEngine(calculatedField.getTenantId(), calculatedField.getEntityId(), result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Throwable t) {
|
||||||
|
log.warn("[{}] Failed to perform calculation. entityId: [{}]", calculatedField.getId(), entityId, t);
|
||||||
|
}
|
||||||
|
}, MoreExecutors.directExecutor());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private void pushMsgToRuleEngine(TenantId tenantId, EntityId originatorId, CalculatedFieldResult calculatedFieldResult) {
|
private void pushMsgToRuleEngine(TenantId tenantId, EntityId originatorId, CalculatedFieldResult calculatedFieldResult) {
|
||||||
try {
|
try {
|
||||||
String type = calculatedFieldResult.getType();
|
String type = calculatedFieldResult.getType();
|
||||||
TbMsgType msgType = "ATTRIBUTES".equals(type) ? TbMsgType.POST_ATTRIBUTES_REQUEST : TbMsgType.POST_TELEMETRY_REQUEST;
|
TbMsgType msgType = "ATTRIBUTES".equals(type) ? TbMsgType.POST_ATTRIBUTES_REQUEST : TbMsgType.POST_TELEMETRY_REQUEST;
|
||||||
TbMsgMetaData md = "ATTRIBUTES".equals(type) ? new TbMsgMetaData(Map.of(SCOPE, calculatedFieldResult.getScope().name())) : TbMsgMetaData.EMPTY;
|
TbMsgMetaData md = "ATTRIBUTES".equals(type) ? new TbMsgMetaData(Map.of(SCOPE, calculatedFieldResult.getScope().name())) : TbMsgMetaData.EMPTY;
|
||||||
ObjectNode jsonNodes = createJsonPayload(calculatedFieldResult);
|
ObjectNode payload = createJsonPayload(calculatedFieldResult);
|
||||||
TbMsg msg = TbMsg.newMsg(msgType, originatorId, md, JacksonUtil.writeValueAsString(jsonNodes));
|
TbMsg msg = TbMsg.newMsg(msgType, originatorId, md, JacksonUtil.writeValueAsString(payload));
|
||||||
clusterService.pushMsgToRuleEngine(tenantId, originatorId, msg, null);
|
clusterService.pushMsgToRuleEngine(tenantId, originatorId, msg, null);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("[{}] Failed to push message to rule engine. CalculatedFieldResult: {}", originatorId, calculatedFieldResult, e);
|
log.warn("[{}] Failed to push message to rule engine. CalculatedFieldResult: {}", originatorId, calculatedFieldResult, e);
|
||||||
@ -334,9 +348,10 @@ public class DefaultCalculatedFieldExecutionService extends AbstractPartitionBas
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ObjectNode createJsonPayload(CalculatedFieldResult calculatedFieldResult) {
|
private ObjectNode createJsonPayload(CalculatedFieldResult calculatedFieldResult) {
|
||||||
ObjectNode jsonNodes = JacksonUtil.newObjectNode();
|
ObjectNode payload = JacksonUtil.newObjectNode();
|
||||||
calculatedFieldResult.getResultMap().forEach(jsonNodes::put);
|
Map<String, Object> resultMap = calculatedFieldResult.getResultMap();
|
||||||
return jsonNodes;
|
resultMap.forEach((k, v) -> payload.set(k, JacksonUtil.convertValue(v, JsonNode.class)));
|
||||||
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
private CalculatedFieldState createStateByType(CalculatedFieldType calculatedFieldType) {
|
private CalculatedFieldState createStateByType(CalculatedFieldType calculatedFieldType) {
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* 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.ListenableFuture;
|
||||||
|
import org.thingsboard.server.common.data.kv.KvEntry;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface CalculatedFieldScriptEngine {
|
||||||
|
|
||||||
|
ListenableFuture<Object> executeScriptAsync(Map<String, KvEntry> arguments);
|
||||||
|
|
||||||
|
void destroy();
|
||||||
|
|
||||||
|
}
|
||||||
@ -18,9 +18,13 @@ package org.thingsboard.server.service.cf.ctx.state;
|
|||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
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 org.thingsboard.script.api.tbel.TbelInvokeService;
|
||||||
|
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.CalculatedFieldType;
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
|
import org.thingsboard.server.common.data.kv.KvEntry;
|
||||||
import org.thingsboard.server.service.cf.CalculatedFieldResult;
|
import org.thingsboard.server.service.cf.CalculatedFieldResult;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -39,12 +43,12 @@ public interface CalculatedFieldState {
|
|||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
CalculatedFieldType getType();
|
CalculatedFieldType getType();
|
||||||
|
|
||||||
default boolean isValid(Map<String, String> argumentValues, Map<String, Argument> arguments) {
|
default boolean isValid(Map<String, KvEntry> argumentValues, Map<String, Argument> arguments) {
|
||||||
return argumentValues.keySet().containsAll(arguments.keySet());
|
return argumentValues.keySet().containsAll(arguments.keySet());
|
||||||
}
|
}
|
||||||
|
|
||||||
void initState(Map<String, String> argumentValues);
|
void initState(Map<String, KvEntry> argumentValues);
|
||||||
|
|
||||||
CalculatedFieldResult performCalculation(CalculatedFieldConfiguration calculatedFieldConfiguration);
|
ListenableFuture<CalculatedFieldResult> performCalculation(TenantId tenantId, CalculatedFieldConfiguration calculatedFieldConfiguration, TbelInvokeService tbelInvokeService);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* 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 com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.thingsboard.common.util.JacksonUtil;
|
||||||
|
import org.thingsboard.script.api.ScriptType;
|
||||||
|
import org.thingsboard.script.api.tbel.TbelInvokeService;
|
||||||
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
|
import org.thingsboard.server.common.data.kv.KvEntry;
|
||||||
|
|
||||||
|
import javax.script.ScriptException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class CalculatedFieldTbelScriptEngine implements CalculatedFieldScriptEngine {
|
||||||
|
|
||||||
|
private final TbelInvokeService tbelInvokeService;
|
||||||
|
|
||||||
|
private final UUID scriptId;
|
||||||
|
private final TenantId tenantId;
|
||||||
|
|
||||||
|
public CalculatedFieldTbelScriptEngine(TenantId tenantId, TbelInvokeService tbelInvokeService, String script, String... argNames) {
|
||||||
|
this.tenantId = tenantId;
|
||||||
|
this.tbelInvokeService = tbelInvokeService;
|
||||||
|
try {
|
||||||
|
this.scriptId = this.tbelInvokeService.eval(tenantId, ScriptType.CALCULATED_FIELD_SCRIPT, script, argNames).get();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Throwable t = e;
|
||||||
|
if (e instanceof ExecutionException) {
|
||||||
|
t = e.getCause();
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Can't compile script: " + t.getMessage(), t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListenableFuture<Object> executeScriptAsync(Map<String, KvEntry> arguments) {
|
||||||
|
log.trace("execute script async, arguments {}", arguments);
|
||||||
|
Object[] args = new Object[arguments.size()];
|
||||||
|
int index = 0;
|
||||||
|
for (KvEntry entry : arguments.values()) {
|
||||||
|
switch (entry.getDataType()) {
|
||||||
|
case BOOLEAN -> args[index] = entry.getBooleanValue().orElse(null);
|
||||||
|
case DOUBLE -> args[index] = entry.getDoubleValue().orElse(null);
|
||||||
|
case LONG -> args[index] = entry.getLongValue().orElse(null);
|
||||||
|
case JSON -> args[index] = entry.getJsonValue().map(JacksonUtil::toJsonNode).orElse(null);
|
||||||
|
default -> args[index] = entry.getValueAsString();
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
return Futures.transformAsync(tbelInvokeService.invokeScript(tenantId, null, this.scriptId, args),
|
||||||
|
o -> {
|
||||||
|
try {
|
||||||
|
return Futures.immediateFuture(o);
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (e.getCause() instanceof ScriptException) {
|
||||||
|
return Futures.immediateFailedFuture(e.getCause());
|
||||||
|
} else if (e.getCause() instanceof RuntimeException) {
|
||||||
|
return Futures.immediateFailedFuture(new ScriptException(e.getCause().getMessage()));
|
||||||
|
} else {
|
||||||
|
return Futures.immediateFailedFuture(new ScriptException(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, MoreExecutors.directExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() {
|
||||||
|
tbelInvokeService.release(this.scriptId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,10 +15,19 @@
|
|||||||
*/
|
*/
|
||||||
package org.thingsboard.server.service.cf.ctx.state;
|
package org.thingsboard.server.service.cf.ctx.state;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.google.common.util.concurrent.Futures;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.thingsboard.common.util.JacksonUtil;
|
||||||
|
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.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.id.TenantId;
|
||||||
|
import org.thingsboard.server.common.data.kv.KvEntry;
|
||||||
import org.thingsboard.server.service.cf.CalculatedFieldResult;
|
import org.thingsboard.server.service.cf.CalculatedFieldResult;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@ -28,7 +37,10 @@ import java.util.Map;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class ScriptCalculatedFieldState implements CalculatedFieldState {
|
public class ScriptCalculatedFieldState implements CalculatedFieldState {
|
||||||
|
|
||||||
private Map<String, String> arguments = new HashMap<>();
|
@JsonIgnore
|
||||||
|
private CalculatedFieldScriptEngine calculatedFieldScriptEngine;
|
||||||
|
|
||||||
|
private Map<String, KvEntry> arguments = new HashMap<>();
|
||||||
|
|
||||||
public ScriptCalculatedFieldState() {
|
public ScriptCalculatedFieldState() {
|
||||||
}
|
}
|
||||||
@ -39,7 +51,7 @@ public class ScriptCalculatedFieldState implements CalculatedFieldState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initState(Map<String, String> argumentValues) {
|
public void initState(Map<String, KvEntry> argumentValues) {
|
||||||
if (arguments == null) {
|
if (arguments == null) {
|
||||||
this.arguments = new HashMap<>();
|
this.arguments = new HashMap<>();
|
||||||
}
|
}
|
||||||
@ -47,9 +59,46 @@ public class ScriptCalculatedFieldState implements CalculatedFieldState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CalculatedFieldResult performCalculation(CalculatedFieldConfiguration calculatedFieldConfiguration) {
|
public ListenableFuture<CalculatedFieldResult> performCalculation(TenantId tenantId, CalculatedFieldConfiguration calculatedFieldConfiguration, TbelInvokeService tbelInvokeService) {
|
||||||
// TODO: implement
|
if (tbelInvokeService == null) {
|
||||||
return null;
|
throw new IllegalArgumentException("TBEL script engine is disabled!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calculatedFieldScriptEngine == null) {
|
||||||
|
initEngine(tenantId, calculatedFieldConfiguration, tbelInvokeService);
|
||||||
|
}
|
||||||
|
|
||||||
|
ListenableFuture<Object> resultFuture = calculatedFieldScriptEngine.executeScriptAsync(arguments);
|
||||||
|
|
||||||
|
return Futures.transform(resultFuture, result -> {
|
||||||
|
Output output = calculatedFieldConfiguration.getOutput();
|
||||||
|
Map<String, Object> resultMap = new HashMap<>();
|
||||||
|
|
||||||
|
if (result instanceof Map<?, ?>) {
|
||||||
|
Map<String, Object> map = JacksonUtil.convertValue(result, Map.class);
|
||||||
|
if (map != null) {
|
||||||
|
resultMap.putAll(map);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resultMap.put(output.getName(), JacksonUtil.convertValue(result, Object.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
CalculatedFieldResult calculatedFieldResult = new CalculatedFieldResult();
|
||||||
|
calculatedFieldResult.setType(output.getType());
|
||||||
|
calculatedFieldResult.setScope(output.getScope());
|
||||||
|
calculatedFieldResult.setResultMap(resultMap);
|
||||||
|
|
||||||
|
return calculatedFieldResult;
|
||||||
|
}, MoreExecutors.directExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initEngine(TenantId tenantId, CalculatedFieldConfiguration calculatedFieldConfiguration, TbelInvokeService tbelInvokeService) {
|
||||||
|
calculatedFieldScriptEngine = new CalculatedFieldTbelScriptEngine(
|
||||||
|
tenantId,
|
||||||
|
tbelInvokeService,
|
||||||
|
calculatedFieldConfiguration.getOutput().getExpression(),
|
||||||
|
arguments.keySet().toArray(new String[0])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,13 +15,18 @@
|
|||||||
*/
|
*/
|
||||||
package org.thingsboard.server.service.cf.ctx.state;
|
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 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.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.CalculatedFieldType;
|
|
||||||
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.service.cf.CalculatedFieldResult;
|
import org.thingsboard.server.service.cf.CalculatedFieldResult;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@ -30,8 +35,7 @@ import java.util.Map;
|
|||||||
@Data
|
@Data
|
||||||
public class SimpleCalculatedFieldState implements CalculatedFieldState {
|
public class SimpleCalculatedFieldState implements CalculatedFieldState {
|
||||||
|
|
||||||
// TODO: use value object(TsKv) instead of string
|
private Map<String, KvEntry> arguments;
|
||||||
private Map<String, String> arguments;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CalculatedFieldType getType() {
|
public CalculatedFieldType getType() {
|
||||||
@ -39,7 +43,7 @@ public class SimpleCalculatedFieldState implements CalculatedFieldState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initState(Map<String, String> argumentValues) {
|
public void initState(Map<String, KvEntry> argumentValues) {
|
||||||
if (arguments == null) {
|
if (arguments == null) {
|
||||||
arguments = new HashMap<>();
|
arguments = new HashMap<>();
|
||||||
}
|
}
|
||||||
@ -47,7 +51,7 @@ public class SimpleCalculatedFieldState implements CalculatedFieldState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CalculatedFieldResult performCalculation(CalculatedFieldConfiguration calculatedFieldConfiguration) {
|
public ListenableFuture<CalculatedFieldResult> performCalculation(TenantId tenantId, CalculatedFieldConfiguration calculatedFieldConfiguration, TbelInvokeService tbelInvokeService) {
|
||||||
Output output = calculatedFieldConfiguration.getOutput();
|
Output output = calculatedFieldConfiguration.getOutput();
|
||||||
Map<String, Argument> arguments = calculatedFieldConfiguration.getArguments();
|
Map<String, Argument> arguments = calculatedFieldConfiguration.getArguments();
|
||||||
|
|
||||||
@ -64,19 +68,17 @@ public class SimpleCalculatedFieldState implements CalculatedFieldState {
|
|||||||
customExpression.set(expr);
|
customExpression.set(expr);
|
||||||
}
|
}
|
||||||
Map<String, Double> variables = new HashMap<>();
|
Map<String, Double> variables = new HashMap<>();
|
||||||
this.arguments.forEach((k, v) -> variables.put(k, Double.parseDouble(v)));
|
this.arguments.forEach((k, v) -> variables.put(k, Double.parseDouble(v.getValueAsString())));
|
||||||
expr.setVariables(variables);
|
expr.setVariables(variables);
|
||||||
|
|
||||||
String expressionResult = String.valueOf(expr.evaluate());
|
double expressionResult = expr.evaluate();
|
||||||
|
|
||||||
result.setType(output.getType());
|
result.setType(output.getType());
|
||||||
result.setScope(output.getScope());
|
result.setScope(output.getScope());
|
||||||
result.setResultMap(Map.of(output.getName(), expressionResult));
|
result.setResultMap(Map.of(output.getName(), expressionResult));
|
||||||
return result;
|
return Futures.immediateFuture(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
// TODO: handle what happens when not valid
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,6 +69,7 @@ import java.util.Objects;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by ashvayka on 27.03.18.
|
* Created by ashvayka on 27.03.18.
|
||||||
@ -207,32 +208,28 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
|
|||||||
CalculatedFieldId calculatedFieldId = link.getCalculatedFieldId();
|
CalculatedFieldId calculatedFieldId = link.getCalculatedFieldId();
|
||||||
Map<String, String> attributes = link.getConfiguration().getAttributes();
|
Map<String, String> attributes = link.getConfiguration().getAttributes();
|
||||||
Map<String, String> timeSeries = link.getConfiguration().getTimeSeries();
|
Map<String, String> timeSeries = link.getConfiguration().getTimeSeries();
|
||||||
List<? extends KvEntry> filteredTelemetry = telemetry.stream()
|
Map<String, KvEntry> updatedTelemetry = telemetry.stream()
|
||||||
.filter(entry -> attributes.containsValue(entry.getKey()) || timeSeries.containsValue(entry.getKey()))
|
.filter(entry -> attributes.containsValue(entry.getKey()) || timeSeries.containsValue(entry.getKey()))
|
||||||
.toList();
|
.collect(Collectors.toMap(
|
||||||
|
entry -> {
|
||||||
|
if (entry instanceof AttributeKvEntry) {
|
||||||
Map<String, String> updatedTelemetry = new HashMap<>();
|
return attributes.entrySet().stream()
|
||||||
for (KvEntry telemetryEntry : filteredTelemetry) {
|
.filter(attr -> attr.getValue().equals(entry.getKey()))
|
||||||
String key = telemetryEntry.getKey();
|
.map(Map.Entry::getKey)
|
||||||
if (telemetryEntry instanceof AttributeKvEntry) {
|
.findFirst()
|
||||||
for (Map.Entry<String, String> attribute : attributes.entrySet()) {
|
.orElse(entry.getKey());
|
||||||
if (telemetryEntry.getKey().equals(attribute.getValue())) {
|
} else if (entry instanceof TsKvEntry) {
|
||||||
key = attribute.getKey();
|
return timeSeries.entrySet().stream()
|
||||||
break;
|
.filter(ts -> ts.getValue().equals(entry.getKey()))
|
||||||
}
|
.map(Map.Entry::getKey)
|
||||||
}
|
.findFirst()
|
||||||
}
|
.orElse(entry.getKey());
|
||||||
if (telemetryEntry instanceof TsKvEntry) {
|
|
||||||
for (Map.Entry<String, String> timeSeriesEntry : timeSeries.entrySet()) {
|
|
||||||
if (telemetryEntry.getKey().equals(timeSeriesEntry.getValue())) {
|
|
||||||
key = timeSeriesEntry.getKey();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
updatedTelemetry.put(key, telemetryEntry.getValueAsString());
|
|
||||||
}
|
}
|
||||||
|
return entry.getKey();
|
||||||
|
},
|
||||||
|
entry -> entry,
|
||||||
|
(v1, v2) -> v1
|
||||||
|
));
|
||||||
|
|
||||||
if (!updatedTelemetry.isEmpty()) {
|
if (!updatedTelemetry.isEmpty()) {
|
||||||
calculatedFieldExecutionService.onTelemetryUpdate(tenantId, calculatedFieldId, updatedTelemetry);
|
calculatedFieldExecutionService.onTelemetryUpdate(tenantId, calculatedFieldId, updatedTelemetry);
|
||||||
|
|||||||
@ -16,5 +16,5 @@
|
|||||||
package org.thingsboard.script.api;
|
package org.thingsboard.script.api;
|
||||||
|
|
||||||
public enum ScriptType {
|
public enum ScriptType {
|
||||||
RULE_NODE_SCRIPT
|
RULE_NODE_SCRIPT, CALCULATED_FIELD_SCRIPT
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user