Browse Source

Math node implementation

pull/7348/head
Andrii Shvaika 4 years ago
parent
commit
fb49273bd6
  1. 54
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
  2. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java
  3. 3
      common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java
  4. 10
      common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java
  5. 7
      dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java
  6. 22
      dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java
  7. 53
      dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java
  8. 18
      dao/src/main/java/org/thingsboard/server/dao/timeseries/AbstractCassandraBaseTimeseriesDao.java
  9. 7
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
  10. 11
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java
  11. 11
      dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java
  12. 11
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java
  13. 35
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgument.java
  14. 22
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgumentType.java
  15. 92
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgumentValue.java
  16. 39
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathFormulaConfiguration.java
  17. 229
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java
  18. 33
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathResult.java
  19. 37
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbRuleNodeMathFunctionType.java

54
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java

@ -19,6 +19,7 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@ -115,6 +116,13 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
super.shutdownExecutor();
}
@Override
public ListenableFuture<Void> saveAndNotify(TenantId tenantId, EntityId entityId, TsKvEntry ts) {
SettableFuture<Void> future = SettableFuture.create();
saveAndNotify(tenantId, entityId, Collections.singletonList(ts), new VoidFutureCallback(future));
return future;
}
@Override
public void saveAndNotify(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, FutureCallback<Void> callback) {
saveAndNotify(tenantId, null, entityId, ts, 0L, callback);
@ -332,6 +340,34 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
, System.currentTimeMillis())), callback);
}
@Override
public ListenableFuture<Void> saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, long value) {
SettableFuture<Void> future = SettableFuture.create();
saveAttrAndNotify(tenantId, entityId, scope, key, value, new VoidFutureCallback(future));
return future;
}
@Override
public ListenableFuture<Void> saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, String value) {
SettableFuture<Void> future = SettableFuture.create();
saveAttrAndNotify(tenantId, entityId, scope, key, value, new VoidFutureCallback(future));
return future;
}
@Override
public ListenableFuture<Void> saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, double value) {
SettableFuture<Void> future = SettableFuture.create();
saveAttrAndNotify(tenantId, entityId, scope, key, value, new VoidFutureCallback(future));
return future;
}
@Override
public ListenableFuture<Void> saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, boolean value) {
SettableFuture<Void> future = SettableFuture.create();
saveAttrAndNotify(tenantId, entityId, scope, key, value, new VoidFutureCallback(future));
return future;
}
private void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes, boolean notifyDevice) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId);
if (currentPartitions.contains(tpi)) {
@ -436,4 +472,22 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
}
}
private static class VoidFutureCallback implements FutureCallback<Void> {
private final SettableFuture<Void> future;
public VoidFutureCallback(SettableFuture<Void> future) {
this.future = future;
}
@Override
public void onSuccess(Void result) {
future.set(null);
}
@Override
public void onFailure(Throwable t) {
future.setException(t);
}
}
}

2
common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java

@ -39,6 +39,8 @@ public interface AttributesService {
ListenableFuture<List<String>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes);
ListenableFuture<String> save(TenantId tenantId, EntityId entityId, String scope, AttributeKvEntry attribute);
ListenableFuture<List<String>> removeAll(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys);
List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId);

3
common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java

