From fb49273bd657536495684e89715059e50556c7d2 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Fri, 23 Sep 2022 18:06:33 +0300 Subject: [PATCH] Math node implementation --- .../DefaultTelemetrySubscriptionService.java | 54 +++++ .../dao/attributes/AttributesService.java | 2 + .../dao/timeseries/TimeseriesService.java | 3 + .../thingsboard/server/common/msg/TbMsg.java | 10 + .../dao/attributes/BaseAttributesService.java | 7 + .../attributes/CachedAttributesService.java | 22 +- .../dao/sqlts/SqlTimeseriesLatestDao.java | 53 ++-- .../AbstractCassandraBaseTimeseriesDao.java | 18 +- .../dao/timeseries/BaseTimeseriesService.java | 7 + .../CassandraBaseTimeseriesLatestDao.java | 11 +- .../dao/timeseries/TimeseriesLatestDao.java | 11 + .../api/RuleEngineTelemetryService.java | 11 + .../rule/engine/math/TbMathArgument.java | 35 +++ .../rule/engine/math/TbMathArgumentType.java | 22 ++ .../rule/engine/math/TbMathArgumentValue.java | 92 +++++++ .../math/TbMathFormulaConfiguration.java | 39 +++ .../rule/engine/math/TbMathNode.java | 229 ++++++++++++++++++ .../rule/engine/math/TbMathResult.java | 33 +++ .../math/TbRuleNodeMathFunctionType.java | 37 +++ 19 files changed, 663 insertions(+), 33 deletions(-) create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgument.java create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgumentType.java create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgumentValue.java create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathFormulaConfiguration.java create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathResult.java create mode 100644 rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbRuleNodeMathFunctionType.java diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java index 947754e405..a5f66968da 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java +++ b/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 saveAndNotify(TenantId tenantId, EntityId entityId, TsKvEntry ts) { + SettableFuture future = SettableFuture.create(); + saveAndNotify(tenantId, entityId, Collections.singletonList(ts), new VoidFutureCallback(future)); + return future; + } + @Override public void saveAndNotify(TenantId tenantId, EntityId entityId, List ts, FutureCallback callback) { saveAndNotify(tenantId, null, entityId, ts, 0L, callback); @@ -332,6 +340,34 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer , System.currentTimeMillis())), callback); } + @Override + public ListenableFuture saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, long value) { + SettableFuture future = SettableFuture.create(); + saveAttrAndNotify(tenantId, entityId, scope, key, value, new VoidFutureCallback(future)); + return future; + } + + @Override + public ListenableFuture saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, String value) { + SettableFuture future = SettableFuture.create(); + saveAttrAndNotify(tenantId, entityId, scope, key, value, new VoidFutureCallback(future)); + return future; + } + + @Override + public ListenableFuture saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, double value) { + SettableFuture future = SettableFuture.create(); + saveAttrAndNotify(tenantId, entityId, scope, key, value, new VoidFutureCallback(future)); + return future; + } + + @Override + public ListenableFuture saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, boolean value) { + SettableFuture future = SettableFuture.create(); + saveAttrAndNotify(tenantId, entityId, scope, key, value, new VoidFutureCallback(future)); + return future; + } + private void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List 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 { + private final SettableFuture future; + + public VoidFutureCallback(SettableFuture future) { + this.future = future; + } + + @Override + public void onSuccess(Void result) { + future.set(null); + } + + @Override + public void onFailure(Throwable t) { + future.setException(t); + } + } + } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java index 6497778676..5a5a78c2a2 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java @@ -39,6 +39,8 @@ public interface AttributesService { ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes); + ListenableFuture save(TenantId tenantId, EntityId entityId, String scope, AttributeKvEntry attribute); + ListenableFuture> removeAll(TenantId tenantId, EntityId entityId, String scope, List attributeKeys); List findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java index cf17eec88d..19d9cef81e 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java +++ b/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> findAll(TenantId tenantId, EntityId entityId, List queries); + ListenableFuture> findLatest(TenantId tenantId, EntityId entityId, String keys); + ListenableFuture> findLatest(TenantId tenantId, EntityId entityId, Collection keys); ListenableFuture> findAllLatest(TenantId tenantId, EntityId entityId); diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java index 55b9f4dbd5..6d756af9dd 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/TbMsg.java +++ b/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()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java index 97c263ad97..d9b7251f21 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/BaseAttributesService.java +++ b/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 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> save(TenantId tenantId, EntityId entityId, String scope, List attributes) { validate(entityId, scope); diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index 802e746122..18c35741ff 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/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 save(TenantId tenantId, EntityId entityId, String scope, AttributeKvEntry attribute) { + validate(entityId, scope); + AttributeUtils.validate(attribute); + ListenableFuture future = attributesDao.save(tenantId, entityId, scope, attribute); + return Futures.transform(future, key -> evict(entityId, scope, attribute, key), cacheExecutor); + } + @Override public ListenableFuture> save(TenantId tenantId, EntityId entityId, String scope, List attributes) { validate(entityId, scope); @@ -213,17 +221,19 @@ public class CachedAttributesService implements AttributesService { List> futures = new ArrayList<>(attributes.size()); for (var attribute : attributes) { ListenableFuture 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> removeAll(TenantId tenantId, EntityId entityId, String scope, List attributeKeys) { validate(entityId, scope); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java index 50975f21de..00933140fb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java +++ b/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> findLatestOpt(TenantId tenantId, EntityId entityId, String key) { + return Futures.immediateFuture(Optional.ofNullable(doFindLatest(entityId, key))); + } + @Override public ListenableFuture 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 getFindLatestFuture(EntityId entityId, String key) { + protected TsKvEntry doFindLatest(EntityId entityId, String key) { TsKvLatestCompositeKey compositeKey = new TsKvLatestCompositeKey( entityId.getId(), getOrSaveKeyId(key)); Optional 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 getRemoveLatestFuture(TenantId tenantId, EntityId entityId, DeleteTsKvQuery query) { - ListenableFuture latestFuture = getFindLatestFuture(entityId, query.getKey()); + TsKvEntry latest = doFindLatest(entityId, query.getKey()); - ListenableFuture 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 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 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()) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/AbstractCassandraBaseTimeseriesDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/AbstractCassandraBaseTimeseriesDao.java index be960f4af8..d193bbf62d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/AbstractCassandraBaseTimeseriesDao.java +++ b/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 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 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 foundKeyOpt = getKey(row); + long ts = row.getLong(ModelConstants.TS_COLUMN); + return new BasicTsKvEntry(ts, toKvEntry(row, foundKeyOpt.orElse(key))); + } + private Optional getKey(Row row){ try{ return Optional.ofNullable(row.getString(ModelConstants.KEY_COLUMN)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java index b4cb27cb53..c8a5076c65 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java +++ b/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> findLatest(TenantId tenantId, EntityId entityId, String key) { + validate(entityId); + return timeseriesLatestDao.findLatestOpt(tenantId, entityId, key); + } + @Override public ListenableFuture> findLatest(TenantId tenantId, EntityId entityId, Collection keys) { validate(entityId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java index 2b3d62712f..4fb5be7dea 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java +++ b/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> findLatestOpt(TenantId tenantId, EntityId entityId, String key) { + return findLatest(tenantId, entityId, key, rs -> convertResultToTsKvEntryOpt(key, rs.one())); + } + @Override public ListenableFuture findLatest(TenantId tenantId, EntityId entityId, String key) { + return findLatest(tenantId, entityId, key, rs -> convertResultToTsKvEntry(key, rs.one())); + } + + private ListenableFuture findLatest(TenantId tenantId, EntityId entityId, String key, java.util.function.Function 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 diff --git a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java b/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java index d24a229766..06459b9df3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java +++ b/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> 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 findLatest(TenantId tenantId, EntityId entityId, String key); ListenableFuture> findAllLatest(TenantId tenantId, EntityId entityId); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java index 7a316bb78e..dbee45c230 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineTelemetryService.java +++ b/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 saveAndNotify(TenantId tenantId, EntityId entityId, TsKvEntry ts); + void saveAndNotify(TenantId tenantId, EntityId entityId, List ts, FutureCallback callback); void saveAndNotify(TenantId tenantId, CustomerId id, EntityId entityId, List ts, long ttl, FutureCallback callback); @@ -43,6 +46,14 @@ public interface RuleEngineTelemetryService { void saveLatestAndNotify(TenantId tenantId, EntityId entityId, List ts, FutureCallback callback); + ListenableFuture saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, long value); + + ListenableFuture saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, String value); + + ListenableFuture saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, double value); + + ListenableFuture saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, boolean value); + void saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, long value, FutureCallback callback); void saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, String value, FutureCallback callback); diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgument.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgument.java new file mode 100644 index 0000000000..2ad5efbb11 --- /dev/null +++ b/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; + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgumentType.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgumentType.java new file mode 100644 index 0000000000..39640bb390 --- /dev/null +++ b/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; + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgumentValue.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathArgumentValue.java new file mode 100644 index 0000000000..5e28192945 --- /dev/null +++ b/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 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!"); + } + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathFormulaConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathFormulaConfiguration.java new file mode 100644 index 0000000000..4f1b4cbe60 --- /dev/null +++ b/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 { + + private TbRuleNodeMathFunctionType operation; + private List 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; + } +} \ No newline at end of file diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathNode.java new file mode 100644 index 0000000000..2a2b94afc8 --- /dev/null +++ b/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 msg property. For example 'temperature = ' + msg.temperature ;. " + + "Message metadata can be accessed via metadata property. For example 'name = ' + metadata.customerName;.", + icon = "functions" +) +public class TbMathNode implements TbNode { + + private static ConcurrentMap 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 msgBodyOpt = convertMsgBodyIfRequired(msg); + var argumentValues = Futures.allAsList(arguments.stream() + .map(arg -> resolveArguments(ctx, msg, msgBodyOpt, arg)).collect(Collectors.toList())); + ListenableFuture 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 updateMsgAndDb(TbContext ctx, TbMsg msg, Optional 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 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 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 convertMsgBodyIfRequired(TbMsg msg) { + Optional 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 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 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 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 function) { + return function.apply(arg.getValue()); + } + + private double apply(TbMathArgumentValue arg1, TbMathArgumentValue arg2, BiFunction function) { + return function.apply(arg1.getValue(), arg2.getValue()); + } + + private ListenableFuture resolveArguments(TbContext ctx, TbMsg msg, Optional 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() { + } +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathResult.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbMathResult.java new file mode 100644 index 0000000000..0c627f1e71 --- /dev/null +++ b/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; + +} diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbRuleNodeMathFunctionType.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/math/TbRuleNodeMathFunctionType.java new file mode 100644 index 0000000000..ba734c8662 --- /dev/null +++ b/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; + } +}