Merge branch 'master' into develop/2.3
This commit is contained in:
commit
4893200a1e
@ -264,6 +264,10 @@
|
||||
<groupId>org.javadelight</groupId>
|
||||
<artifactId>delight-nashorn-sandbox</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.springfox.ui</groupId>
|
||||
<artifactId>springfox-swagger-ui-rfc6570</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@ -14,13 +14,6 @@
|
||||
-- limitations under the License.
|
||||
--
|
||||
|
||||
DROP MATERIALIZED VIEW IF EXISTS thingsboard.entity_view_by_tenant_and_name;
|
||||
DROP MATERIALIZED VIEW IF EXISTS thingsboard.entity_view_by_tenant_and_search_text;
|
||||
DROP MATERIALIZED VIEW IF EXISTS thingsboard.entity_view_by_tenant_and_customer;
|
||||
DROP MATERIALIZED VIEW IF EXISTS thingsboard.entity_view_by_tenant_and_entity_id;
|
||||
|
||||
DROP TABLE IF EXISTS thingsboard.entity_views;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS thingsboard.entity_views (
|
||||
id timeuuid,
|
||||
entity_id timeuuid,
|
||||
|
||||
@ -14,10 +14,8 @@
|
||||
-- limitations under the License.
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS entity_views;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS entity_views (
|
||||
id varchar(31) NOT NULL CONSTRAINT entity_view_pkey PRIMARY KEY,
|
||||
id varchar(31) NOT NULL CONSTRAINT entity_views_pkey PRIMARY KEY,
|
||||
entity_id varchar(31),
|
||||
entity_type varchar(255),
|
||||
tenant_id varchar(31),
|
||||
|
||||
@ -16,7 +16,6 @@
|
||||
package org.thingsboard.server.actors.device;
|
||||
|
||||
import akka.actor.ActorContext;
|
||||
import akka.event.LoggingAdapter;
|
||||
import com.datastax.driver.core.utils.UUIDs;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
@ -26,12 +25,12 @@ import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.thingsboard.rule.engine.api.RpcError;
|
||||
import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg;
|
||||
import org.thingsboard.rule.engine.api.msg.DeviceNameOrTypeUpdateMsg;
|
||||
import org.thingsboard.server.actors.ActorSystemContext;
|
||||
import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
|
||||
import org.thingsboard.server.common.data.DataConstants;
|
||||
import org.thingsboard.server.common.data.Device;
|
||||
import org.thingsboard.server.common.data.id.DeviceId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
@ -42,7 +41,6 @@ import org.thingsboard.server.common.data.rpc.ToDeviceRpcRequestBody;
|
||||
import org.thingsboard.server.common.msg.TbMsg;
|
||||
import org.thingsboard.server.common.msg.TbMsgDataType;
|
||||
import org.thingsboard.server.common.msg.TbMsgMetaData;
|
||||
import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
|
||||
import org.thingsboard.server.common.msg.rpc.ToDeviceRpcRequest;
|
||||
import org.thingsboard.server.common.msg.session.SessionMsgType;
|
||||
import org.thingsboard.server.common.msg.timeout.DeviceActorClientSideRpcTimeoutMsg;
|
||||
@ -81,12 +79,14 @@ import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.thingsboard.server.common.data.DataConstants.CLIENT_SCOPE;
|
||||
import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE;
|
||||
|
||||
/**
|
||||
* @author Andrew Shvayka
|
||||
*/
|
||||
@ -263,10 +263,8 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
|
||||
}
|
||||
|
||||
private void handleGetAttributesRequest(ActorContext context, SessionInfoProto sessionInfo, GetAttributeRequestMsg request) {
|
||||
ListenableFuture<List<AttributeKvEntry>> clientAttributesFuture = getAttributeKvEntries(deviceId, DataConstants.CLIENT_SCOPE, toOptionalSet(request.getClientAttributeNamesList()));
|
||||
ListenableFuture<List<AttributeKvEntry>> sharedAttributesFuture = getAttributeKvEntries(deviceId, DataConstants.SHARED_SCOPE, toOptionalSet(request.getSharedAttributeNamesList()));
|
||||
int requestId = request.getRequestId();
|
||||
Futures.addCallback(Futures.allAsList(Arrays.asList(clientAttributesFuture, sharedAttributesFuture)), new FutureCallback<List<List<AttributeKvEntry>>>() {
|
||||
Futures.addCallback(getAttributesKvEntries(request), new FutureCallback<List<List<AttributeKvEntry>>>() {
|
||||
@Override
|
||||
public void onSuccess(@Nullable List<List<AttributeKvEntry>> result) {
|
||||
GetAttributeResponseMsg responseMsg = GetAttributeResponseMsg.newBuilder()
|
||||
@ -287,16 +285,35 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
|
||||
});
|
||||
}
|
||||
|
||||
private ListenableFuture<List<AttributeKvEntry>> getAttributeKvEntries(DeviceId deviceId, String scope, Optional<Set<String>> names) {
|
||||
if (names.isPresent()) {
|
||||
if (!names.get().isEmpty()) {
|
||||
return systemContext.getAttributesService().find(tenantId, deviceId, scope, names.get());
|
||||
} else {
|
||||
return systemContext.getAttributesService().findAll(tenantId, deviceId, scope);
|
||||
}
|
||||
private ListenableFuture<List<List<AttributeKvEntry>>> getAttributesKvEntries(GetAttributeRequestMsg request) {
|
||||
ListenableFuture<List<AttributeKvEntry>> clientAttributesFuture;
|
||||
ListenableFuture<List<AttributeKvEntry>> sharedAttributesFuture;
|
||||
if (CollectionUtils.isEmpty(request.getClientAttributeNamesList()) && CollectionUtils.isEmpty(request.getSharedAttributeNamesList())) {
|
||||
clientAttributesFuture = findAllAttributesByScope(CLIENT_SCOPE);
|
||||
sharedAttributesFuture = findAllAttributesByScope(SHARED_SCOPE);
|
||||
} else if (!CollectionUtils.isEmpty(request.getClientAttributeNamesList()) && !CollectionUtils.isEmpty(request.getSharedAttributeNamesList())) {
|
||||
clientAttributesFuture = findAttributesByScope(toSet(request.getClientAttributeNamesList()), CLIENT_SCOPE);
|
||||
sharedAttributesFuture = findAttributesByScope(toSet(request.getSharedAttributeNamesList()), SHARED_SCOPE);
|
||||
} else if (CollectionUtils.isEmpty(request.getClientAttributeNamesList()) && !CollectionUtils.isEmpty(request.getSharedAttributeNamesList())) {
|
||||
clientAttributesFuture = Futures.immediateFuture(Collections.emptyList());
|
||||
sharedAttributesFuture = findAttributesByScope(toSet(request.getSharedAttributeNamesList()), SHARED_SCOPE);
|
||||
} else {
|
||||
return Futures.immediateFuture(Collections.emptyList());
|
||||
sharedAttributesFuture = Futures.immediateFuture(Collections.emptyList());
|
||||
clientAttributesFuture = findAttributesByScope(toSet(request.getClientAttributeNamesList()), CLIENT_SCOPE);
|
||||
}
|
||||
return Futures.allAsList(Arrays.asList(clientAttributesFuture, sharedAttributesFuture));
|
||||
}
|
||||
|
||||
private ListenableFuture<List<AttributeKvEntry>> findAllAttributesByScope(String scope) {
|
||||
return systemContext.getAttributesService().findAll(tenantId, deviceId, scope);
|
||||
}
|
||||
|
||||
private ListenableFuture<List<AttributeKvEntry>> findAttributesByScope(Set<String> attributesSet, String scope) {
|
||||
return systemContext.getAttributesService().find(tenantId, deviceId, scope, attributesSet);
|
||||
}
|
||||
|
||||
private Set<String> toSet(List<String> strings) {
|
||||
return new HashSet<>(strings);
|
||||
}
|
||||
|
||||
private void handlePostAttributesRequest(ActorContext context, SessionInfoProto sessionInfo, PostAttributeMsg postAttributes) {
|
||||
@ -368,7 +385,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
|
||||
AttributeUpdateNotificationMsg.Builder notification = AttributeUpdateNotificationMsg.newBuilder();
|
||||
if (msg.isDeleted()) {
|
||||
List<String> sharedKeys = msg.getDeletedKeys().stream()
|
||||
.filter(key -> DataConstants.SHARED_SCOPE.equals(key.getScope()))
|
||||
.filter(key -> SHARED_SCOPE.equals(key.getScope()))
|
||||
.map(AttributeKey::getAttributeKey)
|
||||
.collect(Collectors.toList());
|
||||
if (!sharedKeys.isEmpty()) {
|
||||
@ -376,7 +393,7 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
|
||||
hasNotificationData = true;
|
||||
}
|
||||
} else {
|
||||
if (DataConstants.SHARED_SCOPE.equals(msg.getScope())) {
|
||||
if (SHARED_SCOPE.equals(msg.getScope())) {
|
||||
List<AttributeKvEntry> attributes = new ArrayList<>(msg.getValues());
|
||||
if (attributes.size() > 0) {
|
||||
List<TsKvProto> sharedUpdated = msg.getValues().stream().map(this::toTsKvProto)
|
||||
@ -545,14 +562,6 @@ class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
|
||||
return json;
|
||||
}
|
||||
|
||||
private Optional<Set<String>> toOptionalSet(List<String> strings) {
|
||||
if (strings == null || strings.isEmpty()) {
|
||||
return Optional.empty();
|
||||
} else {
|
||||
return Optional.of(new HashSet<>(strings));
|
||||
}
|
||||
}
|
||||
|
||||
private void sendToTransport(GetAttributeResponseMsg responseMsg, SessionInfoProto sessionInfo) {
|
||||
DeviceActorToTransportMsg msg = DeviceActorToTransportMsg.newBuilder()
|
||||
.setSessionIdMSB(sessionInfo.getSessionIdMSB())
|
||||
|
||||
@ -17,7 +17,6 @@ package org.thingsboard.server.actors.ruleChain;
|
||||
|
||||
import akka.actor.ActorContext;
|
||||
import akka.actor.ActorRef;
|
||||
import akka.event.LoggingAdapter;
|
||||
import org.thingsboard.rule.engine.api.TbContext;
|
||||
import org.thingsboard.rule.engine.api.TbNode;
|
||||
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
|
||||
|
||||
@ -16,9 +16,6 @@
|
||||
package org.thingsboard.server.actors.service;
|
||||
|
||||
import akka.actor.ActorRef;
|
||||
import akka.event.Logging;
|
||||
import akka.event.LoggingAdapter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.thingsboard.server.actors.ActorSystemContext;
|
||||
import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
|
||||
import org.thingsboard.server.actors.stats.StatsPersistMsg;
|
||||
@ -60,7 +57,8 @@ public abstract class ComponentActor<T extends EntityId, P extends ComponentMsgP
|
||||
scheduleStatsPersistTick();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[{}][{}] Failed to start {} processor: {}", tenantId, id, id.getEntityType(), e);
|
||||
log.warn("[{}][{}] Failed to start {} processor.", tenantId, id, id.getEntityType());
|
||||
log.warn("Error:", e);
|
||||
logAndPersist("OnStart", e, true);
|
||||
logLifecycleEvent(ComponentLifecycleEvent.STARTED, e);
|
||||
}
|
||||
@ -149,10 +147,13 @@ public abstract class ComponentActor<T extends EntityId, P extends ComponentMsgP
|
||||
|
||||
private void logAndPersist(String method, Exception e, boolean critical) {
|
||||
errorsOccurred++;
|
||||
String componentName = processor != null ? processor.getComponentName() : "Unknown";
|
||||
if (critical) {
|
||||
log.warn("[{}][{}][{}] Failed to process {} msg: {}", id, tenantId, processor.getComponentName(), method, e);
|
||||
log.warn("[{}][{}][{}] Failed to process {} msg: {}", id, tenantId, componentName, method);
|
||||
log.warn("Critical Error: ", e);
|
||||
} else {
|
||||
log.debug("[{}][{}][{}] Failed to process {} msg: {}", id, tenantId, processor.getComponentName(), method, e);
|
||||
log.debug("[{}][{}][{}] Failed to process {} msg: {}", id, tenantId, componentName, method);
|
||||
log.debug("Debug Error: ", e);
|
||||
}
|
||||
long ts = System.currentTimeMillis();
|
||||
if (ts - lastPersistedErrorTs > getErrorPersistFrequency()) {
|
||||
|
||||
@ -64,7 +64,8 @@ public class SwaggerConfiguration {
|
||||
.paths(apiPaths())
|
||||
.build()
|
||||
.securitySchemes(newArrayList(jwtTokenKey()))
|
||||
.securityContexts(newArrayList(securityContext()));
|
||||
.securityContexts(newArrayList(securityContext()))
|
||||
.enableUrlTemplating(true);
|
||||
}
|
||||
|
||||
private ApiKey jwtTokenKey() {
|
||||
|
||||
@ -280,8 +280,14 @@ public class TelemetryController extends BaseController {
|
||||
deleteFromTs = 0L;
|
||||
deleteToTs = System.currentTimeMillis();
|
||||
} else {
|
||||
deleteFromTs = startTs;
|
||||
deleteToTs = endTs;
|
||||
if (startTs == null || endTs == null) {
|
||||
deleteToTs = endTs;
|
||||
return getImmediateDeferredResult("When deleteAllDataForKeys is false, start and end timestamp values shouldn't be empty", HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
else{
|
||||
deleteFromTs = startTs;
|
||||
deleteToTs = endTs;
|
||||
}
|
||||
}
|
||||
|
||||
return accessValidator.validateEntityAndCallback(user, Operation.WRITE_TELEMETRY, entityIdStr, (result, tenantId, entityId) -> {
|
||||
|
||||
@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.thingsboard.server.common.data.Device;
|
||||
import org.thingsboard.server.common.data.id.DeviceId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
@ -100,7 +101,7 @@ public class LocalTransportApiService implements TransportApiService {
|
||||
//TODO: Make async and enable caching
|
||||
DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(credentialsId);
|
||||
if (credentials != null && credentials.getCredentialsType() == credentialsType) {
|
||||
return getDeviceInfo(credentials.getDeviceId());
|
||||
return getDeviceInfo(credentials.getDeviceId(), credentials);
|
||||
} else {
|
||||
return getEmptyTransportApiResponseFuture();
|
||||
}
|
||||
@ -135,15 +136,20 @@ public class LocalTransportApiService implements TransportApiService {
|
||||
}
|
||||
|
||||
|
||||
private ListenableFuture<TransportApiResponseMsg> getDeviceInfo(DeviceId deviceId) {
|
||||
private ListenableFuture<TransportApiResponseMsg> getDeviceInfo(DeviceId deviceId, DeviceCredentials credentials) {
|
||||
return Futures.transform(deviceService.findDeviceByIdAsync(TenantId.SYS_TENANT_ID, deviceId), device -> {
|
||||
if (device == null) {
|
||||
log.trace("[{}] Failed to lookup device by id", deviceId);
|
||||
return getEmptyTransportApiResponse();
|
||||
}
|
||||
try {
|
||||
ValidateDeviceCredentialsResponseMsg.Builder builder = ValidateDeviceCredentialsResponseMsg.newBuilder();
|
||||
builder.setDeviceInfo(getDeviceInfoProto(device));
|
||||
if(!StringUtils.isEmpty(credentials.getCredentialsValue())){
|
||||
builder.setCredentialsBody(credentials.getCredentialsValue());
|
||||
}
|
||||
return TransportApiResponseMsg.newBuilder()
|
||||
.setValidateTokenResponseMsg(ValidateDeviceCredentialsResponseMsg.newBuilder().setDeviceInfo(getDeviceInfoProto(device)).build()).build();
|
||||
.setValidateTokenResponseMsg(builder.build()).build();
|
||||
} catch (JsonProcessingException e) {
|
||||
log.warn("[{}] Failed to lookup device by id", deviceId, e);
|
||||
return getEmptyTransportApiResponse();
|
||||
|
||||
@ -54,18 +54,18 @@ import java.util.concurrent.TimeUnit;
|
||||
@Slf4j
|
||||
@Component("MqttSslHandlerProvider")
|
||||
@ConditionalOnExpression("'${transport.type:null}'=='null' || ('${transport.type}'=='local' && '${transport.http.enabled}'=='true')")
|
||||
@ConditionalOnProperty(prefix = "mqtt.ssl", value = "enabled", havingValue = "true", matchIfMissing = false)
|
||||
@ConditionalOnProperty(prefix = "transport.mqtt.ssl", value = "enabled", havingValue = "true", matchIfMissing = false)
|
||||
public class MqttSslHandlerProvider {
|
||||
|
||||
@Value("${mqtt.ssl.protocol}")
|
||||
@Value("${transport.mqtt.ssl.protocol}")
|
||||
private String sslProtocol;
|
||||
@Value("${mqtt.ssl.key_store}")
|
||||
@Value("${transport.mqtt.ssl.key_store}")
|
||||
private String keyStoreFile;
|
||||
@Value("${mqtt.ssl.key_store_password}")
|
||||
@Value("${transport.mqtt.ssl.key_store_password}")
|
||||
private String keyStorePassword;
|
||||
@Value("${mqtt.ssl.key_password}")
|
||||
@Value("${transport.mqtt.ssl.key_password}")
|
||||
private String keyPassword;
|
||||
@Value("${mqtt.ssl.key_store_type}")
|
||||
@Value("${transport.mqtt.ssl.key_store_type}")
|
||||
private String keyStoreType;
|
||||
|
||||
@Autowired
|
||||
|
||||
@ -271,6 +271,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement
|
||||
case MqttTopics.DEVICE_RPC_RESPONSE_SUB_TOPIC:
|
||||
case MqttTopics.GATEWAY_ATTRIBUTES_TOPIC:
|
||||
case MqttTopics.GATEWAY_RPC_TOPIC:
|
||||
case MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC:
|
||||
case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC:
|
||||
registerSubQoS(topic, grantedQoSList, reqQoS);
|
||||
break;
|
||||
|
||||
@ -36,9 +36,8 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
|
||||
@Override
|
||||
public void initChannel(SocketChannel ch) {
|
||||
ChannelPipeline pipeline = ch.pipeline();
|
||||
SslHandler sslHandler = null;
|
||||
if (context.getSslHandlerProvider() != null) {
|
||||
sslHandler = context.getSslHandlerProvider().getSslHandler();
|
||||
SslHandler sslHandler = context.getSslHandlerProvider().getSslHandler();
|
||||
pipeline.addLast(sslHandler);
|
||||
context.setSslHandler(sslHandler);
|
||||
}
|
||||
|
||||
@ -40,7 +40,7 @@ public abstract class AbstractTransportService implements TransportService {
|
||||
private boolean rateLimitEnabled;
|
||||
@Value("${transport.rate_limits.tenant}")
|
||||
private String perTenantLimitsConf;
|
||||
@Value("${transport.rate_limits.tenant}")
|
||||
@Value("${transport.rate_limits.device}")
|
||||
private String perDevicesLimitsConf;
|
||||
@Value("${transport.sessions.inactivity_timeout}")
|
||||
private long sessionInactivityTimeout;
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.thingsboard.server.dao.util;
|
||||
|
||||
import com.datastax.driver.core.*;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
@ -22,6 +23,7 @@ import com.google.common.util.concurrent.SettableFuture;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.msg.tools.TbRateLimits;
|
||||
import org.thingsboard.server.dao.nosql.CassandraStatementTask;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.UUID;
|
||||
@ -183,12 +185,39 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
|
||||
|
||||
private void logTask(String action, AsyncTaskContext<T, V> taskCtx) {
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("[{}] {} task: {}", taskCtx.getId(), action, taskCtx);
|
||||
if (taskCtx.getTask() instanceof CassandraStatementTask) {
|
||||
CassandraStatementTask cassStmtTask = (CassandraStatementTask) taskCtx.getTask();
|
||||
if (cassStmtTask.getStatement() instanceof BoundStatement) {
|
||||
BoundStatement stmt = (BoundStatement) cassStmtTask.getStatement();
|
||||
String query = toStringWithValues(stmt, ProtocolVersion.V5);
|
||||
log.trace("[{}] {} task: {}, BoundStatement query: {}", taskCtx.getId(), action, taskCtx, query);
|
||||
}
|
||||
} else {
|
||||
log.trace("[{}] {} task: {}", taskCtx.getId(), action, taskCtx);
|
||||
}
|
||||
} else {
|
||||
log.debug("[{}] {} task", taskCtx.getId(), action);
|
||||
}
|
||||
}
|
||||
|
||||
private static String toStringWithValues(BoundStatement boundStatement, ProtocolVersion protocolVersion) {
|
||||
CodecRegistry codecRegistry = boundStatement.preparedStatement().getCodecRegistry();
|
||||
PreparedStatement preparedStatement = boundStatement.preparedStatement();
|
||||
String query = preparedStatement.getQueryString();
|
||||
ColumnDefinitions defs = preparedStatement.getVariables();
|
||||
int index = 0;
|
||||
for (ColumnDefinitions.Definition def : defs) {
|
||||
DataType type = def.getType();
|
||||
TypeCodec<Object> codec = codecRegistry.codecFor(type);
|
||||
if (boundStatement.getBytesUnsafe(index) != null) {
|
||||
Object value = codec.deserialize(boundStatement.getBytesUnsafe(index), protocolVersion);
|
||||
query = query.replaceFirst("\\?", codec.format(value));
|
||||
}
|
||||
index++;
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
protected int getQueueSize() {
|
||||
return queue.size();
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ CREATE TABLE IF NOT EXISTS attribute_kv (
|
||||
long_v bigint,
|
||||
dbl_v double precision,
|
||||
last_update_ts bigint,
|
||||
CONSTRAINT attribute_kv_unq_key UNIQUE (entity_type, entity_id, attribute_type, attribute_key)
|
||||
CONSTRAINT attribute_kv_pkey PRIMARY KEY (entity_type, entity_id, attribute_type, attribute_key)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS component_descriptor (
|
||||
@ -148,7 +148,7 @@ CREATE TABLE IF NOT EXISTS relation (
|
||||
relation_type_group varchar(255),
|
||||
relation_type varchar(255),
|
||||
additional_info varchar,
|
||||
CONSTRAINT relation_unq_key UNIQUE (from_id, from_type, relation_type_group, relation_type, to_id, to_type)
|
||||
CONSTRAINT relation_pkey PRIMARY KEY (from_id, from_type, relation_type_group, relation_type, to_id, to_type)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tb_user (
|
||||
|
||||
@ -23,7 +23,7 @@ CREATE TABLE IF NOT EXISTS ts_kv (
|
||||
str_v varchar(10000000),
|
||||
long_v bigint,
|
||||
dbl_v double precision,
|
||||
CONSTRAINT ts_kv_unq_key UNIQUE (entity_type, entity_id, key, ts)
|
||||
CONSTRAINT ts_kv_pkey PRIMARY KEY (entity_type, entity_id, key, ts)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ts_kv_latest (
|
||||
@ -35,5 +35,5 @@ CREATE TABLE IF NOT EXISTS ts_kv_latest (
|
||||
str_v varchar(10000000),
|
||||
long_v bigint,
|
||||
dbl_v double precision,
|
||||
CONSTRAINT ts_kv_latest_unq_key UNIQUE (entity_type, entity_id, key)
|
||||
CONSTRAINT ts_kv_latest_pkey PRIMARY KEY (entity_type, entity_id, key)
|
||||
);
|
||||
|
||||
@ -32,7 +32,8 @@ import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
|
||||
import org.apache.http.ssl.SSLContextBuilder;
|
||||
import org.apache.http.ssl.SSLContexts;
|
||||
import org.junit.*;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.rules.TestWatcher;
|
||||
import org.junit.runner.Description;
|
||||
@ -43,7 +44,10 @@ import org.thingsboard.server.common.data.EntityType;
|
||||
import org.thingsboard.server.common.data.id.DeviceId;
|
||||
import org.thingsboard.server.msa.mapper.WsTelemetryResponse;
|
||||
|
||||
import javax.net.ssl.*;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import java.net.URI;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
@ -54,6 +58,7 @@ import java.util.Random;
|
||||
public abstract class AbstractContainerTest {
|
||||
protected static final String HTTPS_URL = "https://localhost";
|
||||
protected static final String WSS_URL = "wss://localhost";
|
||||
protected static String TB_TOKEN;
|
||||
protected static RestClient restClient;
|
||||
protected ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.thingsboard.server.msa.connectivity;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.google.common.collect.Sets;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
@ -25,6 +26,17 @@ import org.thingsboard.server.msa.AbstractContainerTest;
|
||||
import org.thingsboard.server.msa.WsClient;
|
||||
import org.thingsboard.server.msa.mapper.WsTelemetryResponse;
|
||||
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.thingsboard.server.common.data.DataConstants.DEVICE;
|
||||
import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE;
|
||||
|
||||
public class HttpClientTest extends AbstractContainerTest {
|
||||
|
||||
@Test
|
||||
@ -52,6 +64,58 @@ public class HttpClientTest extends AbstractContainerTest {
|
||||
Assert.assertTrue(verify(actualLatestTelemetry, "doubleKey", Double.toString(42.0)));
|
||||
Assert.assertTrue(verify(actualLatestTelemetry, "longKey", Long.toString(73)));
|
||||
|
||||
restClient.getRestTemplate().delete(HTTPS_URL + "/api/device/" + device.getId());
|
||||
restClient.deleteDevice(device.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getAttributes() throws Exception {
|
||||
restClient.login("tenant@thingsboard.org", "tenant");
|
||||
TB_TOKEN = restClient.getToken();
|
||||
|
||||
Device device = createDevice("test");
|
||||
String accessToken = restClient.getCredentials(device.getId()).getCredentialsId();
|
||||
assertNotNull(accessToken);
|
||||
|
||||
ResponseEntity deviceSharedAttributes = restClient.getRestTemplate()
|
||||
.postForEntity(HTTPS_URL + "/api/plugins/telemetry/" + DEVICE + "/" + device.getId().toString() + "/attributes/" + SHARED_SCOPE, mapper.readTree(createPayload().toString()),
|
||||
ResponseEntity.class,
|
||||
accessToken);
|
||||
|
||||
Assert.assertTrue(deviceSharedAttributes.getStatusCode().is2xxSuccessful());
|
||||
|
||||
ResponseEntity deviceClientsAttributes = restClient.getRestTemplate()
|
||||
.postForEntity(HTTPS_URL + "/api/v1/" + accessToken + "/attributes/", mapper.readTree(createPayload().toString()),
|
||||
ResponseEntity.class,
|
||||
accessToken);
|
||||
|
||||
Assert.assertTrue(deviceClientsAttributes.getStatusCode().is2xxSuccessful());
|
||||
|
||||
TimeUnit.SECONDS.sleep(3);
|
||||
|
||||
Optional<JsonNode> allOptional = restClient.getAttributes(accessToken, null, null);
|
||||
assertTrue(allOptional.isPresent());
|
||||
|
||||
|
||||
JsonNode all = allOptional.get();
|
||||
assertEquals(2, all.size());
|
||||
assertEquals(mapper.readTree(createPayload().toString()), all.get("shared"));
|
||||
assertEquals(mapper.readTree(createPayload().toString()), all.get("client"));
|
||||
|
||||
Optional<JsonNode> sharedOptional = restClient.getAttributes(accessToken, null, "stringKey");
|
||||
assertTrue(sharedOptional.isPresent());
|
||||
|
||||
JsonNode shared = sharedOptional.get();
|
||||
assertEquals(shared.get("shared").get("stringKey"), mapper.readTree(createPayload().get("stringKey").toString()));
|
||||
assertFalse(shared.has("client"));
|
||||
|
||||
Optional<JsonNode> clientOptional = restClient.getAttributes(accessToken, "longKey,stringKey", null);
|
||||
assertTrue(clientOptional.isPresent());
|
||||
|
||||
JsonNode client = clientOptional.get();
|
||||
assertFalse(client.has("shared"));
|
||||
assertEquals(mapper.readTree(createPayload().get("longKey").toString()), client.get("client").get("longKey"));
|
||||
assertEquals(client.get("client").get("stringKey"), mapper.readTree(createPayload().get("stringKey").toString()));
|
||||
|
||||
restClient.deleteDevice(device.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@ -222,6 +222,9 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage>
|
||||
|
||||
private void handlePuback(MqttPubAckMessage message) {
|
||||
MqttPendingPublish pendingPublish = this.client.getPendingPublishes().get(message.variableHeader().messageId());
|
||||
if (pendingPublish == null) {
|
||||
return;
|
||||
}
|
||||
pendingPublish.getFuture().setSuccess(null);
|
||||
pendingPublish.onPubackReceived();
|
||||
this.client.getPendingPublishes().remove(message.variableHeader().messageId());
|
||||
|
||||
@ -15,8 +15,6 @@
|
||||
*/
|
||||
package org.thingsboard.mqtt;
|
||||
|
||||
import io.netty.channel.ChannelId;
|
||||
|
||||
/**
|
||||
* Created by Valerii Sosliuk on 12/30/2017.
|
||||
*/
|
||||
|
||||
6
pom.xml
6
pom.xml
@ -70,6 +70,7 @@
|
||||
<surfire.version>2.19.1</surfire.version>
|
||||
<jar-plugin.version>3.0.2</jar-plugin.version>
|
||||
<springfox-swagger.version>2.6.1</springfox-swagger.version>
|
||||
<springfox-swagger-ui-rfc6570.version>1.0.0</springfox-swagger-ui-rfc6570.version>
|
||||
<bouncycastle.version>1.56</bouncycastle.version>
|
||||
<winsw.version>2.0.1</winsw.version>
|
||||
<hsqldb.version>2.4.0</hsqldb.version>
|
||||
@ -794,6 +795,11 @@
|
||||
<artifactId>bucket4j-core</artifactId>
|
||||
<version>${bucket4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.springfox.ui</groupId>
|
||||
<artifactId>springfox-swagger-ui-rfc6570</artifactId>
|
||||
<version>${springfox-swagger-ui-rfc6570.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
||||
@ -17,9 +17,6 @@ package org.thingsboard.rule.engine.action;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Created by igor on 6/1/18.
|
||||
*/
|
||||
@Data
|
||||
public abstract class TbAbstractCustomerActionNodeConfiguration {
|
||||
|
||||
|
||||
@ -0,0 +1,222 @@
|
||||
/**
|
||||
* Copyright © 2016-2018 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.action;
|
||||
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.rule.engine.util.EntityContainer;
|
||||
import org.thingsboard.server.common.data.Customer;
|
||||
import org.thingsboard.server.common.data.DashboardInfo;
|
||||
import org.thingsboard.server.common.data.Device;
|
||||
import org.thingsboard.server.common.data.EntityType;
|
||||
import org.thingsboard.server.common.data.EntityView;
|
||||
import org.thingsboard.server.common.data.asset.Asset;
|
||||
import org.thingsboard.server.common.data.id.EntityId;
|
||||
import org.thingsboard.server.common.data.id.EntityIdFactory;
|
||||
import org.thingsboard.server.common.data.page.TextPageData;
|
||||
import org.thingsboard.server.common.data.page.TextPageLink;
|
||||
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
|
||||
import org.thingsboard.server.common.msg.TbMsg;
|
||||
import org.thingsboard.server.dao.asset.AssetService;
|
||||
import org.thingsboard.server.dao.customer.CustomerService;
|
||||
import org.thingsboard.server.dao.dashboard.DashboardService;
|
||||
import org.thingsboard.server.dao.device.DeviceService;
|
||||
import org.thingsboard.server.dao.entityview.EntityViewService;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE;
|
||||
import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
|
||||
import static org.thingsboard.rule.engine.api.util.DonAsynchron.withCallback;
|
||||
|
||||
@Slf4j
|
||||
public abstract class TbAbstractRelationActionNode<C extends TbAbstractRelationActionNodeConfiguration> implements TbNode {
|
||||
|
||||
protected C config;
|
||||
protected EntityId fromId;
|
||||
protected EntityId toId;
|
||||
|
||||
private LoadingCache<Entitykey, EntityContainer> entityIdCache;
|
||||
|
||||
@Override
|
||||
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
|
||||
this.config = loadEntityNodeActionConfig(configuration);
|
||||
CacheBuilder cacheBuilder = CacheBuilder.newBuilder();
|
||||
if (this.config.getEntityCacheExpiration() > 0) {
|
||||
cacheBuilder.expireAfterWrite(this.config.getEntityCacheExpiration(), TimeUnit.SECONDS);
|
||||
}
|
||||
entityIdCache = cacheBuilder
|
||||
.build(new EntityCacheLoader(ctx, createEntityIfNotExists()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMsg(TbContext ctx, TbMsg msg) {
|
||||
withCallback(processEntityRelationAction(ctx, msg),
|
||||
filterResult -> ctx.tellNext(msg, filterResult ? SUCCESS : FAILURE), t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
}
|
||||
|
||||
private ListenableFuture<Boolean> processEntityRelationAction(TbContext ctx, TbMsg msg) {
|
||||
return Futures.transformAsync(getEntity(ctx, msg), entityContainer -> doProcessEntityRelationAction(ctx, msg, entityContainer));
|
||||
}
|
||||
|
||||
protected abstract boolean createEntityIfNotExists();
|
||||
|
||||
protected abstract ListenableFuture<Boolean> doProcessEntityRelationAction(TbContext ctx, TbMsg msg, EntityContainer entityContainer);
|
||||
|
||||
protected abstract C loadEntityNodeActionConfig(TbNodeConfiguration configuration) throws TbNodeException;
|
||||
|
||||
protected ListenableFuture<EntityContainer> getEntity(TbContext ctx, TbMsg msg) {
|
||||
String entityName = TbNodeUtils.processPattern(this.config.getEntityNamePattern(), msg.getMetaData());
|
||||
String type = null;
|
||||
if (this.config.getEntityTypePattern() != null) {
|
||||
type = TbNodeUtils.processPattern(this.config.getEntityTypePattern(), msg.getMetaData());
|
||||
}
|
||||
EntityType entityType = EntityType.valueOf(this.config.getEntityType());
|
||||
Entitykey key = new Entitykey(entityName, type, entityType);
|
||||
return ctx.getDbCallbackExecutor().executeAsync(() -> {
|
||||
EntityContainer entityContainer = entityIdCache.get(key);
|
||||
if (entityContainer.getEntityId() == null) {
|
||||
throw new RuntimeException("No entity found with type '" + key.getEntityType() + " ' and name '" + key.getEntityName() + "'.");
|
||||
}
|
||||
return entityContainer;
|
||||
});
|
||||
}
|
||||
|
||||
protected void processSearchDirection(TbMsg msg, EntityContainer entityContainer) {
|
||||
if (EntitySearchDirection.FROM.name().equals(config.getDirection())) {
|
||||
fromId = EntityIdFactory.getByTypeAndId(entityContainer.getEntityType().name(), entityContainer.getEntityId().toString());
|
||||
toId = msg.getOriginator();
|
||||
} else {
|
||||
toId = EntityIdFactory.getByTypeAndId(entityContainer.getEntityType().name(), entityContainer.getEntityId().toString());
|
||||
fromId = msg.getOriginator();
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
private static class Entitykey {
|
||||
private String entityName;
|
||||
private String type;
|
||||
private EntityType entityType;
|
||||
}
|
||||
|
||||
private static class EntityCacheLoader extends CacheLoader<Entitykey, EntityContainer> {
|
||||
|
||||
private final TbContext ctx;
|
||||
private final boolean createIfNotExists;
|
||||
|
||||
private EntityCacheLoader(TbContext ctx, boolean createIfNotExists) {
|
||||
this.ctx = ctx;
|
||||
this.createIfNotExists = createIfNotExists;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EntityContainer load(Entitykey key) {
|
||||
return loadEntity(key);
|
||||
}
|
||||
|
||||
private EntityContainer loadEntity(Entitykey entitykey) {
|
||||
EntityType type = entitykey.getEntityType();
|
||||
EntityContainer targetEntity = new EntityContainer();
|
||||
targetEntity.setEntityType(type);
|
||||
switch (type) {
|
||||
case DEVICE:
|
||||
DeviceService deviceService = ctx.getDeviceService();
|
||||
Device device = deviceService.findDeviceByTenantIdAndName(ctx.getTenantId(), entitykey.getEntityName());
|
||||
if (device != null) {
|
||||
targetEntity.setEntityId(device.getId());
|
||||
} else if (createIfNotExists) {
|
||||
Device newDevice = new Device();
|
||||
newDevice.setName(entitykey.getEntityName());
|
||||
newDevice.setType(entitykey.getType());
|
||||
newDevice.setTenantId(ctx.getTenantId());
|
||||
Device savedDevice = deviceService.saveDevice(newDevice);
|
||||
targetEntity.setEntityId(savedDevice.getId());
|
||||
}
|
||||
break;
|
||||
case ASSET:
|
||||
AssetService assetService = ctx.getAssetService();
|
||||
Asset asset = assetService.findAssetByTenantIdAndName(ctx.getTenantId(), entitykey.getEntityName());
|
||||
if (asset != null) {
|
||||
targetEntity.setEntityId(asset.getId());
|
||||
} else if (createIfNotExists) {
|
||||
Asset newAsset = new Asset();
|
||||
newAsset.setName(entitykey.getEntityName());
|
||||
newAsset.setType(entitykey.getType());
|
||||
newAsset.setTenantId(ctx.getTenantId());
|
||||
Asset savedAsset = assetService.saveAsset(newAsset);
|
||||
targetEntity.setEntityId(savedAsset.getId());
|
||||
}
|
||||
break;
|
||||
case CUSTOMER:
|
||||
CustomerService customerService = ctx.getCustomerService();
|
||||
Optional<Customer> customerOptional = customerService.findCustomerByTenantIdAndTitle(ctx.getTenantId(), entitykey.getEntityName());
|
||||
if (customerOptional.isPresent()) {
|
||||
targetEntity.setEntityId(customerOptional.get().getId());
|
||||
} else if (createIfNotExists) {
|
||||
Customer newCustomer = new Customer();
|
||||
newCustomer.setTitle(entitykey.getEntityName());
|
||||
newCustomer.setTenantId(ctx.getTenantId());
|
||||
Customer savedCustomer = customerService.saveCustomer(newCustomer);
|
||||
targetEntity.setEntityId(savedCustomer.getId());
|
||||
}
|
||||
break;
|
||||
case TENANT:
|
||||
targetEntity.setEntityId(ctx.getTenantId());
|
||||
break;
|
||||
case ENTITY_VIEW:
|
||||
EntityViewService entityViewService = ctx.getEntityViewService();
|
||||
EntityView entityView = entityViewService.findEntityViewByTenantIdAndName(ctx.getTenantId(), entitykey.getEntityName());
|
||||
if (entityView != null) {
|
||||
targetEntity.setEntityId(entityView.getId());
|
||||
}
|
||||
break;
|
||||
case DASHBOARD:
|
||||
DashboardService dashboardService = ctx.getDashboardService();
|
||||
TextPageData<DashboardInfo> dashboardInfoTextPageData = dashboardService.findDashboardsByTenantId(ctx.getTenantId(), new TextPageLink(200, entitykey.getEntityName()));
|
||||
for (DashboardInfo dashboardInfo : dashboardInfoTextPageData.getData()) {
|
||||
if (dashboardInfo.getTitle().equals(entitykey.getEntityName())) {
|
||||
targetEntity.setEntityId(dashboardInfo.getId());
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return targetEntity;
|
||||
}
|
||||
return targetEntity;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright © 2016-2018 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.action;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public abstract class TbAbstractRelationActionNodeConfiguration {
|
||||
|
||||
private String direction;
|
||||
private String relationType;
|
||||
|
||||
private String entityType;
|
||||
private String entityNamePattern;
|
||||
private String entityTypePattern;
|
||||
|
||||
private long entityCacheExpiration;
|
||||
|
||||
}
|
||||
@ -52,6 +52,10 @@ public class TbAssignToCustomerNode extends TbAbstractCustomerActionNode<TbAssig
|
||||
|
||||
@Override
|
||||
protected void doProcessCustomerAction(TbContext ctx, TbMsg msg, CustomerId customerId) {
|
||||
processAssign(ctx, msg, customerId);
|
||||
}
|
||||
|
||||
private void processAssign(TbContext ctx, TbMsg msg, CustomerId customerId) {
|
||||
EntityType originatorType = msg.getOriginator().getEntityType();
|
||||
switch (originatorType) {
|
||||
case DEVICE:
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
package org.thingsboard.rule.engine.action;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
@ -31,6 +32,8 @@ import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.data.plugin.ComponentType;
|
||||
import org.thingsboard.server.common.msg.TbMsg;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Slf4j
|
||||
@RuleNode(
|
||||
type = ComponentType.ACTION,
|
||||
@ -49,6 +52,8 @@ import org.thingsboard.server.common.msg.TbMsg;
|
||||
)
|
||||
public class TbCreateAlarmNode extends TbAbstractAlarmNode<TbCreateAlarmNodeConfiguration> {
|
||||
|
||||
private static ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
protected TbCreateAlarmNodeConfiguration loadAlarmNodeConfig(TbNodeConfiguration configuration) throws TbNodeException {
|
||||
return TbNodeUtils.convert(configuration, TbCreateAlarmNodeConfiguration.class);
|
||||
@ -56,32 +61,59 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode<TbCreateAlarmNodeConf
|
||||
|
||||
@Override
|
||||
protected ListenableFuture<AlarmResult> processAlarm(TbContext ctx, TbMsg msg) {
|
||||
ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), config.getAlarmType());
|
||||
return Futures.transformAsync(latest, a -> {
|
||||
if (a == null || a.getStatus().isCleared()) {
|
||||
return createNewAlarm(ctx, msg);
|
||||
String alarmType;
|
||||
final Alarm msgAlarm;
|
||||
|
||||
if (!config.isUseMessageAlarmData()) {
|
||||
alarmType = config.getAlarmType();
|
||||
msgAlarm = null;
|
||||
} else {
|
||||
try {
|
||||
msgAlarm = mapper.readValue(msg.getData(), Alarm.class);
|
||||
msgAlarm.setTenantId(ctx.getTenantId());
|
||||
alarmType = msgAlarm.getType();
|
||||
} catch (IOException e) {
|
||||
ctx.tellFailure(msg, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
ListenableFuture<Alarm> latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), alarmType);
|
||||
return Futures.transformAsync(latest, existingAlarm -> {
|
||||
if (existingAlarm == null || existingAlarm.getStatus().isCleared()) {
|
||||
return createNewAlarm(ctx, msg, msgAlarm);
|
||||
} else {
|
||||
return updateAlarm(ctx, msg, a);
|
||||
return updateAlarm(ctx, msg, existingAlarm, msgAlarm);
|
||||
}
|
||||
}, ctx.getDbCallbackExecutor());
|
||||
|
||||
}
|
||||
|
||||
private ListenableFuture<AlarmResult> createNewAlarm(TbContext ctx, TbMsg msg) {
|
||||
ListenableFuture<Alarm> asyncAlarm = Futures.transform(buildAlarmDetails(ctx, msg, null),
|
||||
details -> buildAlarm(msg, details, ctx.getTenantId()));
|
||||
private ListenableFuture<AlarmResult> createNewAlarm(TbContext ctx, TbMsg msg, Alarm msgAlarm) {
|
||||
ListenableFuture<Alarm> asyncAlarm;
|
||||
if (msgAlarm != null ) {
|
||||
asyncAlarm = Futures.immediateCheckedFuture(msgAlarm);
|
||||
} else {
|
||||
asyncAlarm = Futures.transform(buildAlarmDetails(ctx, msg, null),
|
||||
details -> buildAlarm(msg, details, ctx.getTenantId()));
|
||||
}
|
||||
ListenableFuture<Alarm> asyncCreated = Futures.transform(asyncAlarm,
|
||||
alarm -> ctx.getAlarmService().createOrUpdateAlarm(alarm), ctx.getDbCallbackExecutor());
|
||||
return Futures.transform(asyncCreated, alarm -> new AlarmResult(true, false, false, alarm));
|
||||
}
|
||||
|
||||
private ListenableFuture<AlarmResult> updateAlarm(TbContext ctx, TbMsg msg, Alarm alarm) {
|
||||
ListenableFuture<Alarm> asyncUpdated = Futures.transform(buildAlarmDetails(ctx, msg, alarm.getDetails()), (Function<JsonNode, Alarm>) details -> {
|
||||
alarm.setSeverity(config.getSeverity());
|
||||
alarm.setPropagate(config.isPropagate());
|
||||
alarm.setDetails(details);
|
||||
alarm.setEndTs(System.currentTimeMillis());
|
||||
return ctx.getAlarmService().createOrUpdateAlarm(alarm);
|
||||
private ListenableFuture<AlarmResult> updateAlarm(TbContext ctx, TbMsg msg, Alarm existingAlarm, Alarm msgAlarm) {
|
||||
ListenableFuture<Alarm> asyncUpdated = Futures.transform(buildAlarmDetails(ctx, msg, existingAlarm.getDetails()), (Function<JsonNode, Alarm>) details -> {
|
||||
if (msgAlarm != null) {
|
||||
existingAlarm.setSeverity(msgAlarm.getSeverity());
|
||||
existingAlarm.setPropagate(msgAlarm.isPropagate());
|
||||
} else {
|
||||
existingAlarm.setSeverity(config.getSeverity());
|
||||
existingAlarm.setPropagate(config.isPropagate());
|
||||
}
|
||||
existingAlarm.setDetails(details);
|
||||
existingAlarm.setEndTs(System.currentTimeMillis());
|
||||
return ctx.getAlarmService().createOrUpdateAlarm(existingAlarm);
|
||||
}, ctx.getDbCallbackExecutor());
|
||||
|
||||
return Futures.transform(asyncUpdated, a -> new AlarmResult(false, true, false, a));
|
||||
|
||||
@ -24,6 +24,7 @@ public class TbCreateAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigura
|
||||
|
||||
private AlarmSeverity severity;
|
||||
private boolean propagate;
|
||||
private boolean useMessageAlarmData;
|
||||
|
||||
@Override
|
||||
public TbCreateAlarmNodeConfiguration defaultConfiguration() {
|
||||
@ -36,6 +37,7 @@ public class TbCreateAlarmNodeConfiguration extends TbAbstractAlarmNodeConfigura
|
||||
configuration.setAlarmType("General Alarm");
|
||||
configuration.setSeverity(AlarmSeverity.CRITICAL);
|
||||
configuration.setPropagate(false);
|
||||
configuration.setUseMessageAlarmData(false);
|
||||
return configuration;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Copyright © 2016-2018 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.action;
|
||||
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.thingsboard.rule.engine.api.RuleNode;
|
||||
import org.thingsboard.rule.engine.api.TbContext;
|
||||
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.rule.engine.util.EntityContainer;
|
||||
import org.thingsboard.server.common.data.id.AssetId;
|
||||
import org.thingsboard.server.common.data.id.CustomerId;
|
||||
import org.thingsboard.server.common.data.id.DashboardId;
|
||||
import org.thingsboard.server.common.data.id.DeviceId;
|
||||
import org.thingsboard.server.common.data.id.EntityViewId;
|
||||
import org.thingsboard.server.common.data.id.TenantId;
|
||||
import org.thingsboard.server.common.data.plugin.ComponentType;
|
||||
import org.thingsboard.server.common.data.relation.EntityRelation;
|
||||
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
|
||||
import org.thingsboard.server.common.msg.TbMsg;
|
||||
|
||||
@Slf4j
|
||||
@RuleNode(
|
||||
type = ComponentType.ACTION,
|
||||
name = "create relation",
|
||||
configClazz = TbCreateRelationNodeConfiguration.class,
|
||||
nodeDescription = "Finds target Entity by entity name pattern and (entity type pattern for Asset, Device) and then create a relation to Originator Entity by type and direction." +
|
||||
" If Selected entity type: Asset, Device or Customer will create new Entity if it doesn't exist and 'Create new entity if not exists' is set to true.",
|
||||
nodeDetails = "If the relation already exists or successfully created - Message send via <b>Success</b> chain, otherwise <b>Failure</b> chain will be used.",
|
||||
uiResources = {"static/rulenode/rulenode-core-config.js"},
|
||||
configDirective = "tbActionNodeCreateRelationConfig",
|
||||
icon = "add_circle"
|
||||
)
|
||||
public class TbCreateRelationNode extends TbAbstractRelationActionNode<TbCreateRelationNodeConfiguration> {
|
||||
|
||||
@Override
|
||||
protected TbCreateRelationNodeConfiguration loadEntityNodeActionConfig(TbNodeConfiguration configuration) throws TbNodeException {
|
||||
return TbNodeUtils.convert(configuration, TbCreateRelationNodeConfiguration.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean createEntityIfNotExists() {
|
||||
return config.isCreateEntityIfNotExists();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ListenableFuture<Boolean> doProcessEntityRelationAction(TbContext ctx, TbMsg msg, EntityContainer entity) {
|
||||
return createIfAbsent(ctx, msg, entity);
|
||||
}
|
||||
|
||||
private ListenableFuture<Boolean> createIfAbsent(TbContext ctx, TbMsg msg, EntityContainer entityContainer) {
|
||||
processSearchDirection(msg, entityContainer);
|
||||
return Futures.transformAsync(ctx.getRelationService().checkRelation(ctx.getTenantId(), fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON),
|
||||
result -> {
|
||||
if (!result) {
|
||||
return processCreateRelation(ctx, entityContainer);
|
||||
}
|
||||
return Futures.immediateFuture(true);
|
||||
});
|
||||
}
|
||||
|
||||
private ListenableFuture<Boolean> processCreateRelation(TbContext ctx, EntityContainer entityContainer) {
|
||||
switch (entityContainer.getEntityType()) {
|
||||
case ASSET:
|
||||
return processAsset(ctx, entityContainer);
|
||||
case DEVICE:
|
||||
return processDevice(ctx, entityContainer);
|
||||
case CUSTOMER:
|
||||
return processCustomer(ctx, entityContainer);
|
||||
case DASHBOARD:
|
||||
return processDashboard(ctx, entityContainer);
|
||||
case ENTITY_VIEW:
|
||||
return processView(ctx, entityContainer);
|
||||
case TENANT:
|
||||
return processTenant(ctx, entityContainer);
|
||||
}
|
||||
return Futures.immediateFuture(true);
|
||||
}
|
||||
|
||||
private ListenableFuture<Boolean> processView(TbContext ctx, EntityContainer entityContainer) {
|
||||
return Futures.transformAsync(ctx.getEntityViewService().findEntityViewByIdAsync(ctx.getTenantId(), new EntityViewId(entityContainer.getEntityId().getId())), entityView -> {
|
||||
if (entityView != null) {
|
||||
return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON));
|
||||
} else {
|
||||
return Futures.immediateFuture(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ListenableFuture<Boolean> processDevice(TbContext ctx, EntityContainer entityContainer) {
|
||||
return Futures.transformAsync(ctx.getDeviceService().findDeviceByIdAsync(ctx.getTenantId(), new DeviceId(entityContainer.getEntityId().getId())), device -> {
|
||||
if (device != null) {
|
||||
return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON));
|
||||
} else {
|
||||
return Futures.immediateFuture(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ListenableFuture<Boolean> processAsset(TbContext ctx, EntityContainer entityContainer) {
|
||||
return Futures.transformAsync(ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), new AssetId(entityContainer.getEntityId().getId())), asset -> {
|
||||
if (asset != null) {
|
||||
return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON));
|
||||
} else {
|
||||
return Futures.immediateFuture(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ListenableFuture<Boolean> processCustomer(TbContext ctx, EntityContainer entityContainer) {
|
||||
return Futures.transformAsync(ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), new CustomerId(entityContainer.getEntityId().getId())), customer -> {
|
||||
if (customer != null) {
|
||||
return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON));
|
||||
} else {
|
||||
return Futures.immediateFuture(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ListenableFuture<Boolean> processDashboard(TbContext ctx, EntityContainer entityContainer) {
|
||||
return Futures.transformAsync(ctx.getDashboardService().findDashboardByIdAsync(ctx.getTenantId(), new DashboardId(entityContainer.getEntityId().getId())), dashboard -> {
|
||||
if (dashboard != null) {
|
||||
return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON));
|
||||
} else {
|
||||
return Futures.immediateFuture(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ListenableFuture<Boolean> processTenant(TbContext ctx, EntityContainer entityContainer) {
|
||||
return Futures.transformAsync(ctx.getTenantService().findTenantByIdAsync(ctx.getTenantId(), new TenantId(entityContainer.getEntityId().getId())), tenant -> {
|
||||
if (tenant != null) {
|
||||
return ctx.getRelationService().saveRelationAsync(ctx.getTenantId(), new EntityRelation(fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON));
|
||||
} else {
|
||||
return Futures.immediateFuture(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright © 2016-2018 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.action;
|
||||
|
||||
import lombok.Data;
|
||||
import org.thingsboard.rule.engine.api.NodeConfiguration;
|
||||
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
|
||||
|
||||
@Data
|
||||
public class TbCreateRelationNodeConfiguration extends TbAbstractRelationActionNodeConfiguration implements NodeConfiguration<TbCreateRelationNodeConfiguration> {
|
||||
|
||||
private boolean createEntityIfNotExists;
|
||||
|
||||
@Override
|
||||
public TbCreateRelationNodeConfiguration defaultConfiguration() {
|
||||
TbCreateRelationNodeConfiguration configuration = new TbCreateRelationNodeConfiguration();
|
||||
configuration.setDirection(EntitySearchDirection.FROM.name());
|
||||
configuration.setRelationType("Contains");
|
||||
configuration.setEntityNamePattern("");
|
||||
configuration.setEntityCacheExpiration(300);
|
||||
configuration.setCreateEntityIfNotExists(false);
|
||||
return configuration;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright © 2016-2018 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.action;
|
||||
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.thingsboard.rule.engine.api.RuleNode;
|
||||
import org.thingsboard.rule.engine.api.TbContext;
|
||||
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.rule.engine.util.EntityContainer;
|
||||
import org.thingsboard.server.common.data.plugin.ComponentType;
|
||||
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
|
||||
import org.thingsboard.server.common.msg.TbMsg;
|
||||
|
||||
@Slf4j
|
||||
@RuleNode(
|
||||
type = ComponentType.ACTION,
|
||||
name = "delete relation",
|
||||
configClazz = TbDeleteRelationNodeConfiguration.class,
|
||||
nodeDescription = "Finds target Entity by entity name pattern and then delete a relation to Originator Entity by type and direction.",
|
||||
nodeDetails = "If the relation successfully deleted - Message send via <b>Success</b> chain, otherwise <b>Failure</b> chain will be used.",
|
||||
uiResources = {"static/rulenode/rulenode-core-config.js"},
|
||||
configDirective = "tbActionNodeDeleteRelationConfig",
|
||||
icon = "remove_circle"
|
||||
)
|
||||
public class TbDeleteRelationNode extends TbAbstractRelationActionNode<TbDeleteRelationNodeConfiguration> {
|
||||
|
||||
@Override
|
||||
protected TbDeleteRelationNodeConfiguration loadEntityNodeActionConfig(TbNodeConfiguration configuration) throws TbNodeException {
|
||||
return TbNodeUtils.convert(configuration, TbDeleteRelationNodeConfiguration.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean createEntityIfNotExists() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ListenableFuture<Boolean> doProcessEntityRelationAction(TbContext ctx, TbMsg msg, EntityContainer entityContainer) {
|
||||
return deleteIfExist(ctx, msg, entityContainer);
|
||||
}
|
||||
|
||||
private ListenableFuture<Boolean> deleteIfExist(TbContext ctx, TbMsg msg, EntityContainer entityContainer) {
|
||||
processSearchDirection(msg, entityContainer);
|
||||
return Futures.transformAsync(ctx.getRelationService().checkRelation(ctx.getTenantId(), fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON),
|
||||
result -> {
|
||||
if (result) {
|
||||
return processDeleteRelation(ctx);
|
||||
}
|
||||
return Futures.immediateFuture(true);
|
||||
});
|
||||
}
|
||||
|
||||
private ListenableFuture<Boolean> processDeleteRelation(TbContext ctx) {
|
||||
return ctx.getRelationService().deleteRelationAsync(ctx.getTenantId(), fromId, toId, config.getRelationType(), RelationTypeGroup.COMMON);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright © 2016-2018 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.action;
|
||||
|
||||
import lombok.Data;
|
||||
import org.thingsboard.rule.engine.api.NodeConfiguration;
|
||||
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
|
||||
|
||||
@Data
|
||||
public class TbDeleteRelationNodeConfiguration extends TbAbstractRelationActionNodeConfiguration implements NodeConfiguration<TbDeleteRelationNodeConfiguration> {
|
||||
|
||||
|
||||
@Override
|
||||
public TbDeleteRelationNodeConfiguration defaultConfiguration() {
|
||||
TbDeleteRelationNodeConfiguration configuration = new TbDeleteRelationNodeConfiguration();
|
||||
configuration.setDirection(EntitySearchDirection.FROM.name());
|
||||
configuration.setRelationType("Contains");
|
||||
configuration.setEntityNamePattern("");
|
||||
configuration.setEntityCacheExpiration(300);
|
||||
return configuration;
|
||||
}
|
||||
}
|
||||
@ -21,8 +21,15 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.thingsboard.rule.engine.api.*;
|
||||
import org.apache.commons.lang3.math.NumberUtils;
|
||||
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.DonAsynchron;
|
||||
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
|
||||
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
|
||||
@ -37,7 +44,9 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.thingsboard.rule.engine.api.TbRelationTypes.SUCCESS;
|
||||
import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.*;
|
||||
import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.FETCH_MODE_ALL;
|
||||
import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.FETCH_MODE_FIRST;
|
||||
import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfiguration.MAX_FETCH_SIZE;
|
||||
import static org.thingsboard.server.common.data.kv.Aggregation.NONE;
|
||||
|
||||
/**
|
||||
@ -50,6 +59,7 @@ import static org.thingsboard.server.common.data.kv.Aggregation.NONE;
|
||||
nodeDescription = "Add Message Originator Telemetry for selected time range into Message Metadata\n",
|
||||
nodeDetails = "The node allows you to select fetch mode <b>FIRST/LAST/ALL</b> to fetch telemetry of certain time range that are added into Message metadata without any prefix. " +
|
||||
"If selected fetch mode <b>ALL</b> Telemetry will be added like array into Message Metadata where <b>key</b> is Timestamp and <b>value</b> is value of Telemetry. " +
|
||||
"<b>Note</b>: The maximum size of the fetched array is 1000 records. " +
|
||||
"If selected fetch mode <b>FIRST</b> or <b>LAST</b> Telemetry will be added like string without Timestamp",
|
||||
uiResources = {"static/rulenode/rulenode-core-config.js"},
|
||||
configDirective = "tbEnrichmentNodeGetTelemetryFromDatabase")
|
||||
@ -57,8 +67,6 @@ public class TbGetTelemetryNode implements TbNode {
|
||||
|
||||
private TbGetTelemetryNodeConfiguration config;
|
||||
private List<String> tsKeyNames;
|
||||
private long startTsOffset;
|
||||
private long endTsOffset;
|
||||
private int limit;
|
||||
private ObjectMapper mapper;
|
||||
private String fetchMode;
|
||||
@ -67,8 +75,6 @@ public class TbGetTelemetryNode implements TbNode {
|
||||
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
|
||||
this.config = TbNodeUtils.convert(configuration, TbGetTelemetryNodeConfiguration.class);
|
||||
tsKeyNames = config.getLatestTsKeyNames();
|
||||
startTsOffset = TimeUnit.valueOf(config.getStartIntervalTimeUnit()).toMillis(config.getStartInterval());
|
||||
endTsOffset = TimeUnit.valueOf(config.getEndIntervalTimeUnit()).toMillis(config.getEndInterval());
|
||||
limit = config.getFetchMode().equals(FETCH_MODE_ALL) ? MAX_FETCH_SIZE : 1;
|
||||
fetchMode = config.getFetchMode();
|
||||
mapper = new ObjectMapper();
|
||||
@ -82,8 +88,10 @@ public class TbGetTelemetryNode implements TbNode {
|
||||
ctx.tellFailure(msg, new IllegalStateException("Telemetry is not selected!"));
|
||||
} else {
|
||||
try {
|
||||
List<ReadTsKvQuery> queries = buildQueries();
|
||||
ListenableFuture<List<TsKvEntry>> list = ctx.getTimeseriesService().findAll(ctx.getTenantId(), msg.getOriginator(), queries);
|
||||
if (config.isUseMetadataIntervalPatterns()) {
|
||||
checkMetadataKeyPatterns(msg);
|
||||
}
|
||||
ListenableFuture<List<TsKvEntry>> list = ctx.getTimeseriesService().findAll(ctx.getTenantId(), msg.getOriginator(), buildQueries(msg));
|
||||
DonAsynchron.withCallback(list, data -> {
|
||||
process(data, msg);
|
||||
TbMsg newMsg = ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), msg.getData());
|
||||
@ -95,10 +103,12 @@ public class TbGetTelemetryNode implements TbNode {
|
||||
}
|
||||
}
|
||||
|
||||
private List<ReadTsKvQuery> buildQueries() {
|
||||
long ts = System.currentTimeMillis();
|
||||
long startTs = ts - startTsOffset;
|
||||
long endTs = ts - endTsOffset;
|
||||
@Override
|
||||
public void destroy() {
|
||||
|
||||
}
|
||||
|
||||
private List<ReadTsKvQuery> buildQueries(TbMsg msg) {
|
||||
String orderBy;
|
||||
if (fetchMode.equals(FETCH_MODE_FIRST) || fetchMode.equals(FETCH_MODE_ALL)) {
|
||||
orderBy = "ASC";
|
||||
@ -106,7 +116,7 @@ public class TbGetTelemetryNode implements TbNode {
|
||||
orderBy = "DESC";
|
||||
}
|
||||
return tsKeyNames.stream()
|
||||
.map(key -> new BaseReadTsKvQuery(key, startTs, endTs, 1, limit, NONE, orderBy))
|
||||
.map(key -> new BaseReadTsKvQuery(key, getInterval(msg).getStartTs(), getInterval(msg).getEndTs(), 1, limit, NONE, orderBy))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@ -162,8 +172,79 @@ public class TbGetTelemetryNode implements TbNode {
|
||||
return obj;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
|
||||
private Interval getInterval(TbMsg msg) {
|
||||
Interval interval = new Interval();
|
||||
if (config.isUseMetadataIntervalPatterns()) {
|
||||
if (isParsable(msg, config.getStartIntervalPattern())) {
|
||||
interval.setStartTs(Long.parseLong(TbNodeUtils.processPattern(config.getStartIntervalPattern(), msg.getMetaData())));
|
||||
}
|
||||
if (isParsable(msg, config.getEndIntervalPattern())) {
|
||||
interval.setEndTs(Long.parseLong(TbNodeUtils.processPattern(config.getEndIntervalPattern(), msg.getMetaData())));
|
||||
}
|
||||
} else {
|
||||
long ts = System.currentTimeMillis();
|
||||
interval.setStartTs(ts - TimeUnit.valueOf(config.getStartIntervalTimeUnit()).toMillis(config.getStartInterval()));
|
||||
interval.setEndTs(ts - TimeUnit.valueOf(config.getEndIntervalTimeUnit()).toMillis(config.getEndInterval()));
|
||||
}
|
||||
return interval;
|
||||
}
|
||||
|
||||
private boolean isParsable(TbMsg msg, String pattern) {
|
||||
return NumberUtils.isParsable(TbNodeUtils.processPattern(pattern, msg.getMetaData()));
|
||||
}
|
||||
|
||||
private void checkMetadataKeyPatterns(TbMsg msg) {
|
||||
isUndefined(msg, config.getStartIntervalPattern(), config.getEndIntervalPattern());
|
||||
isInvalid(msg, config.getStartIntervalPattern(), config.getEndIntervalPattern());
|
||||
}
|
||||
|
||||
private void isUndefined(TbMsg msg, String startIntervalPattern, String endIntervalPattern) {
|
||||
if (getMetadataValue(msg, startIntervalPattern) == null && getMetadataValue(msg, endIntervalPattern) == null) {
|
||||
throw new IllegalArgumentException("Message metadata values: '" +
|
||||
replaceRegex(startIntervalPattern) + "' and '" +
|
||||
replaceRegex(endIntervalPattern) + "' are undefined");
|
||||
} else {
|
||||
if (getMetadataValue(msg, startIntervalPattern) == null) {
|
||||
throw new IllegalArgumentException("Message metadata value: '" +
|
||||
replaceRegex(startIntervalPattern) + "' is undefined");
|
||||
}
|
||||
if (getMetadataValue(msg, endIntervalPattern) == null) {
|
||||
throw new IllegalArgumentException("Message metadata value: '" +
|
||||
replaceRegex(endIntervalPattern) + "' is undefined");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void isInvalid(TbMsg msg, String startIntervalPattern, String endIntervalPattern) {
|
||||
if (getInterval(msg).getStartTs() == null && getInterval(msg).getEndTs() == null) {
|
||||
throw new IllegalArgumentException("Message metadata values: '" +
|
||||
replaceRegex(startIntervalPattern) + "' and '" +
|
||||
replaceRegex(endIntervalPattern) + "' have invalid format");
|
||||
} else {
|
||||
if (getInterval(msg).getStartTs() == null) {
|
||||
throw new IllegalArgumentException("Message metadata value: '" +
|
||||
replaceRegex(startIntervalPattern) + "' has invalid format");
|
||||
}
|
||||
if (getInterval(msg).getEndTs() == null) {
|
||||
throw new IllegalArgumentException("Message metadata value: '" +
|
||||
replaceRegex(endIntervalPattern) + "' has invalid format");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getMetadataValue(TbMsg msg, String pattern) {
|
||||
return msg.getMetaData().getValue(replaceRegex(pattern));
|
||||
}
|
||||
|
||||
private String replaceRegex(String pattern) {
|
||||
return pattern.replaceAll("[${}]", "");
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
private static class Interval {
|
||||
private Long startTs;
|
||||
private Long endTs;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -35,6 +35,12 @@ public class TbGetTelemetryNodeConfiguration implements NodeConfiguration<TbGetT
|
||||
|
||||
private int startInterval;
|
||||
private int endInterval;
|
||||
|
||||
private String startIntervalPattern;
|
||||
private String endIntervalPattern;
|
||||
|
||||
private boolean useMetadataIntervalPatterns;
|
||||
|
||||
private String startIntervalTimeUnit;
|
||||
private String endIntervalTimeUnit;
|
||||
private String fetchMode; //FIRST, LAST, LATEST
|
||||
@ -52,6 +58,9 @@ public class TbGetTelemetryNodeConfiguration implements NodeConfiguration<TbGetT
|
||||
configuration.setStartInterval(2);
|
||||
configuration.setEndIntervalTimeUnit(TimeUnit.MINUTES.name());
|
||||
configuration.setEndInterval(1);
|
||||
configuration.setUseMetadataIntervalPatterns(false);
|
||||
configuration.setStartIntervalPattern("");
|
||||
configuration.setEndIntervalPattern("");
|
||||
return configuration;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright © 2016-2018 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.util;
|
||||
|
||||
import lombok.Data;
|
||||
import org.thingsboard.server.common.data.EntityType;
|
||||
import org.thingsboard.server.common.data.id.EntityId;
|
||||
|
||||
@Data
|
||||
public class EntityContainer {
|
||||
|
||||
private EntityId entityId;
|
||||
private EntityType entityType;
|
||||
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@ -23,7 +23,6 @@
|
||||
<version>2.3.0-SNAPSHOT</version>
|
||||
<artifactId>thingsboard</artifactId>
|
||||
</parent>
|
||||
<groupId>org.thingsboard</groupId>
|
||||
<artifactId>tools</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
|
||||
@ -36,6 +36,7 @@ import org.thingsboard.server.common.data.id.DeviceId;
|
||||
import org.thingsboard.server.common.data.id.EntityId;
|
||||
import org.thingsboard.server.common.data.relation.EntityRelation;
|
||||
import org.thingsboard.server.common.data.security.DeviceCredentials;
|
||||
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
@ -107,6 +108,27 @@ public class RestClient implements ClientHttpRequestInterceptor {
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<JsonNode> getAttributes(String accessToken, String clientKeys, String sharedKeys) {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("accessToken", accessToken);
|
||||
params.put("clientKeys", clientKeys);
|
||||
params.put("sharedKeys", sharedKeys);
|
||||
try {
|
||||
ResponseEntity<JsonNode> telemetryEntity = restTemplate.getForEntity(baseURL + "/api/v1/{accessToken}/attributes?clientKeys={clientKeys}&sharedKeys={sharedKeys}", JsonNode.class, params);
|
||||
return Optional.of(telemetryEntity.getBody());
|
||||
} catch (HttpClientErrorException exception) {
|
||||
if (exception.getStatusCode() == HttpStatus.NOT_FOUND) {
|
||||
return Optional.empty();
|
||||
} else {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Customer createCustomer(Customer customer) {
|
||||
return restTemplate.postForEntity(baseURL + "/api/customer", customer, Customer.class).getBody();
|
||||
}
|
||||
|
||||
public Customer createCustomer(String title) {
|
||||
Customer customer = new Customer();
|
||||
customer.setTitle(title);
|
||||
@ -120,6 +142,25 @@ public class RestClient implements ClientHttpRequestInterceptor {
|
||||
return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody();
|
||||
}
|
||||
|
||||
public DeviceCredentials updateDeviceCredentials(DeviceId deviceId, String token) {
|
||||
DeviceCredentials deviceCredentials = getCredentials(deviceId);
|
||||
deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN);
|
||||
deviceCredentials.setCredentialsId(token);
|
||||
return saveDeviceCredentials(deviceCredentials);
|
||||
}
|
||||
|
||||
public DeviceCredentials saveDeviceCredentials(DeviceCredentials deviceCredentials) {
|
||||
return restTemplate.postForEntity(baseURL + "/api/device/credentials", deviceCredentials, DeviceCredentials.class).getBody();
|
||||
}
|
||||
|
||||
public Device createDevice(Device device) {
|
||||
return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody();
|
||||
}
|
||||
|
||||
public Asset createAsset(Asset asset) {
|
||||
return restTemplate.postForEntity(baseURL + "/api/asset", asset, Asset.class).getBody();
|
||||
}
|
||||
|
||||
public Asset createAsset(String name, String type) {
|
||||
Asset asset = new Asset();
|
||||
asset.setName(name);
|
||||
@ -131,6 +172,18 @@ public class RestClient implements ClientHttpRequestInterceptor {
|
||||
return restTemplate.postForEntity(baseURL + "/api/alarm", alarm, Alarm.class).getBody();
|
||||
}
|
||||
|
||||
public void deleteCustomer(CustomerId customerId) {
|
||||
restTemplate.delete(baseURL + "/api/customer/{customerId}", customerId);
|
||||
}
|
||||
|
||||
public void deleteDevice(DeviceId deviceId) {
|
||||
restTemplate.delete(baseURL + "/api/device/{deviceId}", deviceId);
|
||||
}
|
||||
|
||||
public void deleteAsset(AssetId assetId) {
|
||||
restTemplate.delete(baseURL + "/api/asset/{assetId}", assetId);
|
||||
}
|
||||
|
||||
public Device assignDevice(CustomerId customerId, DeviceId deviceId) {
|
||||
return restTemplate.postForEntity(baseURL + "/api/customer/{customerId}/device/{deviceId}", null, Device.class,
|
||||
customerId.toString(), deviceId.toString()).getBody();
|
||||
|
||||
@ -648,6 +648,12 @@ export default class Subscription {
|
||||
}
|
||||
|
||||
dataUpdated(sourceData, datasourceIndex, dataKeyIndex, apply) {
|
||||
for (var x = 0; x < this.datasourceListeners.length; x++) {
|
||||
this.datasources[x].dataReceived = this.datasources[x].dataReceived === true;
|
||||
if (this.datasourceListeners[x].datasourceIndex === datasourceIndex && sourceData.data.length > 0) {
|
||||
this.datasources[x].dataReceived = true;
|
||||
}
|
||||
}
|
||||
this.notifyDataLoaded();
|
||||
var update = true;
|
||||
var currentData;
|
||||
|
||||
@ -31,17 +31,38 @@ export default function ExtensionFormModbusDirective($compile, $templateCache, $
|
||||
|
||||
var linker = function(scope, element) {
|
||||
|
||||
|
||||
function Server() {
|
||||
this.transport = {
|
||||
"type": "tcp",
|
||||
"host": "localhost",
|
||||
"port": 502,
|
||||
"timeout": 3000
|
||||
};
|
||||
this.devices = []
|
||||
function TcpTransport() {
|
||||
this.type = "tcp",
|
||||
this.host = "localhost",
|
||||
this.port = 502,
|
||||
this.timeout = 5000,
|
||||
this.reconnect = true,
|
||||
this.rtuOverTcp = false
|
||||
}
|
||||
|
||||
function UdpTransport() {
|
||||
this.type = "udp",
|
||||
this.host = "localhost",
|
||||
this.port = 502,
|
||||
this.timeout = 5000
|
||||
}
|
||||
|
||||
function RtuTransport() {
|
||||
this.type = "rtu",
|
||||
this.portName = "COM1",
|
||||
this.encoding = "ascii",
|
||||
this.timeout = 5000,
|
||||
this.baudRate = 115200,
|
||||
this.dataBits = 7,
|
||||
this.stopBits = 1,
|
||||
this.parity ="even"
|
||||
}
|
||||
|
||||
function Server() {
|
||||
this.transport = new TcpTransport();
|
||||
this.devices = []
|
||||
}
|
||||
|
||||
function Device() {
|
||||
this.unitId = 1;
|
||||
this.deviceName = "";
|
||||
@ -105,9 +126,13 @@ export default function ExtensionFormModbusDirective($compile, $templateCache, $
|
||||
scope.onTransportChanged = function(server) {
|
||||
var type = server.transport.type;
|
||||
|
||||
server.transport = {};
|
||||
server.transport.type = type;
|
||||
server.transport.timeout = 3000;
|
||||
if (type === "tcp") {
|
||||
server.transport = new TcpTransport();
|
||||
} else if (type === "udp") {
|
||||
server.transport = new UdpTransport();
|
||||
} else if (type === "rtu") {
|
||||
server.transport = new RtuTransport();
|
||||
}
|
||||
|
||||
scope.theForm.$setDirty();
|
||||
};
|
||||
|
||||
@ -73,49 +73,69 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div layout="row" ng-if="server.transport.type == 'tcp' || server.transport.type == 'udp'">
|
||||
<md-input-container flex="33" class="md-block">
|
||||
<label translate>extension.host</label>
|
||||
<input required name="transportHost_{{serverIndex}}" ng-model="server.transport.host">
|
||||
<div ng-messages="theForm['transportHost_' + serverIndex].$error">
|
||||
<div translate ng-message="required">extension.field-required</div>
|
||||
</div>
|
||||
</md-input-container>
|
||||
<div ng-if="server.transport.type == 'tcp' || server.transport.type == 'udp'">
|
||||
<div layout="row">
|
||||
<md-input-container flex="33" class="md-block">
|
||||
<label translate>extension.host</label>
|
||||
<input required name="transportHost_{{serverIndex}}" ng-model="server.transport.host">
|
||||
<div ng-messages="theForm['transportHost_' + serverIndex].$error">
|
||||
<div translate ng-message="required">extension.field-required</div>
|
||||
</div>
|
||||
</md-input-container>
|
||||
|
||||
<md-input-container flex="33" class="md-block">
|
||||
<label translate>extension.port</label>
|
||||
<input type="number"
|
||||
required
|
||||
name="transportPort_{{serverIndex}}"
|
||||
ng-model="server.transport.port"
|
||||
min="1"
|
||||
max="65535"
|
||||
>
|
||||
<div ng-messages="theForm['transportPort_' + serverIndex].$error">
|
||||
<div translate
|
||||
ng-message="required"
|
||||
>extension.field-required</div>
|
||||
<div translate
|
||||
ng-message="min"
|
||||
>extension.port-range</div>
|
||||
<div translate
|
||||
ng-message="max"
|
||||
>extension.port-range</div>
|
||||
</div>
|
||||
</md-input-container>
|
||||
|
||||
<md-input-container flex="33" class="md-block">
|
||||
<label translate>extension.timeout</label>
|
||||
<input type="number"
|
||||
required name="transportTimeout_{{serverIndex}}"
|
||||
ng-model="server.transport.timeout"
|
||||
>
|
||||
<div ng-messages="theForm['transportTimeout_' + serverIndex].$error">
|
||||
<div translate
|
||||
ng-message="required"
|
||||
>extension.field-required</div>
|
||||
</div>
|
||||
</md-input-container>
|
||||
<md-input-container flex="33" class="md-block">
|
||||
<label translate>extension.port</label>
|
||||
<input type="number"
|
||||
required
|
||||
name="transportPort_{{serverIndex}}"
|
||||
ng-model="server.transport.port"
|
||||
min="1"
|
||||
max="65535"
|
||||
>
|
||||
<div ng-messages="theForm['transportPort_' + serverIndex].$error">
|
||||
<div translate
|
||||
ng-message="required"
|
||||
>extension.field-required</div>
|
||||
<div translate
|
||||
ng-message="min"
|
||||
>extension.port-range</div>
|
||||
<div translate
|
||||
ng-message="max"
|
||||
>extension.port-range</div>
|
||||
</div>
|
||||
</md-input-container>
|
||||
|
||||
<md-input-container flex="33" class="md-block">
|
||||
<label translate>extension.timeout</label>
|
||||
<input type="number"
|
||||
required name="transportTimeout_{{serverIndex}}"
|
||||
ng-model="server.transport.timeout"
|
||||
>
|
||||
<div ng-messages="theForm['transportTimeout_' + serverIndex].$error">
|
||||
<div translate
|
||||
ng-message="required"
|
||||
>extension.field-required</div>
|
||||
</div>
|
||||
</md-input-container>
|
||||
</div>
|
||||
|
||||
<div layout="row" ng-if="server.transport.type == 'tcp'">
|
||||
<md-input-container flex="50" class="md-block">
|
||||
<md-checkbox aria-label="{{ 'extension.modbus-tcp-reconnect' | translate }}"
|
||||
ng-checked="server.transport.reconnect"
|
||||
name="transportTcpReconnect_{{serverIndex}}"
|
||||
ng-model="server.transport.reconnect">{{ 'extension.modbus-tcp-reconnect' | translate }}
|
||||
</md-checkbox>
|
||||
</md-input-container>
|
||||
|
||||
<md-input-container flex="50" class="md-block">
|
||||
<md-checkbox aria-label="{{ 'extension.modbus-rtu-over-tcp' | translate }}"
|
||||
ng-checked="server.transport.rtuOverTcp"
|
||||
name="transportRtuOverTcp_{{serverIndex}}"
|
||||
ng-model="server.transport.rtuOverTcp">{{ 'extension.modbus-rtu-over-tcp' | translate }}
|
||||
</md-checkbox>
|
||||
</md-input-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="server.transport.type == 'rtu'">
|
||||
|
||||
@ -261,7 +261,7 @@
|
||||
</md-input-container>
|
||||
<md-input-container flex="60" class="md-block" md-is-error="theForm['mqttFilterExpression' + brokerIndex + mapIndex].$touched && theForm['mqttFilterExpression' + brokerIndex + mapIndex].$invalid">
|
||||
<label translate>extension.filter-expression</label>
|
||||
<input required name="mqttFilterExpression{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.filterExpression">
|
||||
<input name="mqttFilterExpression{{brokerIndex}}{{mapIndex}}" ng-model="map.converter.filterExpression">
|
||||
<div ng-messages="theForm['mqttFilterExpression' + brokerIndex + mapIndex].$error">
|
||||
<div translate ng-message="required">extension.field-required</div>
|
||||
</div>
|
||||
|
||||
@ -1008,6 +1008,8 @@
|
||||
"modbus-add-server": "Add server/slave",
|
||||
"modbus-add-server-prompt": "Please add server/slave",
|
||||
"modbus-transport": "Transport",
|
||||
"modbus-tcp-reconnect": "Automatically reconnect",
|
||||
"modbus-rtu-over-tcp": "RTU over TCP",
|
||||
"modbus-port-name": "Serial port name",
|
||||
"modbus-encoding": "Encoding",
|
||||
"modbus-parity": "Parity",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -60,6 +60,9 @@ export default class TbMapWidgetV2 {
|
||||
|
||||
var minZoomLevel = this.drawRoutes ? 18 : 15;
|
||||
|
||||
|
||||
|
||||
|
||||
var initCallback = function() {
|
||||
tbMap.update();
|
||||
tbMap.resize();
|
||||
@ -87,6 +90,9 @@ export default class TbMapWidgetV2 {
|
||||
} else if (mapProvider === 'tencent-map') {
|
||||
this.map = new TbTencentMap($element,this.utils, initCallback, this.defaultZoomLevel, this.dontFitMapBounds, minZoomLevel, settings.tmApiKey, settings.tmDefaultMapType);
|
||||
}
|
||||
|
||||
|
||||
tbMap.initBounds = true;
|
||||
}
|
||||
|
||||
setCallbacks(callbacks) {
|
||||
@ -442,6 +448,7 @@ export default class TbMapWidgetV2 {
|
||||
}
|
||||
|
||||
function updateLocations(data, datasources) {
|
||||
|
||||
var locationsChanged = false;
|
||||
var bounds = tbMap.map.createBounds();
|
||||
var dataMap = toLabelValueMap(data, datasources);
|
||||
@ -454,9 +461,13 @@ export default class TbMapWidgetV2 {
|
||||
tbMap.map.extendBoundsWithMarker(bounds, location.marker);
|
||||
}
|
||||
}
|
||||
if (locationsChanged) {
|
||||
if (locationsChanged && tbMap.initBounds) {
|
||||
tbMap.initBounds = !datasources.every(
|
||||
function (ds) {
|
||||
return ds.dataReceived === true;
|
||||
});
|
||||
tbMap.map.fitBounds(bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createTooltipContent(tooltip, data, datasources) {
|
||||
@ -477,7 +488,6 @@ export default class TbMapWidgetV2 {
|
||||
content = fillPattern(settings.tooltipPattern, settings.tooltipReplaceInfo, data);
|
||||
return fillPatternWithActions(content, 'onTooltipAction', tooltip.markerArgs);
|
||||
}
|
||||
|
||||
if (this.map && this.map.inited() && this.subscription) {
|
||||
if (this.subscription.data) {
|
||||
if (!this.locations) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user