@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
/**
* @author Andrew Shvayka
@ -37,6 +38,8 @@ public interface TimeseriesService {
ListenableFuture<List<TsKvEntry>> findAll(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries);
ListenableFuture<Optional<TsKvEntry>> findLatest(TenantId tenantId, EntityId entityId, String keys);
ListenableFuture<List<TsKvEntry>> findLatest(TenantId tenantId, EntityId entityId, Collection<String> keys);
ListenableFuture<List<TsKvEntry>> findAllLatest(TenantId tenantId, EntityId entityId);

10
common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java

@ -122,6 +122,16 @@ public final class TbMsg implements Serializable {
data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.callback);
}
public static TbMsg transformMsgData(TbMsg tbMsg, String data) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, tbMsg.metaData, tbMsg.dataType,
data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback());
}
public static TbMsg transformMsg(TbMsg tbMsg, TbMsgMetaData metadata) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, tbMsg.customerId, metadata.copy(), tbMsg.dataType,
tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback());
}
public static TbMsg transformMsg(TbMsg tbMsg, CustomerId customerId) {
return new TbMsg(tbMsg.queueName, tbMsg.id, tbMsg.ts, tbMsg.type, tbMsg.originator, customerId, tbMsg.metaData, tbMsg.dataType,
tbMsg.data, tbMsg.ruleChainId, tbMsg.ruleNodeId, tbMsg.ctx.copy(), tbMsg.getCallback());

7
dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java

@ -79,6 +79,13 @@ public class BaseAttributesService implements AttributesService {
return attributesDao.findAllKeysByEntityIds(tenantId, entityType, entityIds);
}
@Override
public ListenableFuture<String> save(TenantId tenantId, EntityId entityId, String scope, AttributeKvEntry attribute) {
validate(entityId, scope);
AttributeUtils.validate(attribute);
return attributesDao.save(tenantId, entityId, scope, attribute);
}
@Override
public ListenableFuture<List<String>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
validate(entityId, scope);

22
dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java

@ -205,6 +205,14 @@ public class CachedAttributesService implements AttributesService {
return attributesDao.findAllKeysByEntityIds(tenantId, entityType, entityIds);
}
@Override
public ListenableFuture<String> save(TenantId tenantId, EntityId entityId, String scope, AttributeKvEntry attribute) {
validate(entityId, scope);
AttributeUtils.validate(attribute);
ListenableFuture<String> future = attributesDao.save(tenantId, entityId, scope, attribute);
return Futures.transform(future, key -> evict(entityId, scope, attribute, key), cacheExecutor);
}
@Override
public ListenableFuture<List<String>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes) {
validate(entityId, scope);
@ -213,17 +221,19 @@ public class CachedAttributesService implements AttributesService {
List<ListenableFuture<String>> futures = new ArrayList<>(attributes.size());
for (var attribute : attributes) {
ListenableFuture<String> future = attributesDao.save(tenantId, entityId, scope, attribute);
futures.add(Futures.transform(future, key -> {
log.trace("[{}][{}][{}] Before cache evict: {}", entityId, scope, key, attribute);
cache.evictOrPut(new AttributeCacheKey(scope, entityId, key), attribute);
log.trace("[{}][{}][{}] after cache evict.", entityId, scope, key);
return key;
}, cacheExecutor));
futures.add(Futures.transform(future, key -> evict(entityId, scope, attribute, key), cacheExecutor));
}
return Futures.allAsList(futures);
}
private String evict(EntityId entityId, String scope, AttributeKvEntry attribute, String key) {
log.trace("[{}][{}][{}] Before cache evict: {}", entityId, scope, key, attribute);
cache.evictOrPut(new AttributeCacheKey(scope, entityId, key), attribute);
log.trace("[{}][{}][{}] after cache evict.", entityId, scope, key);
return key;
}
@Override
public ListenableFuture<List<String>> removeAll(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys) {
validate(entityId, scope);

53
dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java

@ -149,9 +149,18 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
return getRemoveLatestFuture(tenantId, entityId, query);
}
@Override
public ListenableFuture<Optional<TsKvEntry>> findLatestOpt(TenantId tenantId, EntityId entityId, String key) {
return Futures.immediateFuture(Optional.ofNullable(doFindLatest(entityId, key)));
}
@Override
public ListenableFuture<TsKvEntry> findLatest(TenantId tenantId, EntityId entityId, String key) {
return getFindLatestFuture(entityId, key);
TsKvEntry latest = doFindLatest(entityId, key);
if (latest == null) {
latest = new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(key, null));
}
return Futures.immediateFuture(latest);
}
@Override
@ -195,43 +204,41 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
ReadTsKvQueryResult::getData, MoreExecutors.directExecutor());
}
protected ListenableFuture<TsKvEntry> getFindLatestFuture(EntityId entityId, String key) {
protected TsKvEntry doFindLatest(EntityId entityId, String key) {
TsKvLatestCompositeKey compositeKey =
new TsKvLatestCompositeKey(
entityId.getId(),
getOrSaveKeyId(key));
Optional<TsKvLatestEntity> entry = tsKvLatestRepository.findById(compositeKey);
TsKvEntry result;
if (entry.isPresent()) {
TsKvLatestEntity tsKvLatestEntity = entry.get();
tsKvLatestEntity.setStrKey(key);
result = DaoUtil.getData(tsKvLatestEntity);
return DaoUtil.getData(tsKvLatestEntity);
} else {
result = new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(key, null));
return null;
}
return Futures.immediateFuture(result);
}
protected ListenableFuture<TsKvLatestRemovingResult> getRemoveLatestFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) {
ListenableFuture<TsKvEntry> latestFuture = getFindLatestFuture(entityId, query.getKey());
TsKvEntry latest = doFindLatest(entityId, query.getKey());
ListenableFuture<Boolean> booleanFuture = Futures.transform(latestFuture, tsKvEntry -> {
long ts = tsKvEntry.getTs();
return ts > query.getStartTs() && ts <= query.getEndTs();
}, service);
if (latest == null) {
return Futures.immediateFuture(new TsKvLatestRemovingResult(query.getKey(), false));
}
ListenableFuture<Boolean> removedLatestFuture = Futures.transformAsync(booleanFuture, isRemove -> {
if (isRemove) {
TsKvLatestEntity latestEntity = new TsKvLatestEntity();
latestEntity.setEntityId(entityId.getId());
latestEntity.setKey(getOrSaveKeyId(query.getKey()));
return service.submit(() -> {
tsKvLatestRepository.delete(latestEntity);
return true;
});
}
return Futures.immediateFuture(false);
}, service);
long ts = latest.getTs();
ListenableFuture<Boolean> removedLatestFuture;
if (ts > query.getStartTs() && ts <= query.getEndTs()) {
TsKvLatestEntity latestEntity = new TsKvLatestEntity();
latestEntity.setEntityId(entityId.getId());
latestEntity.setKey(getOrSaveKeyId(query.getKey()));
removedLatestFuture = service.submit(() -> {
tsKvLatestRepository.delete(latestEntity);
return true;
});
} else {
removedLatestFuture = Futures.immediateFuture(false);
}
return Futures.transformAsync(removedLatestFuture, isRemoved -> {
if (isRemoved && query.getRewriteLatestIfDeleted()) {

18
dao/src/main/java/org/thingsboard/server/dao/timeseries/AbstractCassandraBaseTimeseriesDao.java

@ -88,14 +88,26 @@ public abstract class AbstractCassandraBaseTimeseriesDao extends CassandraAbstra
protected TsKvEntry convertResultToTsKvEntry(String key, Row row) {
if (row != null) {
Optional<String> foundKeyOpt = getKey(row);
long ts = row.getLong(ModelConstants.TS_COLUMN);
return new BasicTsKvEntry(ts, toKvEntry(row, foundKeyOpt.orElse(key)));
return getBasicTsKvEntry(key, row);
} else {
return new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(key, null));
}
}
protected Optional<TsKvEntry> convertResultToTsKvEntryOpt(String key, Row row) {
if (row != null) {
return Optional.of(getBasicTsKvEntry(key, row));
} else {
return Optional.empty();
}
}
private BasicTsKvEntry getBasicTsKvEntry(String key, Row row) {
Optional<String> foundKeyOpt = getKey(row);
long ts = row.getLong(ModelConstants.TS_COLUMN);
return new BasicTsKvEntry(ts, toKvEntry(row, foundKeyOpt.orElse(key)));
}
private Optional<String> getKey(Row row){
try{
return Optional.ofNullable(row.getString(ModelConstants.KEY_COLUMN));

7
dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java

@ -47,6 +47,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.StringUtils.isBlank;
@ -117,6 +118,12 @@ public class BaseTimeseriesService implements TimeseriesService {
}, MoreExecutors.directExecutor());
}
@Override
public ListenableFuture<Optional<TsKvEntry>> findLatest(TenantId tenantId, EntityId entityId, String key) {
validate(entityId);
return timeseriesLatestDao.findLatestOpt(tenantId, entityId, key);
}
@Override
public ListenableFuture<List<TsKvEntry>> findLatest(TenantId tenantId, EntityId entityId, Collection<String> keys) {
validate(entityId);

11
dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java

@ -59,15 +59,24 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes
private PreparedStatement findLatestStmt;
private PreparedStatement findAllLatestStmt;
@Override
public ListenableFuture<Optional<TsKvEntry>> findLatestOpt(TenantId tenantId, EntityId entityId, String key) {
return findLatest(tenantId, entityId, key, rs -> convertResultToTsKvEntryOpt(key, rs.one()));
}
@Override
public ListenableFuture<TsKvEntry> findLatest(TenantId tenantId, EntityId entityId, String key) {
return findLatest(tenantId, entityId, key, rs -> convertResultToTsKvEntry(key, rs.one()));
}
private <T> ListenableFuture<T> findLatest(TenantId tenantId, EntityId entityId, String key, java.util.function.Function<TbResultSet, T> function) {
BoundStatementBuilder stmtBuilder = new BoundStatementBuilder(getFindLatestStmt().bind());
stmtBuilder.setString(0, entityId.getEntityType().name());
stmtBuilder.setUuid(1, entityId.getId());
stmtBuilder.setString(2, key);
BoundStatement stmt = stmtBuilder.build();
log.debug(GENERATED_QUERY_FOR_ENTITY_TYPE_AND_ENTITY_ID, stmt, entityId.getEntityType(), entityId.getId());
return getFuture(executeAsyncRead(tenantId, stmt), rs -> convertResultToTsKvEntry(key, rs.one()));
return getFuture(executeAsyncRead(tenantId, stmt), function);
}
@Override

11
dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java

@ -24,9 +24,20 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult;
import java.util.List;
import java.util.Optional;
public interface TimeseriesLatestDao {
/**
* Optional TsKvEntry if the value is present in the DB
*
*/
ListenableFuture<Optional<TsKvEntry>> findLatestOpt(TenantId tenantId, EntityId entityId, String key);
/**
* Returns new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(key, null)) if the value is NOT present in the DB
*
*/
ListenableFuture<TsKvEntry> findLatest(TenantId tenantId, EntityId entityId, String key);
ListenableFuture<List<TsKvEntry>> findAllLatest(TenantId tenantId, EntityId entityId);

11
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java

@ -16,6 +16,7 @@
package org.thingsboard.rule.engine.api;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
@ -31,6 +32,8 @@ import java.util.List;
*/
public interface RuleEngineTelemetryService {
ListenableFuture<Void> saveAndNotify(TenantId tenantId, EntityId entityId, TsKvEntry ts);
void saveAndNotify(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, FutureCallback<Void> callback);
void saveAndNotify(TenantId tenantId, CustomerId id, EntityId entityId, List<TsKvEntry> ts, long ttl, FutureCallback<Void> callback);
@ -43,6 +46,14 @@ public interface RuleEngineTelemetryService {
void saveLatestAndNotify(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, FutureCallback<Void> callback);
ListenableFuture<Void> saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, long value);
ListenableFuture<Void> saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, String value);
ListenableFuture<Void> saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, double value);
ListenableFuture<Void> saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, boolean value);
void saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, long value, FutureCallback<Void> callback);
void saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, String value, FutureCallback<Void> callback);

35
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgument.java

@ -0,0 +1,35 @@
/**
* Copyright © 2016-2022 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.rule.engine.math;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TbMathArgument {
private TbMathArgumentType type;
private String value;
private String attributeScope;
public TbMathArgument(TbMathArgumentType type, String value) {
this.type = type;
this.value = value;
}
}

22
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgumentType.java

@ -0,0 +1,22 @@
/**
* Copyright © 2016-2022 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.rule.engine.math;
public enum TbMathArgumentType {
ATTRIBUTE, TIME_SERIES, MESSAGE_BODY, MESSAGE_METADATA, CONSTANT;
}

92
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgumentValue.java

@ -0,0 +1,92 @@
/**
* Copyright © 2016-2022 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.rule.engine.math;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Getter;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.util.Optional;
public class TbMathArgumentValue {
@Getter
private final double value;
private TbMathArgumentValue(double value) {
this.value = value;
}
public static TbMathArgumentValue constant(TbMathArgument arg) {
return fromString(arg.getValue());
}
public static TbMathArgumentValue fromMessageBody(String key, Optional<ObjectNode> jsonNodeOpt) {
if (jsonNodeOpt.isEmpty()) {
throw new RuntimeException("Message body is empty!");
}
var json = jsonNodeOpt.get();
if (!json.has(key)) {
throw new RuntimeException("Message body has no '" + key + "'!");
}
JsonNode valueNode = json.get(key);
if (valueNode.isEmpty() || valueNode.isNull()) {
throw new RuntimeException("Message body has empty or null '" + key + "'!");
}
double value;
if (valueNode.isNumber()) {
value = valueNode.doubleValue();
} else if (valueNode.isTextual()) {
try {
value = Double.parseDouble(valueNode.asText());
} catch (NumberFormatException ne) {
throw new RuntimeException("Can't convert value '" + valueNode.asText() + "' to double!");
}
} else {
throw new RuntimeException("Can't convert value '" + valueNode.toString() + "' to double!");
}
return new TbMathArgumentValue(value);
}
public static TbMathArgumentValue fromMessageMetadata(String key, TbMsgMetaData metaData) {
if (metaData == null) {
throw new RuntimeException("Message metadata is empty!");
}
var value = metaData.getValue(key);
if (StringUtils.isEmpty(value)) {
throw new RuntimeException("Message metadata has no '" + key + "'!");
}
return fromString(value);
}
public static TbMathArgumentValue fromLong(long value) {
return new TbMathArgumentValue(value);
}
public static TbMathArgumentValue fromDouble(double value) {
return new TbMathArgumentValue(value);
}
public static TbMathArgumentValue fromString(String value) {
try {
return new TbMathArgumentValue(Double.parseDouble(value));
} catch (NumberFormatException ne) {
throw new RuntimeException("Can't convert value '" + value + "' to double!");
}
}
}

39
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathFormulaConfiguration.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2022 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.rule.engine.math;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import java.util.Arrays;
import java.util.List;
@Data
public class TbMathFormulaConfiguration implements NodeConfiguration<TbMathFormulaConfiguration> {
private TbRuleNodeMathFunctionType operation;
private List<TbMathArgument> arguments;
private TbMathResult result;
@Override
public TbMathFormulaConfiguration defaultConfiguration() {
TbMathFormulaConfiguration configuration = new TbMathFormulaConfiguration();
configuration.setOperation(TbRuleNodeMathFunctionType.ADD);
configuration.setArguments(Arrays.asList(new TbMathArgument(TbMathArgumentType.CONSTANT, "2"), new TbMathArgument(TbMathArgumentType.CONSTANT, "2")));
configuration.setResult(new TbMathResult(TbMathArgumentType.MESSAGE_BODY, "result", false, false, null));
return configuration;
}
}

229
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java

@ -0,0 +1,229 @@
/**
* Copyright © 2016-2022 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.rule.engine.math;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.thingsboard.common.util.DonAsynchron;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
@SuppressWarnings("UnstableApiUsage")
@Slf4j
@RuleNode(
type = ComponentType.ACTION,
name = "math formula",
configClazz = TbMathFormulaConfiguration.class,
nodeDescription = "Calculates the mathematics formula based on message and/or database values",
nodeDetails = "Transform incoming Message with configured JS function to String and log final value into Thingsboard log file. " +
"Message payload can be accessed via <code>msg</code> property. For example <code>'temperature = ' + msg.temperature ;</code>. " +
"Message metadata can be accessed via <code>metadata</code> property. For example <code>'name = ' + metadata.customerName;</code>.",
icon = "functions"
)
public class TbMathNode implements TbNode {
private static ConcurrentMap<EntityId, Semaphore> semaphores = new ConcurrentReferenceHashMap<>();
private TbMathFormulaConfiguration config;
private boolean msgBodyToJsonConversionRequired;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbMathFormulaConfiguration.class);
var operation = config.getOperation();
var argsCount = config.getArguments().size();
if (argsCount < operation.getMinArgs() || argsCount > operation.getMaxArgs()) {
throw new RuntimeException("Args count: " + argsCount + " does not match operation: " + operation.name());
}
msgBodyToJsonConversionRequired = config.getArguments().stream().anyMatch(arg -> TbMathArgumentType.MESSAGE_BODY.equals(arg.getType()));
msgBodyToJsonConversionRequired = msgBodyToJsonConversionRequired || TbMathArgumentType.MESSAGE_BODY.equals(config.getResult().getType());
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
var originator = msg.getOriginator();
var originatorSemaphore = semaphores.computeIfAbsent(originator, tmp -> new Semaphore(1, true));
var arguments = config.getArguments();
Optional<ObjectNode> msgBodyOpt = convertMsgBodyIfRequired(msg);
var argumentValues = Futures.allAsList(arguments.stream()
.map(arg -> resolveArguments(ctx, msg, msgBodyOpt, arg)).collect(Collectors.toList()));
ListenableFuture<TbMsg> resultMsgFuture = Futures.transformAsync(argumentValues, args ->
updateMsgAndDb(ctx, msg, msgBodyOpt, calculateResult(ctx, msg, args)), ctx.getDbCallbackExecutor());
DonAsynchron.withCallback(resultMsgFuture, ctx::tellSuccess, t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
}
private ListenableFuture<TbMsg> updateMsgAndDb(TbContext ctx, TbMsg msg, Optional<ObjectNode> msgBodyOpt, double result) {
TbMathResult mathResultDef = config.getResult();
switch (mathResultDef.getType()) {
case MESSAGE_BODY:
return Futures.immediateFuture(addToBody(msg, mathResultDef, msgBodyOpt, result));
case MESSAGE_METADATA:
return Futures.immediateFuture(addToMeta(msg, mathResultDef, result));
case ATTRIBUTE:
ListenableFuture<Void> attrSave = ctx.getTelemetryService().saveAttrAndNotify(
ctx.getTenantId(), msg.getOriginator(), getAttributeScope(mathResultDef.getAttributeScope()), mathResultDef.getValue(), result);
return Futures.transform(attrSave, attr -> addToBodyAndMeta(msg, msgBodyOpt, result, mathResultDef), ctx.getDbCallbackExecutor());
case TIME_SERIES:
ListenableFuture<Void> tsSave = ctx.getTelemetryService().saveAndNotify(ctx.getTenantId(), msg.getOriginator(),
new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry(mathResultDef.getValue(), result)));
return Futures.transform(tsSave, ts -> addToBodyAndMeta(msg, msgBodyOpt, result, mathResultDef), ctx.getDbCallbackExecutor());
default:
throw new RuntimeException("Result type is not supported: " + mathResultDef.getType() + "!");
}
}
private Optional<ObjectNode> convertMsgBodyIfRequired(TbMsg msg) {
Optional<ObjectNode> msgBodyOpt;
if (msgBodyToJsonConversionRequired) {
var jsonNode = JacksonUtil.toJsonNode(msg.getData());
if (jsonNode.isObject()) {
msgBodyOpt = Optional.of((ObjectNode) jsonNode);
} else {
throw new RuntimeException("Message body is not a JSON object!");
}
} else {
msgBodyOpt = Optional.empty();
}
return msgBodyOpt;
}
private TbMsg addToBodyAndMeta(TbMsg msg, Optional<ObjectNode> msgBodyOpt, double result, TbMathResult mathResultDef) {
TbMsg tmpMsg = msg;
if (mathResultDef.isAddToBody()) {
tmpMsg = addToBody(msg, mathResultDef, msgBodyOpt, result);
}
if (mathResultDef.isAddToMetadata()) {
tmpMsg = addToMeta(msg, mathResultDef, result);
}
return tmpMsg;
}
private TbMsg addToBody(TbMsg msg, TbMathResult mathResultDef, Optional<ObjectNode> msgBodyOpt, double result) {
ObjectNode body = msgBodyOpt.get();
body.put(mathResultDef.getValue(), result);
return TbMsg.transformMsgData(msg, JacksonUtil.toString(body));
}
private TbMsg addToMeta(TbMsg msg, TbMathResult mathResultDef, double result) {
var md = msg.getMetaData();
md.putValue(mathResultDef.getValue(), Double.toString(result));
return TbMsg.transformMsg(msg, md);
}
private double calculateResult(TbContext ctx, TbMsg msg, List<TbMathArgumentValue> args) {
switch (config.getOperation()) {
case ADD:
return apply(args.get(0), args.get(1), Double::sum);
case SUB:
return apply(args.get(0), args.get(1), (a, b) -> a - b);
case MULT:
return apply(args.get(0), args.get(1), (a, b) -> a * b);
case DIV:
return apply(args.get(0), args.get(1), (a, b) -> a / b);
case SIN:
return apply(args.get(0), Math::sin);
case COS:
return apply(args.get(0), Math::cos);
case SQRT:
return apply(args.get(0), Math::sqrt);
case ABS:
return apply(args.get(0), Math::abs);
default:
throw new RuntimeException("Not supported operation: " + config.getOperation());
}
}
private double apply(TbMathArgumentValue arg, Function<Double, Double> function) {
return function.apply(arg.getValue());
}
private double apply(TbMathArgumentValue arg1, TbMathArgumentValue arg2, BiFunction<Double, Double, Double> function) {
return function.apply(arg1.getValue(), arg2.getValue());
}
private ListenableFuture<TbMathArgumentValue> resolveArguments(TbContext ctx, TbMsg msg, Optional<ObjectNode> msgBodyOpt, TbMathArgument arg) {
switch (arg.getType()) {
case CONSTANT:
return Futures.immediateFuture(TbMathArgumentValue.constant(arg));
case MESSAGE_BODY:
return Futures.immediateFuture(TbMathArgumentValue.fromMessageBody(arg.getValue(), msgBodyOpt));
case MESSAGE_METADATA:
return Futures.immediateFuture(TbMathArgumentValue.fromMessageMetadata(arg.getValue(), msg.getMetaData()));
case ATTRIBUTE:
String scope = getAttributeScope(arg.getAttributeScope());
return Futures.transform(ctx.getAttributesService().find(ctx.getTenantId(), msg.getOriginator(), scope, arg.getValue()),
opt -> getTbMathArgumentValue(opt.orElseThrow(() ->
new RuntimeException("Attribute: " + arg.getValue() + " with scope: " + scope + " not found for entity: " + msg.getOriginator())))
, MoreExecutors.directExecutor());
case TIME_SERIES:
return Futures.transform(ctx.getTimeseriesService().findLatest(ctx.getTenantId(), msg.getOriginator(), arg.getValue()),
opt -> getTbMathArgumentValue(opt.orElseThrow(() ->
new RuntimeException("Time-series: " + arg.getValue() + " not found for entity: " + msg.getOriginator())))
, MoreExecutors.directExecutor());
default:
throw new RuntimeException("Unsupported argument type: " + arg.getType() + "!");
}
}
private String getAttributeScope(String attrScope) {
return StringUtils.isEmpty(attrScope) ? DataConstants.SERVER_SCOPE : attrScope;
}
private TbMathArgumentValue getTbMathArgumentValue(KvEntry kv) {
switch (kv.getDataType()) {
case LONG:
return TbMathArgumentValue.fromLong(kv.getLongValue().get());
case DOUBLE:
return TbMathArgumentValue.fromDouble(kv.getDoubleValue().get());
default:
return TbMathArgumentValue.fromString(kv.getValueAsString());
}
}
@Override
public void destroy() {
}
}

33
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathResult.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2022 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.rule.engine.math;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TbMathResult {
private TbMathArgumentType type;
private String value;
private boolean addToBody;
private boolean addToMetadata;
private String attributeScope;
}

37
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbRuleNodeMathFunctionType.java

@ -0,0 +1,37 @@
/**
* Copyright © 2016-2022 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.rule.engine.math;
import lombok.Getter;
public enum TbRuleNodeMathFunctionType {
ADD(2), SUB(2), MULT(2), DIV(2), SIN(1), COS(1), SQRT(1), ABS(1);
@Getter
private final int minArgs;
@Getter
private final int maxArgs;
TbRuleNodeMathFunctionType(int args) {
this(args, args);
}
TbRuleNodeMathFunctionType(int minArgs, int maxArgs) {
this.minArgs = minArgs;
this.maxArgs = maxArgs;
}
}
Loading…
Cancel
Save