Browse Source

Merge branch 'master' of https://github.com/thingsboard/thingsboard

# Conflicts:
#	ui/src/app/locale/locale.constant-zh.js

Signed-off-by: charlie <charlie_weng@163.com>
pull/874/head
charlie 8 years ago
parent
commit
3121d47489
  1. 2
      application/pom.xml
  2. 2
      application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
  3. 3
      application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
  4. 4
      application/src/main/java/org/thingsboard/server/actors/shared/rulechain/TenantRuleChainManager.java
  5. 7
      application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java
  6. 25
      application/src/main/java/org/thingsboard/server/service/install/DefaultDataUpdateService.java
  7. 171
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  8. 4
      application/src/main/java/org/thingsboard/server/service/state/DeviceStateService.java
  9. 2
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
  10. 12
      application/src/main/proto/cluster.proto
  11. 1
      application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java
  12. 2
      common/data/pom.xml
  13. 1
      common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java
  14. 2
      common/message/pom.xml
  15. 2
      common/pom.xml
  16. 2
      common/transport/pom.xml
  17. 2
      dao/pom.xml
  18. 4
      netty-mqtt/pom.xml
  19. 59
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java
  20. 20
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClient.java
  21. 10
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientCallback.java
  22. 161
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java
  23. 4
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingSubscription.java
  24. 4
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingUnsubscription.java
  25. 6
      netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscription.java
  26. 2
      pom.xml
  27. 2
      rule-engine/pom.xml
  28. 2
      rule-engine/rule-engine-api/pom.xml
  29. 2
      rule-engine/rule-engine-components/pom.xml
  30. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java
  31. 14
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java
  32. 2
      tools/pom.xml
  33. 2
      transport/coap/pom.xml
  34. 2
      transport/http/pom.xml
  35. 2
      transport/mqtt/pom.xml
  36. 1
      transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java
  37. 2
      transport/pom.xml
  38. 2
      ui/package.json
  39. 2
      ui/pom.xml
  40. 1957
      ui/src/app/locale/locale.constant-es.js
  41. 773
      ui/src/app/locale/locale.constant-ko.js
  42. 760
      ui/src/app/locale/locale.constant-ru.js
  43. 38
      ui/src/app/locale/locale.constant.js

2
application/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>2.0.1-SNAPSHOT</version> <version>2.0.2-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<artifactId>application</artifactId> <artifactId>application</artifactId>

2
application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java

@ -294,7 +294,7 @@ public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcesso
private void handleGetAttributesRequest(DeviceToDeviceActorMsg src) { private void handleGetAttributesRequest(DeviceToDeviceActorMsg src) {
GetAttributesRequest request = (GetAttributesRequest) src.getPayload(); GetAttributesRequest request = (GetAttributesRequest) src.getPayload();
ListenableFuture<List<AttributeKvEntry>> clientAttributesFuture = getAttributeKvEntries(deviceId, DataConstants.CLIENT_SCOPE, request.getClientAttributeNames()); ListenableFuture<List<AttributeKvEntry>> clientAttributesFuture = getAttributeKvEntries(deviceId, DataConstants.CLIENT_SCOPE, request.getClientAttributeNames());
ListenableFuture<List<AttributeKvEntry>> sharedAttributesFuture = getAttributeKvEntries(deviceId, DataConstants.SHARED_SCOPE, request.getClientAttributeNames()); ListenableFuture<List<AttributeKvEntry>> sharedAttributesFuture = getAttributeKvEntries(deviceId, DataConstants.SHARED_SCOPE, request.getSharedAttributeNames());
Futures.addCallback(Futures.allAsList(Arrays.asList(clientAttributesFuture, sharedAttributesFuture)), new FutureCallback<List<List<AttributeKvEntry>>>() { Futures.addCallback(Futures.allAsList(Arrays.asList(clientAttributesFuture, sharedAttributesFuture)), new FutureCallback<List<List<AttributeKvEntry>>>() {
@Override @Override

3
application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java

@ -241,6 +241,9 @@ public class DefaultActorService implements ActorService {
case CLUSTER_RPC_FROM_DEVICE_RESPONSE_MESSAGE: case CLUSTER_RPC_FROM_DEVICE_RESPONSE_MESSAGE:
actorContext.getDeviceRpcService().processRemoteResponseFromDevice(serverAddress, msg.getPayload().toByteArray()); actorContext.getDeviceRpcService().processRemoteResponseFromDevice(serverAddress, msg.getPayload().toByteArray());
break; break;
case CLUSTER_DEVICE_STATE_SERVICE_MESSAGE:
actorContext.getDeviceStateService().onRemoteMsg(serverAddress, msg.getPayload().toByteArray());
break;
} }
} }

4
application/src/main/java/org/thingsboard/server/actors/shared/rulechain/TenantRuleChainManager.java

@ -33,9 +33,7 @@ public class TenantRuleChainManager extends RuleChainManager {
@Override @Override
public void init(ActorContext context) { public void init(ActorContext context) {
if (systemContext.isTenantComponentsInitEnabled()) { super.init(context);
super.init(context);
}
} }
@Override @Override

7
application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java

@ -36,6 +36,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.thingsboard.server.common.msg.cluster.ServerAddress; import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import org.thingsboard.server.utils.MiscUtils; import org.thingsboard.server.utils.MiscUtils;
@ -74,6 +75,10 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
@Lazy @Lazy
private TelemetrySubscriptionService tsSubService; private TelemetrySubscriptionService tsSubService;
@Autowired
@Lazy
private DeviceStateService deviceStateService;
private final List<DiscoveryServiceListener> listeners = new CopyOnWriteArrayList<>(); private final List<DiscoveryServiceListener> listeners = new CopyOnWriteArrayList<>();
private CuratorFramework client; private CuratorFramework client;
@ -203,6 +208,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
switch (pathChildrenCacheEvent.getType()) { switch (pathChildrenCacheEvent.getType()) {
case CHILD_ADDED: case CHILD_ADDED:
tsSubService.onClusterUpdate(); tsSubService.onClusterUpdate();
deviceStateService.onClusterUpdate();
listeners.forEach(listener -> listener.onServerAdded(instance)); listeners.forEach(listener -> listener.onServerAdded(instance));
break; break;
case CHILD_UPDATED: case CHILD_UPDATED:
@ -210,6 +216,7 @@ public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheLi
break; break;
case CHILD_REMOVED: case CHILD_REMOVED:
tsSubService.onClusterUpdate(); tsSubService.onClusterUpdate();
deviceStateService.onClusterUpdate();
listeners.forEach(listener -> listener.onServerRemoved(instance)); listeners.forEach(listener -> listener.onServerRemoved(instance));
break; break;
default: default:

25
application/src/main/java/org/thingsboard/server/service/install/DefaultDataUpdateService.java

@ -19,16 +19,15 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.SearchTextBased;
import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.id.IdBased; import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.page.TextPageData;
import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import java.util.List;
import java.util.UUID;
@Service @Service
@Profile("install") @Profile("install")
@Slf4j @Slf4j
@ -59,8 +58,8 @@ public class DefaultDataUpdateService implements DataUpdateService {
new PaginatedUpdater<String, Tenant>() { new PaginatedUpdater<String, Tenant>() {
@Override @Override
protected List<Tenant> findEntities(String region, TextPageLink pageLink) { protected TextPageData<Tenant> findEntities(String region, TextPageLink pageLink) {
return tenantService.findTenants(pageLink).getData(); return tenantService.findTenants(pageLink);
} }
@Override @Override
@ -76,7 +75,7 @@ public class DefaultDataUpdateService implements DataUpdateService {
} }
}; };
public abstract class PaginatedUpdater<I, D extends IdBased<?>> { public abstract class PaginatedUpdater<I, D extends SearchTextBased<? extends UUIDBased>> {
private static final int DEFAULT_LIMIT = 100; private static final int DEFAULT_LIMIT = 100;
@ -84,20 +83,18 @@ public class DefaultDataUpdateService implements DataUpdateService {
TextPageLink pageLink = new TextPageLink(DEFAULT_LIMIT); TextPageLink pageLink = new TextPageLink(DEFAULT_LIMIT);
boolean hasNext = true; boolean hasNext = true;
while (hasNext) { while (hasNext) {
List<D> entities = findEntities(id, pageLink); TextPageData<D> entities = findEntities(id, pageLink);
for (D entity : entities) { for (D entity : entities.getData()) {
updateEntity(entity); updateEntity(entity);
} }
hasNext = entities.size() == pageLink.getLimit(); hasNext = entities.hasNext();
if (hasNext) { if (hasNext) {
int index = entities.size() - 1; pageLink = entities.getNextPageLink();
UUID idOffset = entities.get(index).getUuidId();
pageLink.setIdOffset(idOffset);
} }
} }
} }
protected abstract List<D> findEntities(I id, TextPageLink pageLink); protected abstract TextPageData<D> findEntities(I id, TextPageLink pageLink);
protected abstract void updateEntity(D entity); protected abstract void updateEntity(D entity);

171
application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java

@ -23,11 +23,13 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningScheduledExecutorService; import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
import com.google.protobuf.InvalidProtocolBufferException;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.RpcError;
import org.thingsboard.server.actors.service.ActorService; import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
@ -36,13 +38,19 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry; import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.page.TextPageLink; import org.thingsboard.server.common.data.page.TextPageLink;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg; import org.thingsboard.server.common.msg.system.ServiceToRuleEngineMsg;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
import org.thingsboard.server.service.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -54,6 +62,7 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -98,6 +107,12 @@ public class DefaultDeviceStateService implements DeviceStateService {
@Autowired @Autowired
private TelemetrySubscriptionService tsSubService; private TelemetrySubscriptionService tsSubService;
@Autowired
private ClusterRoutingService routingService;
@Autowired
private ClusterRpcService clusterRpcService;
@Value("${state.defaultInactivityTimeoutInSec}") @Value("${state.defaultInactivityTimeoutInSec}")
@Getter @Getter
private long defaultInactivityTimeoutInSec; private long defaultInactivityTimeoutInSec;
@ -172,12 +187,57 @@ public class DefaultDeviceStateService implements DeviceStateService {
} }
@Override @Override
public Optional<DeviceState> getDeviceState(DeviceId deviceId) { public void onClusterUpdate() {
DeviceStateData state = deviceStates.get(deviceId); queueExecutor.submit(this::onClusterUpdateSync);
if (state != null) { }
return Optional.of(state.getState());
@Override
public void onRemoteMsg(ServerAddress serverAddress, byte[] data) {
ClusterAPIProtos.DeviceStateServiceMsgProto proto;
try {
proto = ClusterAPIProtos.DeviceStateServiceMsgProto.parseFrom(data);
} catch (InvalidProtocolBufferException e) {
throw new RuntimeException(e);
}
TenantId tenantId = new TenantId(new UUID(proto.getTenantIdMSB(), proto.getTenantIdLSB()));
DeviceId deviceId = new DeviceId(new UUID(proto.getDeviceIdMSB(), proto.getDeviceIdLSB()));
if (proto.getDeleted()) {
queueExecutor.submit(() -> onDeviceDeleted(tenantId, deviceId));
} else { } else {
return Optional.empty(); Device device = deviceService.findDeviceById(deviceId);
if (device != null) {
if (proto.getAdded()) {
onDeviceAdded(device);
} else if (proto.getUpdated()) {
onDeviceUpdated(device);
}
}
}
}
private void onClusterUpdateSync() {
List<Tenant> tenants = tenantService.findTenants(new TextPageLink(Integer.MAX_VALUE)).getData();
for (Tenant tenant : tenants) {
List<ListenableFuture<DeviceStateData>> fetchFutures = new ArrayList<>();
List<Device> devices = deviceService.findDevicesByTenantId(tenant.getId(), new TextPageLink(Integer.MAX_VALUE)).getData();
for (Device device : devices) {
if (!routingService.resolveById(device.getId()).isPresent()) {
if (!deviceStates.containsKey(device.getId())) {
fetchFutures.add(fetchDeviceState(device));
}
} else {
Set<DeviceId> tenantDeviceSet = tenantDevices.get(tenant.getId());
if (tenantDeviceSet != null) {
tenantDeviceSet.remove(device.getId());
}
deviceStates.remove(device.getId());
}
}
try {
Futures.successfulAsList(fetchFutures).get().forEach(this::addDeviceUsingState);
} catch (InterruptedException | ExecutionException e) {
log.warn("Failed to init device state service from DB", e);
}
} }
} }
@ -187,7 +247,9 @@ public class DefaultDeviceStateService implements DeviceStateService {
List<ListenableFuture<DeviceStateData>> fetchFutures = new ArrayList<>(); List<ListenableFuture<DeviceStateData>> fetchFutures = new ArrayList<>();
List<Device> devices = deviceService.findDevicesByTenantId(tenant.getId(), new TextPageLink(Integer.MAX_VALUE)).getData(); List<Device> devices = deviceService.findDevicesByTenantId(tenant.getId(), new TextPageLink(Integer.MAX_VALUE)).getData();
for (Device device : devices) { for (Device device : devices) {
fetchFutures.add(fetchDeviceState(device)); if (!routingService.resolveById(device.getId()).isPresent()) {
fetchFutures.add(fetchDeviceState(device));
}
} }
try { try {
Futures.successfulAsList(fetchFutures).get().forEach(this::addDeviceUsingState); Futures.successfulAsList(fetchFutures).get().forEach(this::addDeviceUsingState);
@ -209,7 +271,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
DeviceStateData stateData = deviceStates.get(deviceId); DeviceStateData stateData = deviceStates.get(deviceId);
DeviceState state = stateData.getState(); DeviceState state = stateData.getState();
state.setActive(ts < state.getLastActivityTime() + state.getInactivityTimeout()); state.setActive(ts < state.getLastActivityTime() + state.getInactivityTimeout());
if (!state.isActive() && state.getLastInactivityAlarmTime() < state.getLastActivityTime()) { if (!state.isActive() && (state.getLastInactivityAlarmTime() == 0L || state.getLastInactivityAlarmTime() < state.getLastActivityTime())) {
state.setLastInactivityAlarmTime(ts); state.setLastInactivityAlarmTime(ts);
pushRuleEngineMessage(stateData, INACTIVITY_EVENT); pushRuleEngineMessage(stateData, INACTIVITY_EVENT);
saveAttribute(deviceId, INACTIVITY_ALARM_TIME, ts); saveAttribute(deviceId, INACTIVITY_ALARM_TIME, ts);
@ -219,7 +281,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
} }
private void onDeviceConnectSync(DeviceId deviceId) { private void onDeviceConnectSync(DeviceId deviceId) {
DeviceStateData stateData = deviceStates.get(deviceId); DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
if (stateData != null) { if (stateData != null) {
long ts = System.currentTimeMillis(); long ts = System.currentTimeMillis();
stateData.getState().setLastConnectTime(ts); stateData.getState().setLastConnectTime(ts);
@ -229,7 +291,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
} }
private void onDeviceDisconnectSync(DeviceId deviceId) { private void onDeviceDisconnectSync(DeviceId deviceId) {
DeviceStateData stateData = deviceStates.get(deviceId); DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
if (stateData != null) { if (stateData != null) {
long ts = System.currentTimeMillis(); long ts = System.currentTimeMillis();
stateData.getState().setLastDisconnectTime(ts); stateData.getState().setLastDisconnectTime(ts);
@ -239,7 +301,7 @@ public class DefaultDeviceStateService implements DeviceStateService {
} }
private void onDeviceActivitySync(DeviceId deviceId) { private void onDeviceActivitySync(DeviceId deviceId) {
DeviceStateData stateData = deviceStates.get(deviceId); DeviceStateData stateData = getOrFetchDeviceStateData(deviceId);
if (stateData != null) { if (stateData != null) {
DeviceState state = stateData.getState(); DeviceState state = stateData.getState();
long ts = System.currentTimeMillis(); long ts = System.currentTimeMillis();
@ -251,6 +313,23 @@ public class DefaultDeviceStateService implements DeviceStateService {
} }
} }
private DeviceStateData getOrFetchDeviceStateData(DeviceId deviceId) {
DeviceStateData deviceStateData = deviceStates.get(deviceId);
if (deviceStateData == null) {
if (!routingService.resolveById(deviceId).isPresent()) {
Device device = deviceService.findDeviceById(deviceId);
if (device != null) {
try {
deviceStateData = fetchDeviceState(device).get();
} catch (InterruptedException | ExecutionException e) {
log.debug("[{}] Failed to fetch device state!", deviceId, e);
}
}
}
}
return deviceStateData;
}
private void onInactivityTimeoutUpdate(DeviceId deviceId, long inactivityTimeout) { private void onInactivityTimeoutUpdate(DeviceId deviceId, long inactivityTimeout) {
if (inactivityTimeout == 0L) { if (inactivityTimeout == 0L) {
return; return;
@ -269,37 +348,65 @@ public class DefaultDeviceStateService implements DeviceStateService {
} }
private void onDeviceAddedSync(Device device) { private void onDeviceAddedSync(Device device) {
Futures.addCallback(fetchDeviceState(device), new FutureCallback<DeviceStateData>() { Optional<ServerAddress> address = routingService.resolveById(device.getId());
@Override if (!address.isPresent()) {
public void onSuccess(@Nullable DeviceStateData state) { Futures.addCallback(fetchDeviceState(device), new FutureCallback<DeviceStateData>() {
addDeviceUsingState(state); @Override
} public void onSuccess(@Nullable DeviceStateData state) {
addDeviceUsingState(state);
}
@Override
public void onFailure(Throwable t) {
log.warn("Failed to register device to the state service", t);
}
});
} else {
sendDeviceEvent(device.getTenantId(), device.getId(), address.get(), true, false, false);
}
}
@Override private void sendDeviceEvent(TenantId tenantId, DeviceId deviceId, ServerAddress address, boolean added, boolean updated, boolean deleted) {
public void onFailure(Throwable t) { log.trace("[{}][{}] Device is monitored on other server: {}", tenantId, deviceId, address);
log.warn("Failed to register device to the state service", t); ClusterAPIProtos.DeviceStateServiceMsgProto.Builder builder = ClusterAPIProtos.DeviceStateServiceMsgProto.newBuilder();
} builder.setTenantIdMSB(tenantId.getId().getMostSignificantBits());
}); builder.setTenantIdLSB(tenantId.getId().getLeastSignificantBits());
builder.setDeviceIdMSB(deviceId.getId().getMostSignificantBits());
builder.setDeviceIdLSB(deviceId.getId().getLeastSignificantBits());
builder.setAdded(added);
builder.setUpdated(updated);
builder.setDeleted(deleted);
clusterRpcService.tell(address, ClusterAPIProtos.MessageType.CLUSTER_DEVICE_STATE_SERVICE_MESSAGE, builder.build().toByteArray());
} }
private void onDeviceUpdatedSync(Device device) { private void onDeviceUpdatedSync(Device device) {
DeviceStateData stateData = deviceStates.get(device.getId()); Optional<ServerAddress> address = routingService.resolveById(device.getId());
if (stateData != null) { if (!address.isPresent()) {
TbMsgMetaData md = new TbMsgMetaData(); DeviceStateData stateData = getOrFetchDeviceStateData(device.getId());
md.putValue("deviceName", device.getName()); if (stateData != null) {
md.putValue("deviceType", device.getType()); TbMsgMetaData md = new TbMsgMetaData();
stateData.setMetaData(md); md.putValue("deviceName", device.getName());
md.putValue("deviceType", device.getType());
stateData.setMetaData(md);
}
} else {
sendDeviceEvent(device.getTenantId(), device.getId(), address.get(), false, true, false);
} }
} }
private void onDeviceDeleted(TenantId tenantId, DeviceId deviceId) { private void onDeviceDeleted(TenantId tenantId, DeviceId deviceId) {
deviceStates.remove(deviceId); Optional<ServerAddress> address = routingService.resolveById(deviceId);
Set<DeviceId> deviceIds = tenantDevices.get(tenantId); if (!address.isPresent()) {
if (deviceIds != null) { deviceStates.remove(deviceId);
deviceIds.remove(deviceId); Set<DeviceId> deviceIds = tenantDevices.get(tenantId);
if (deviceIds.isEmpty()) { if (deviceIds != null) {
tenantDevices.remove(tenantId); deviceIds.remove(deviceId);
if (deviceIds.isEmpty()) {
tenantDevices.remove(tenantId);
}
} }
} else {
sendDeviceEvent(tenantId, deviceId, address.get(), false, false, true);
} }
} }

4
application/src/main/java/org/thingsboard/server/service/state/DeviceStateService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.service.state;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.msg.cluster.ServerAddress;
import java.util.Optional; import java.util.Optional;
@ -39,6 +40,7 @@ public interface DeviceStateService {
void onDeviceInactivityTimeoutUpdate(DeviceId deviceId, long inactivityTimeout); void onDeviceInactivityTimeoutUpdate(DeviceId deviceId, long inactivityTimeout);
Optional<DeviceState> getDeviceState(DeviceId deviceId); void onClusterUpdate();
void onRemoteMsg(ServerAddress serverAddress, byte[] bytes);
} }

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

@ -362,7 +362,7 @@ public class DefaultTelemetrySubscriptionService implements TelemetrySubscriptio
DonAsynchron.withCallback(tsService.findAll(entityId, queries), DonAsynchron.withCallback(tsService.findAll(entityId, queries),
missedUpdates -> { missedUpdates -> {
if (!missedUpdates.isEmpty()) { if (missedUpdates != null && !missedUpdates.isEmpty()) {
tellRemoteSubUpdate(address, sessionId, new SubscriptionUpdate(subscription.getSubscriptionId(), missedUpdates)); tellRemoteSubUpdate(address, sessionId, new SubscriptionUpdate(subscription.getSubscriptionId(), missedUpdates));
} }
}, },

12
application/src/main/proto/cluster.proto

@ -57,6 +57,8 @@ enum MessageType {
CLUSTER_TELEMETRY_ATTR_UPDATE_MESSAGE = 10; CLUSTER_TELEMETRY_ATTR_UPDATE_MESSAGE = 10;
CLUSTER_TELEMETRY_TS_UPDATE_MESSAGE = 11; CLUSTER_TELEMETRY_TS_UPDATE_MESSAGE = 11;
CLUSTER_RPC_FROM_DEVICE_RESPONSE_MESSAGE = 12; CLUSTER_RPC_FROM_DEVICE_RESPONSE_MESSAGE = 12;
CLUSTER_DEVICE_STATE_SERVICE_MESSAGE = 13;
} }
// Messages related to CLUSTER_TELEMETRY_MESSAGE // Messages related to CLUSTER_TELEMETRY_MESSAGE
@ -128,3 +130,13 @@ message FromDeviceRPCResponseProto {
string response = 3; string response = 3;
int32 error = 4; int32 error = 4;
} }
message DeviceStateServiceMsgProto {
int64 tenantIdMSB = 1;
int64 tenantIdLSB = 2;
int64 deviceIdMSB = 3;
int64 deviceIdLSB = 4;
bool added = 5;
bool updated = 6;
bool deleted = 7;
}

1
application/src/test/java/org/thingsboard/server/system/BaseHttpDeviceApiTest.java

@ -64,6 +64,7 @@ public abstract class BaseHttpDeviceApiTest extends AbstractControllerTest {
mockMvc.perform( mockMvc.perform(
asyncDispatch(doPost("/api/v1/" + deviceCredentials.getCredentialsId() + "/attributes", attrMap, new String[]{}).andReturn())) asyncDispatch(doPost("/api/v1/" + deviceCredentials.getCredentialsId() + "/attributes", attrMap, new String[]{}).andReturn()))
.andExpect(status().isOk()); .andExpect(status().isOk());
Thread.sleep(2000);
doGetAsync("/api/v1/" + deviceCredentials.getCredentialsId() + "/attributes?clientKeys=keyA,keyB,keyC").andExpect(status().isOk()); doGetAsync("/api/v1/" + deviceCredentials.getCredentialsId() + "/attributes?clientKeys=keyA,keyB,keyC").andExpect(status().isOk());
} }

2
common/data/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>2.0.1-SNAPSHOT</version> <version>2.0.2-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

1
common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java

@ -98,6 +98,7 @@ public class DashboardInfo extends SearchTextBased<DashboardId> implements HasNa
public boolean updateAssignedCustomer(Customer customer) { public boolean updateAssignedCustomer(Customer customer) {
ShortCustomerInfo customerInfo = customer.toShortCustomerInfo(); ShortCustomerInfo customerInfo = customer.toShortCustomerInfo();
if (this.assignedCustomers != null && this.assignedCustomers.contains(customerInfo)) { if (this.assignedCustomers != null && this.assignedCustomers.contains(customerInfo)) {
this.assignedCustomers.remove(customerInfo);
this.assignedCustomers.add(customerInfo); this.assignedCustomers.add(customerInfo);
return true; return true;
} else { } else {

2
common/message/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>2.0.1-SNAPSHOT</version> <version>2.0.2-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

2
common/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>2.0.1-SNAPSHOT</version> <version>2.0.2-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>

2
common/transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>2.0.1-SNAPSHOT</version> <version>2.0.2-SNAPSHOT</version>
<artifactId>common</artifactId> <artifactId>common</artifactId>
</parent> </parent>
<groupId>org.thingsboard.common</groupId> <groupId>org.thingsboard.common</groupId>

2
dao/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>2.0.1-SNAPSHOT</version> <version>2.0.2-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<artifactId>dao</artifactId> <artifactId>dao</artifactId>

4
netty-mqtt/pom.xml

@ -19,12 +19,12 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>2.0.1-SNAPSHOT</version> <version>2.0.2-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<artifactId>netty-mqtt</artifactId> <artifactId>netty-mqtt</artifactId>
<version>2.0.1-SNAPSHOT</version> <version>2.0.2-SNAPSHOT</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>Netty MQTT Client</name> <name>Netty MQTT Client</name>

59
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttChannelHandler.java

@ -98,33 +98,25 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage>
} }
private void invokeHandlersForIncomingPublish(MqttPublishMessage message) { private void invokeHandlersForIncomingPublish(MqttPublishMessage message) {
for (MqttSubscribtion subscribtion : ImmutableSet.copyOf(this.client.getSubscriptions().values())) { boolean handlerInvoked = false;
if (subscribtion.matches(message.variableHeader().topicName())) { for (MqttSubscription subscription : ImmutableSet.copyOf(this.client.getSubscriptions().values())) {
if (subscribtion.isOnce() && subscribtion.isCalled()) { if (subscription.matches(message.variableHeader().topicName())) {
if (subscription.isOnce() && subscription.isCalled()) {
continue; continue;
} }
message.payload().markReaderIndex(); message.payload().markReaderIndex();
subscribtion.setCalled(true); subscription.setCalled(true);
subscribtion.getHandler().onMessage(message.variableHeader().topicName(), message.payload()); subscription.getHandler().onMessage(message.variableHeader().topicName(), message.payload());
if (subscribtion.isOnce()) { if (subscription.isOnce()) {
this.client.off(subscribtion.getTopic(), subscribtion.getHandler()); this.client.off(subscription.getTopic(), subscription.getHandler());
} }
message.payload().resetReaderIndex(); message.payload().resetReaderIndex();
handlerInvoked = true;
} }
} }
/*Set<MqttSubscribtion> subscribtions = ImmutableSet.copyOf(this.client.getSubscriptions().get(message.variableHeader().topicName())); if (!handlerInvoked && client.getDefaultHandler() != null) {
for (MqttSubscribtion subscribtion : subscribtions) { client.getDefaultHandler().onMessage(message.variableHeader().topicName(), message.payload());
if(subscribtion.isOnce() && subscribtion.isCalled()){ }
continue;
}
message.payload().markReaderIndex();
subscribtion.setCalled(true);
subscribtion.getHandler().onMessage(message.variableHeader().topicName(), message.payload());
if(subscribtion.isOnce()){
this.client.off(subscribtion.getTopic(), subscribtion.getHandler());
}
message.payload().resetReaderIndex();
}*/
message.payload().release(); message.payload().release();
} }
@ -133,7 +125,7 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage>
case CONNECTION_ACCEPTED: case CONNECTION_ACCEPTED:
this.connectFuture.setSuccess(new MqttConnectResult(true, MqttConnectReturnCode.CONNECTION_ACCEPTED, channel.closeFuture())); this.connectFuture.setSuccess(new MqttConnectResult(true, MqttConnectReturnCode.CONNECTION_ACCEPTED, channel.closeFuture()));
this.client.getPendingSubscribtions().entrySet().stream().filter((e) -> !e.getValue().isSent()).forEach((e) -> { this.client.getPendingSubscriptions().entrySet().stream().filter((e) -> !e.getValue().isSent()).forEach((e) -> {
channel.write(e.getValue().getSubscribeMessage()); channel.write(e.getValue().getSubscribeMessage());
e.getValue().setSent(true); e.getValue().setSent(true);
}); });
@ -148,6 +140,9 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage>
} }
}); });
channel.flush(); channel.flush();
if (this.client.isReconnect()) {
this.client.onSuccessfulReconnect();
}
break; break;
case CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD: case CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD:
@ -163,19 +158,19 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage>
} }
private void handleSubAck(MqttSubAckMessage message) { private void handleSubAck(MqttSubAckMessage message) {
MqttPendingSubscribtion pendingSubscription = this.client.getPendingSubscribtions().remove(message.variableHeader().messageId()); MqttPendingSubscription pendingSubscription = this.client.getPendingSubscriptions().remove(message.variableHeader().messageId());
if (pendingSubscription == null) { if (pendingSubscription == null) {
return; return;
} }
pendingSubscription.onSubackReceived(); pendingSubscription.onSubackReceived();
for (MqttPendingSubscribtion.MqttPendingHandler handler : pendingSubscription.getHandlers()) { for (MqttPendingSubscription.MqttPendingHandler handler : pendingSubscription.getHandlers()) {
MqttSubscribtion subscribtion = new MqttSubscribtion(pendingSubscription.getTopic(), handler.getHandler(), handler.isOnce()); MqttSubscription subscription = new MqttSubscription(pendingSubscription.getTopic(), handler.getHandler(), handler.isOnce());
this.client.getSubscriptions().put(pendingSubscription.getTopic(), subscribtion); this.client.getSubscriptions().put(pendingSubscription.getTopic(), subscription);
this.client.getHandlerToSubscribtion().put(handler.getHandler(), subscribtion); this.client.getHandlerToSubscribtion().put(handler.getHandler(), subscription);
} }
this.client.getPendingSubscribeTopics().remove(pendingSubscription.getTopic()); this.client.getPendingSubscribeTopics().remove(pendingSubscription.getTopic());
this.client.getServerSubscribtions().add(pendingSubscription.getTopic()); this.client.getServerSubscriptions().add(pendingSubscription.getTopic());
if (!pendingSubscription.getFuture().isDone()) { if (!pendingSubscription.getFuture().isDone()) {
pendingSubscription.getFuture().setSuccess(null); pendingSubscription.getFuture().setSuccess(null);
@ -215,13 +210,13 @@ final class MqttChannelHandler extends SimpleChannelInboundHandler<MqttMessage>
} }
private void handleUnsuback(MqttUnsubAckMessage message) { private void handleUnsuback(MqttUnsubAckMessage message) {
MqttPendingUnsubscribtion unsubscribtion = this.client.getPendingServerUnsubscribes().get(message.variableHeader().messageId()); MqttPendingUnsubscription unsubscription = this.client.getPendingServerUnsubscribes().get(message.variableHeader().messageId());
if (unsubscribtion == null) { if (unsubscription == null) {
return; return;
} }
unsubscribtion.onUnsubackReceived(); unsubscription.onUnsubackReceived();
this.client.getServerSubscribtions().remove(unsubscribtion.getTopic()); this.client.getServerSubscriptions().remove(unsubscription.getTopic());
unsubscribtion.getFuture().setSuccess(null); unsubscription.getFuture().setSuccess(null);
this.client.getPendingServerUnsubscribes().remove(message.variableHeader().messageId()); this.client.getPendingServerUnsubscribes().remove(message.variableHeader().messageId());
} }

20
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClient.java

@ -92,7 +92,7 @@ public interface MqttClient {
/** /**
* Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler * Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler
* This subscribtion is only once. If the MqttClient has received 1 message, the subscribtion will be removed * This subscription is only once. If the MqttClient has received 1 message, the subscription will be removed
* *
* @param topic The topic filter to subscribe to * @param topic The topic filter to subscribe to
* @param handler The handler to invoke when we receive a message * @param handler The handler to invoke when we receive a message
@ -102,7 +102,7 @@ public interface MqttClient {
/** /**
* Subscribe on the given topic, with the given qos. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler * Subscribe on the given topic, with the given qos. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler
* This subscribtion is only once. If the MqttClient has received 1 message, the subscribtion will be removed * This subscription is only once. If the MqttClient has received 1 message, the subscription will be removed
* *
* @param topic The topic filter to subscribe to * @param topic The topic filter to subscribe to
* @param handler The handler to invoke when we receive a message * @param handler The handler to invoke when we receive a message
@ -112,7 +112,7 @@ public interface MqttClient {
Future<Void> once(String topic, MqttHandler handler, MqttQoS qos); Future<Void> once(String topic, MqttHandler handler, MqttQoS qos);
/** /**
* Remove the subscribtion for the given topic and handler * Remove the subscription for the given topic and handler
* If you want to unsubscribe from all handlers known for this topic, use {@link #off(String)} * If you want to unsubscribe from all handlers known for this topic, use {@link #off(String)}
* *
* @param topic The topic to unsubscribe for * @param topic The topic to unsubscribe for
@ -122,7 +122,7 @@ public interface MqttClient {
Future<Void> off(String topic, MqttHandler handler); Future<Void> off(String topic, MqttHandler handler);
/** /**
* Remove all subscribtions for the given topic. * Remove all subscriptions for the given topic.
* If you want to specify which handler to unsubscribe, use {@link #off(String, MqttHandler)} * If you want to specify which handler to unsubscribe, use {@link #off(String, MqttHandler)}
* *
* @param topic The topic to unsubscribe for * @param topic The topic to unsubscribe for
@ -172,24 +172,18 @@ public interface MqttClient {
*/ */
MqttClientConfig getClientConfig(); MqttClientConfig getClientConfig();
/**
* Construct the MqttClientImpl with default config
*/
static MqttClient create(){
return new MqttClientImpl();
}
/** /**
* Construct the MqttClientImpl with additional config. * Construct the MqttClientImpl with additional config.
* This config can also be changed using the {@link #getClientConfig()} function * This config can also be changed using the {@link #getClientConfig()} function
* *
* @param config The config object to use while looking for settings * @param config The config object to use while looking for settings
* @param defaultHandler The handler for incoming messages that do not match any topic subscriptions
*/ */
static MqttClient create(MqttClientConfig config){ static MqttClient create(MqttClientConfig config, MqttHandler defaultHandler){
return new MqttClientImpl(config); return new MqttClientImpl(config, defaultHandler);
} }
/** /**
* Send disconnect and close channel * Send disconnect and close channel
* *

10
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientCallback.java

@ -15,6 +15,8 @@
*/ */
package org.thingsboard.mqtt; package org.thingsboard.mqtt;
import io.netty.channel.ChannelId;
/** /**
* Created by Valerii Sosliuk on 12/30/2017. * Created by Valerii Sosliuk on 12/30/2017.
*/ */
@ -25,5 +27,11 @@ public interface MqttClientCallback {
* *
* @param cause the reason behind the loss of connection. * @param cause the reason behind the loss of connection.
*/ */
public void connectionLost(Throwable cause); void connectionLost(Throwable cause);
/**
* This method is called when the connection to the server is recovered.
*
*/
void onSuccessfulReconnect();
} }

161
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttClientImpl.java

@ -40,23 +40,26 @@ import java.util.concurrent.atomic.AtomicInteger;
@SuppressWarnings({"WeakerAccess", "unused"}) @SuppressWarnings({"WeakerAccess", "unused"})
final class MqttClientImpl implements MqttClient { final class MqttClientImpl implements MqttClient {
private final Set<String> serverSubscribtions = new HashSet<>(); private final Set<String> serverSubscriptions = new HashSet<>();
private final IntObjectHashMap<MqttPendingUnsubscribtion> pendingServerUnsubscribes = new IntObjectHashMap<>(); private final IntObjectHashMap<MqttPendingUnsubscription> pendingServerUnsubscribes = new IntObjectHashMap<>();
private final IntObjectHashMap<MqttIncomingQos2Publish> qos2PendingIncomingPublishes = new IntObjectHashMap<>(); private final IntObjectHashMap<MqttIncomingQos2Publish> qos2PendingIncomingPublishes = new IntObjectHashMap<>();
private final IntObjectHashMap<MqttPendingPublish> pendingPublishes = new IntObjectHashMap<>(); private final IntObjectHashMap<MqttPendingPublish> pendingPublishes = new IntObjectHashMap<>();
private final HashMultimap<String, MqttSubscribtion> subscriptions = HashMultimap.create(); private final HashMultimap<String, MqttSubscription> subscriptions = HashMultimap.create();
private final IntObjectHashMap<MqttPendingSubscribtion> pendingSubscribtions = new IntObjectHashMap<>(); private final IntObjectHashMap<MqttPendingSubscription> pendingSubscriptions = new IntObjectHashMap<>();
private final Set<String> pendingSubscribeTopics = new HashSet<>(); private final Set<String> pendingSubscribeTopics = new HashSet<>();
private final HashMultimap<MqttHandler, MqttSubscribtion> handlerToSubscribtion = HashMultimap.create(); private final HashMultimap<MqttHandler, MqttSubscription> handlerToSubscribtion = HashMultimap.create();
private final AtomicInteger nextMessageId = new AtomicInteger(1); private final AtomicInteger nextMessageId = new AtomicInteger(1);
private final MqttClientConfig clientConfig; private final MqttClientConfig clientConfig;
private final MqttHandler defaultHandler;
private EventLoopGroup eventLoop; private EventLoopGroup eventLoop;
private Channel channel; private volatile Channel channel;
private boolean disconnected = false; private volatile boolean disconnected = false;
private volatile boolean reconnect = false;
private String host; private String host;
private int port; private int port;
private MqttClientCallback callback; private MqttClientCallback callback;
@ -65,8 +68,9 @@ final class MqttClientImpl implements MqttClient {
/** /**
* Construct the MqttClientImpl with default config * Construct the MqttClientImpl with default config
*/ */
public MqttClientImpl() { public MqttClientImpl(MqttHandler defaultHandler) {
this.clientConfig = new MqttClientConfig(); this.clientConfig = new MqttClientConfig();
this.defaultHandler = defaultHandler;
} }
/** /**
@ -75,8 +79,9 @@ final class MqttClientImpl implements MqttClient {
* *
* @param clientConfig The config object to use while looking for settings * @param clientConfig The config object to use while looking for settings
*/ */
public MqttClientImpl(MqttClientConfig clientConfig) { public MqttClientImpl(MqttClientConfig clientConfig, MqttHandler defaultHandler) {
this.clientConfig = clientConfig; this.clientConfig = clientConfig;
this.defaultHandler = defaultHandler;
} }
/** /**
@ -100,12 +105,15 @@ final class MqttClientImpl implements MqttClient {
*/ */
@Override @Override
public Future<MqttConnectResult> connect(String host, int port) { public Future<MqttConnectResult> connect(String host, int port) {
return connect(host, port, false);
}
private Future<MqttConnectResult> connect(String host, int port, boolean reconnect) {
if (this.eventLoop == null) { if (this.eventLoop == null) {
this.eventLoop = new NioEventLoopGroup(); this.eventLoop = new NioEventLoopGroup();
} }
this.host = host; this.host = host;
this.port = port; this.port = port;
Promise<MqttConnectResult> connectFuture = new DefaultPromise<>(this.eventLoop.next()); Promise<MqttConnectResult> connectFuture = new DefaultPromise<>(this.eventLoop.next());
Bootstrap bootstrap = new Bootstrap(); Bootstrap bootstrap = new Bootstrap();
bootstrap.group(this.eventLoop); bootstrap.group(this.eventLoop);
@ -113,22 +121,47 @@ final class MqttClientImpl implements MqttClient {
bootstrap.remoteAddress(host, port); bootstrap.remoteAddress(host, port);
bootstrap.handler(new MqttChannelInitializer(connectFuture, host, port, clientConfig.getSslContext())); bootstrap.handler(new MqttChannelInitializer(connectFuture, host, port, clientConfig.getSslContext()));
ChannelFuture future = bootstrap.connect(); ChannelFuture future = bootstrap.connect();
future.addListener((ChannelFutureListener) f -> { future.addListener((ChannelFutureListener) f -> {
if (f.isSuccess()) { if (f.isSuccess()) {
MqttClientImpl.this.channel = f.channel(); MqttClientImpl.this.channel = f.channel();
} else if (clientConfig.isReconnect() && !disconnected) { MqttClientImpl.this.channel.closeFuture().addListener((ChannelFutureListener) channelFuture -> {
eventLoop.schedule((Runnable) () -> connect(host, port), 1L, TimeUnit.SECONDS); if (isConnected()) {
return;
}
ChannelClosedException e = new ChannelClosedException("Channel is closed!");
if (callback != null) {
callback.connectionLost(e);
}
pendingSubscriptions.clear();
serverSubscriptions.clear();
subscriptions.clear();
pendingServerUnsubscribes.clear();
qos2PendingIncomingPublishes.clear();
pendingPublishes.clear();
pendingSubscribeTopics.clear();
handlerToSubscribtion.clear();
scheduleConnectIfRequired(host, port, true);
});
} else {
scheduleConnectIfRequired(host, port, reconnect);
} }
}); });
return connectFuture; return connectFuture;
} }
private void scheduleConnectIfRequired(String host, int port, boolean reconnect) {
if (clientConfig.isReconnect() && !disconnected) {
if (reconnect) {
this.reconnect = true;
}
eventLoop.schedule((Runnable) () -> connect(host, port, reconnect), 1L, TimeUnit.SECONDS);
}
}
@Override @Override
public boolean isConnected() { public boolean isConnected() {
if (!disconnected) { return !disconnected && channel != null && channel.isActive();
return channel == null ? false : channel.isActive();
};
return false;
} }
@Override @Override
@ -183,12 +216,12 @@ final class MqttClientImpl implements MqttClient {
*/ */
@Override @Override
public Future<Void> on(String topic, MqttHandler handler, MqttQoS qos) { public Future<Void> on(String topic, MqttHandler handler, MqttQoS qos) {
return createSubscribtion(topic, handler, false, qos); return createSubscription(topic, handler, false, qos);
} }
/** /**
* Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler * Subscribe on the given topic. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler
* This subscribtion is only once. If the MqttClient has received 1 message, the subscribtion will be removed * This subscription is only once. If the MqttClient has received 1 message, the subscription will be removed
* *
* @param topic The topic filter to subscribe to * @param topic The topic filter to subscribe to
* @param handler The handler to invoke when we receive a message * @param handler The handler to invoke when we receive a message
@ -201,7 +234,7 @@ final class MqttClientImpl implements MqttClient {
/** /**
* Subscribe on the given topic, with the given qos. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler * Subscribe on the given topic, with the given qos. When a message is received, MqttClient will invoke the {@link MqttHandler#onMessage(String, ByteBuf)} function of the given handler
* This subscribtion is only once. If the MqttClient has received 1 message, the subscribtion will be removed * This subscription is only once. If the MqttClient has received 1 message, the subscription will be removed
* *
* @param topic The topic filter to subscribe to * @param topic The topic filter to subscribe to
* @param handler The handler to invoke when we receive a message * @param handler The handler to invoke when we receive a message
@ -210,11 +243,11 @@ final class MqttClientImpl implements MqttClient {
*/ */
@Override @Override
public Future<Void> once(String topic, MqttHandler handler, MqttQoS qos) { public Future<Void> once(String topic, MqttHandler handler, MqttQoS qos) {
return createSubscribtion(topic, handler, true, qos); return createSubscription(topic, handler, true, qos);
} }
/** /**
* Remove the subscribtion for the given topic and handler * Remove the subscription for the given topic and handler
* If you want to unsubscribe from all handlers known for this topic, use {@link #off(String)} * If you want to unsubscribe from all handlers known for this topic, use {@link #off(String)}
* *
* @param topic The topic to unsubscribe for * @param topic The topic to unsubscribe for
@ -224,8 +257,8 @@ final class MqttClientImpl implements MqttClient {
@Override @Override
public Future<Void> off(String topic, MqttHandler handler) { public Future<Void> off(String topic, MqttHandler handler) {
Promise<Void> future = new DefaultPromise<>(this.eventLoop.next()); Promise<Void> future = new DefaultPromise<>(this.eventLoop.next());
for (MqttSubscribtion subscribtion : this.handlerToSubscribtion.get(handler)) { for (MqttSubscription subscription : this.handlerToSubscribtion.get(handler)) {
this.subscriptions.remove(topic, subscribtion); this.subscriptions.remove(topic, subscription);
} }
this.handlerToSubscribtion.removeAll(handler); this.handlerToSubscribtion.removeAll(handler);
this.checkSubscribtions(topic, future); this.checkSubscribtions(topic, future);
@ -233,7 +266,7 @@ final class MqttClientImpl implements MqttClient {
} }
/** /**
* Remove all subscribtions for the given topic. * Remove all subscriptions for the given topic.
* If you want to specify which handler to unsubscribe, use {@link #off(String, MqttHandler)} * If you want to specify which handler to unsubscribe, use {@link #off(String, MqttHandler)}
* *
* @param topic The topic to unsubscribe for * @param topic The topic to unsubscribe for
@ -242,12 +275,12 @@ final class MqttClientImpl implements MqttClient {
@Override @Override
public Future<Void> off(String topic) { public Future<Void> off(String topic) {
Promise<Void> future = new DefaultPromise<>(this.eventLoop.next()); Promise<Void> future = new DefaultPromise<>(this.eventLoop.next());
ImmutableSet<MqttSubscribtion> subscribtions = ImmutableSet.copyOf(this.subscriptions.get(topic)); ImmutableSet<MqttSubscription> subscriptions = ImmutableSet.copyOf(this.subscriptions.get(topic));
for (MqttSubscribtion subscribtion : subscribtions) { for (MqttSubscription subscription : subscriptions) {
for (MqttSubscribtion handSub : this.handlerToSubscribtion.get(subscribtion.getHandler())) { for (MqttSubscription handSub : this.handlerToSubscribtion.get(subscription.getHandler())) {
this.subscriptions.remove(topic, handSub); this.subscriptions.remove(topic, handSub);
} }
this.handlerToSubscribtion.remove(subscribtion.getHandler(), subscribtion); this.handlerToSubscribtion.remove(subscription.getHandler(), subscription);
} }
this.checkSubscribtions(topic, future); this.checkSubscribtions(topic, future);
return future; return future;
@ -310,7 +343,7 @@ final class MqttClientImpl implements MqttClient {
ChannelFuture channelFuture = this.sendAndFlushPacket(message); ChannelFuture channelFuture = this.sendAndFlushPacket(message);
if (channelFuture != null) { if (channelFuture != null) {
pendingPublish.setSent(channelFuture != null); pendingPublish.setSent(true);
if (channelFuture.cause() != null) { if (channelFuture.cause() != null) {
future.setFailure(channelFuture.cause()); future.setFailure(channelFuture.cause());
return future; return future;
@ -352,6 +385,15 @@ final class MqttClientImpl implements MqttClient {
///////////////////////////////////////////// PRIVATE API ///////////////////////////////////////////// ///////////////////////////////////////////// PRIVATE API /////////////////////////////////////////////
public boolean isReconnect() {
return reconnect;
}
public void onSuccessfulReconnect() {
callback.onSuccessfulReconnect();
}
ChannelFuture sendAndFlushPacket(Object message) { ChannelFuture sendAndFlushPacket(Object message) {
if (this.channel == null) { if (this.channel == null) {
return null; return null;
@ -359,11 +401,7 @@ final class MqttClientImpl implements MqttClient {
if (this.channel.isActive()) { if (this.channel.isActive()) {
return this.channel.writeAndFlush(message); return this.channel.writeAndFlush(message);
} }
ChannelClosedException e = new ChannelClosedException("Channel is closed"); return this.channel.newFailedFuture(new ChannelClosedException("Channel is closed!"));
if (callback != null) {
callback.connectionLost(e);
}
return this.channel.newFailedFuture(e);
} }
private MqttMessageIdVariableHeader getNewMessageId() { private MqttMessageIdVariableHeader getNewMessageId() {
@ -371,18 +409,18 @@ final class MqttClientImpl implements MqttClient {
return MqttMessageIdVariableHeader.from(this.nextMessageId.getAndIncrement()); return MqttMessageIdVariableHeader.from(this.nextMessageId.getAndIncrement());
} }
private Future<Void> createSubscribtion(String topic, MqttHandler handler, boolean once, MqttQoS qos) { private Future<Void> createSubscription(String topic, MqttHandler handler, boolean once, MqttQoS qos) {
if (this.pendingSubscribeTopics.contains(topic)) { if (this.pendingSubscribeTopics.contains(topic)) {
Optional<Map.Entry<Integer, MqttPendingSubscribtion>> subscribtionEntry = this.pendingSubscribtions.entrySet().stream().filter((e) -> e.getValue().getTopic().equals(topic)).findAny(); Optional<Map.Entry<Integer, MqttPendingSubscription>> subscriptionEntry = this.pendingSubscriptions.entrySet().stream().filter((e) -> e.getValue().getTopic().equals(topic)).findAny();
if (subscribtionEntry.isPresent()) { if (subscriptionEntry.isPresent()) {
subscribtionEntry.get().getValue().addHandler(handler, once); subscriptionEntry.get().getValue().addHandler(handler, once);
return subscribtionEntry.get().getValue().getFuture(); return subscriptionEntry.get().getValue().getFuture();
} }
} }
if (this.serverSubscribtions.contains(topic)) { if (this.serverSubscriptions.contains(topic)) {
MqttSubscribtion subscribtion = new MqttSubscribtion(topic, handler, once); MqttSubscription subscription = new MqttSubscription(topic, handler, once);
this.subscriptions.put(topic, subscribtion); this.subscriptions.put(topic, subscription);
this.handlerToSubscribtion.put(handler, subscribtion); this.handlerToSubscribtion.put(handler, subscription);
return this.channel.newSucceededFuture(); return this.channel.newSucceededFuture();
} }
@ -393,27 +431,27 @@ final class MqttClientImpl implements MqttClient {
MqttSubscribePayload payload = new MqttSubscribePayload(Collections.singletonList(subscription)); MqttSubscribePayload payload = new MqttSubscribePayload(Collections.singletonList(subscription));
MqttSubscribeMessage message = new MqttSubscribeMessage(fixedHeader, variableHeader, payload); MqttSubscribeMessage message = new MqttSubscribeMessage(fixedHeader, variableHeader, payload);
final MqttPendingSubscribtion pendingSubscribtion = new MqttPendingSubscribtion(future, topic, message); final MqttPendingSubscription pendingSubscription = new MqttPendingSubscription(future, topic, message);
pendingSubscribtion.addHandler(handler, once); pendingSubscription.addHandler(handler, once);
this.pendingSubscribtions.put(variableHeader.messageId(), pendingSubscribtion); this.pendingSubscriptions.put(variableHeader.messageId(), pendingSubscription);
this.pendingSubscribeTopics.add(topic); this.pendingSubscribeTopics.add(topic);
pendingSubscribtion.setSent(this.sendAndFlushPacket(message) != null); //If not sent, we will send it when the connection is opened pendingSubscription.setSent(this.sendAndFlushPacket(message) != null); //If not sent, we will send it when the connection is opened
pendingSubscribtion.startRetransmitTimer(this.eventLoop.next(), this::sendAndFlushPacket); pendingSubscription.startRetransmitTimer(this.eventLoop.next(), this::sendAndFlushPacket);
return future; return future;
} }
private void checkSubscribtions(String topic, Promise<Void> promise) { private void checkSubscribtions(String topic, Promise<Void> promise) {
if (!(this.subscriptions.containsKey(topic) && this.subscriptions.get(topic).size() != 0) && this.serverSubscribtions.contains(topic)) { if (!(this.subscriptions.containsKey(topic) && this.subscriptions.get(topic).size() != 0) && this.serverSubscriptions.contains(topic)) {
MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.UNSUBSCRIBE, false, MqttQoS.AT_LEAST_ONCE, false, 0); MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.UNSUBSCRIBE, false, MqttQoS.AT_LEAST_ONCE, false, 0);
MqttMessageIdVariableHeader variableHeader = getNewMessageId(); MqttMessageIdVariableHeader variableHeader = getNewMessageId();
MqttUnsubscribePayload payload = new MqttUnsubscribePayload(Collections.singletonList(topic)); MqttUnsubscribePayload payload = new MqttUnsubscribePayload(Collections.singletonList(topic));
MqttUnsubscribeMessage message = new MqttUnsubscribeMessage(fixedHeader, variableHeader, payload); MqttUnsubscribeMessage message = new MqttUnsubscribeMessage(fixedHeader, variableHeader, payload);
MqttPendingUnsubscribtion pendingUnsubscribtion = new MqttPendingUnsubscribtion(promise, topic, message); MqttPendingUnsubscription pendingUnsubscription = new MqttPendingUnsubscription(promise, topic, message);
this.pendingServerUnsubscribes.put(variableHeader.messageId(), pendingUnsubscribtion); this.pendingServerUnsubscribes.put(variableHeader.messageId(), pendingUnsubscription);
pendingUnsubscribtion.startRetransmissionTimer(this.eventLoop.next(), this::sendAndFlushPacket); pendingUnsubscription.startRetransmissionTimer(this.eventLoop.next(), this::sendAndFlushPacket);
this.sendAndFlushPacket(message); this.sendAndFlushPacket(message);
} else { } else {
@ -421,11 +459,11 @@ final class MqttClientImpl implements MqttClient {
} }
} }
IntObjectHashMap<MqttPendingSubscribtion> getPendingSubscribtions() { IntObjectHashMap<MqttPendingSubscription> getPendingSubscriptions() {
return pendingSubscribtions; return pendingSubscriptions;
} }
HashMultimap<String, MqttSubscribtion> getSubscriptions() { HashMultimap<String, MqttSubscription> getSubscriptions() {
return subscriptions; return subscriptions;
} }
@ -433,15 +471,15 @@ final class MqttClientImpl implements MqttClient {
return pendingSubscribeTopics; return pendingSubscribeTopics;
} }
HashMultimap<MqttHandler, MqttSubscribtion> getHandlerToSubscribtion() { HashMultimap<MqttHandler, MqttSubscription> getHandlerToSubscribtion() {
return handlerToSubscribtion; return handlerToSubscribtion;
} }
Set<String> getServerSubscribtions() { Set<String> getServerSubscriptions() {
return serverSubscribtions; return serverSubscriptions;
} }
IntObjectHashMap<MqttPendingUnsubscribtion> getPendingServerUnsubscribes() { IntObjectHashMap<MqttPendingUnsubscription> getPendingServerUnsubscribes() {
return pendingServerUnsubscribes; return pendingServerUnsubscribes;
} }
@ -481,4 +519,9 @@ final class MqttClientImpl implements MqttClient {
ch.pipeline().addLast("mqttHandler", new MqttChannelHandler(MqttClientImpl.this, connectFuture)); ch.pipeline().addLast("mqttHandler", new MqttChannelHandler(MqttClientImpl.this, connectFuture));
} }
} }
MqttHandler getDefaultHandler() {
return defaultHandler;
}
} }

4
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingSubscribtion.java → netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingSubscription.java

@ -23,7 +23,7 @@ import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
final class MqttPendingSubscribtion { final class MqttPendingSubscription {
private final Promise<Void> future; private final Promise<Void> future;
private final String topic; private final String topic;
@ -34,7 +34,7 @@ final class MqttPendingSubscribtion {
private boolean sent = false; private boolean sent = false;
MqttPendingSubscribtion(Promise<Void> future, String topic, MqttSubscribeMessage message) { MqttPendingSubscription(Promise<Void> future, String topic, MqttSubscribeMessage message) {
this.future = future; this.future = future;
this.topic = topic; this.topic = topic;
this.subscribeMessage = message; this.subscribeMessage = message;

4
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingUnsubscribtion.java → netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttPendingUnsubscription.java

@ -21,14 +21,14 @@ import io.netty.util.concurrent.Promise;
import java.util.function.Consumer; import java.util.function.Consumer;
final class MqttPendingUnsubscribtion { final class MqttPendingUnsubscription {
private final Promise<Void> future; private final Promise<Void> future;
private final String topic; private final String topic;
private final RetransmissionHandler<MqttUnsubscribeMessage> retransmissionHandler = new RetransmissionHandler<>(); private final RetransmissionHandler<MqttUnsubscribeMessage> retransmissionHandler = new RetransmissionHandler<>();
MqttPendingUnsubscribtion(Promise<Void> future, String topic, MqttUnsubscribeMessage unsubscribeMessage) { MqttPendingUnsubscription(Promise<Void> future, String topic, MqttUnsubscribeMessage unsubscribeMessage) {
this.future = future; this.future = future;
this.topic = topic; this.topic = topic;

6
netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscribtion.java → netty-mqtt/src/main/java/org/thingsboard/mqtt/MqttSubscription.java

@ -17,7 +17,7 @@ package org.thingsboard.mqtt;
import java.util.regex.Pattern; import java.util.regex.Pattern;
final class MqttSubscribtion { final class MqttSubscription {
private final String topic; private final String topic;
private final Pattern topicRegex; private final Pattern topicRegex;
@ -27,7 +27,7 @@ final class MqttSubscribtion {
private boolean called; private boolean called;
MqttSubscribtion(String topic, MqttHandler handler, boolean once) { MqttSubscription(String topic, MqttHandler handler, boolean once) {
if(topic == null){ if(topic == null){
throw new NullPointerException("topic"); throw new NullPointerException("topic");
} }
@ -65,7 +65,7 @@ final class MqttSubscribtion {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
MqttSubscribtion that = (MqttSubscribtion) o; MqttSubscription that = (MqttSubscription) o;
return once == that.once && topic.equals(that.topic) && handler.equals(that.handler); return once == that.once && topic.equals(that.topic) && handler.equals(that.handler);
} }

2
pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
<version>2.0.1-SNAPSHOT</version> <version>2.0.2-SNAPSHOT</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>Thingsboard</name> <name>Thingsboard</name>

2
rule-engine/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>2.0.1-SNAPSHOT</version> <version>2.0.2-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<artifactId>rule-engine</artifactId> <artifactId>rule-engine</artifactId>

2
rule-engine/rule-engine-api/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>2.0.1-SNAPSHOT</version> <version>2.0.2-SNAPSHOT</version>
<artifactId>rule-engine</artifactId> <artifactId>rule-engine</artifactId>
</parent> </parent>
<groupId>org.thingsboard.rule-engine</groupId> <groupId>org.thingsboard.rule-engine</groupId>

2
rule-engine/rule-engine-components/pom.xml

@ -22,7 +22,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>2.0.1-SNAPSHOT</version> <version>2.0.2-SNAPSHOT</version>
<artifactId>rule-engine</artifactId> <artifactId>rule-engine</artifactId>
</parent> </parent>
<groupId>org.thingsboard.rule-engine</groupId> <groupId>org.thingsboard.rule-engine</groupId>

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mqtt/TbMqttNode.java

@ -113,7 +113,7 @@ public class TbMqttNode implements TbNode {
} }
config.setCleanSession(this.config.isCleanSession()); config.setCleanSession(this.config.isCleanSession());
this.config.getCredentials().configure(config); this.config.getCredentials().configure(config);
MqttClient client = MqttClient.create(config); MqttClient client = MqttClient.create(config, null);
client.setEventLoop(this.eventLoopGroup); client.setEventLoop(this.eventLoopGroup);
Future<MqttConnectResult> connectFuture = client.connect(this.config.getHost(), this.config.getPort()); Future<MqttConnectResult> connectFuture = client.connect(this.config.getHost(), this.config.getPort());
MqttConnectResult result; MqttConnectResult result;

14
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/rpc/TbSendRPCRequestNode.java

@ -16,7 +16,10 @@
package org.thingsboard.rule.engine.rpc; package org.thingsboard.rule.engine.rpc;
import com.datastax.driver.core.utils.UUIDs; import com.datastax.driver.core.utils.UUIDs;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -35,6 +38,7 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import java.io.IOException;
import java.util.Random; import java.util.Random;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -86,10 +90,18 @@ public class TbSendRPCRequestNode implements TbNode {
tmp = msg.getMetaData().getValue("expirationTime"); tmp = msg.getMetaData().getValue("expirationTime");
long expirationTime = !StringUtils.isEmpty(tmp) ? Long.parseLong(tmp) : (System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(config.getTimeoutInSeconds())); long expirationTime = !StringUtils.isEmpty(tmp) ? Long.parseLong(tmp) : (System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(config.getTimeoutInSeconds()));
String params;
JsonElement paramsEl = json.get("params");
if (paramsEl.isJsonPrimitive()) {
params = paramsEl.getAsString();
} else {
params = gson.toJson(paramsEl);
}
RuleEngineDeviceRpcRequest request = RuleEngineDeviceRpcRequest.builder() RuleEngineDeviceRpcRequest request = RuleEngineDeviceRpcRequest.builder()
.oneway(oneway) .oneway(oneway)
.method(json.get("method").getAsString()) .method(json.get("method").getAsString())
.body(gson.toJson(json.get("params"))) .body(params)
.deviceId(new DeviceId(msg.getOriginator().getId())) .deviceId(new DeviceId(msg.getOriginator().getId()))
.requestId(requestId) .requestId(requestId)
.requestUUID(requestUUID) .requestUUID(requestUUID)

2
tools/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>2.0.1-SNAPSHOT</version> <version>2.0.2-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>

2
transport/coap/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>2.0.1-SNAPSHOT</version> <version>2.0.2-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>
<groupId>org.thingsboard.transport</groupId> <groupId>org.thingsboard.transport</groupId>

2
transport/http/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>2.0.1-SNAPSHOT</version> <version>2.0.2-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>
<groupId>org.thingsboard.transport</groupId> <groupId>org.thingsboard.transport</groupId>

2
transport/mqtt/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>2.0.1-SNAPSHOT</version> <version>2.0.2-SNAPSHOT</version>
<artifactId>transport</artifactId> <artifactId>transport</artifactId>
</parent> </parent>
<groupId>org.thingsboard.transport</groupId> <groupId>org.thingsboard.transport</groupId>

1
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionCtx.java

@ -155,6 +155,7 @@ public class GatewaySessionCtx {
GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName); GatewayDeviceSessionCtx deviceSessionCtx = devices.get(deviceName);
processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(), processor.process(new BasicTransportToDeviceSessionActorMsg(deviceSessionCtx.getDevice(),
new BasicAdaptorToSessionActorMsg(deviceSessionCtx, new ToDeviceRpcResponseMsg(requestId, data)))); new BasicAdaptorToSessionActorMsg(deviceSessionCtx, new ToDeviceRpcResponseMsg(requestId, data))));
ack(mqttMsg);
} else { } else {
throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json);
} }

2
transport/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>2.0.1-SNAPSHOT</version> <version>2.0.2-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>

2
ui/package.json

@ -1,7 +1,7 @@
{ {
"name": "thingsboard", "name": "thingsboard",
"private": true, "private": true,
"version": "2.0.1", "version": "2.0.2",
"description": "Thingsboard UI", "description": "Thingsboard UI",
"licenses": [ "licenses": [
{ {

2
ui/pom.xml

@ -20,7 +20,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>
<version>2.0.1-SNAPSHOT</version> <version>2.0.2-SNAPSHOT</version>
<artifactId>thingsboard</artifactId> <artifactId>thingsboard</artifactId>
</parent> </parent>
<groupId>org.thingsboard</groupId> <groupId>org.thingsboard</groupId>

1957
ui/src/app/locale/locale.constant-es.js

File diff suppressed because it is too large

773
ui/src/app/locale/locale.constant-ko.js

@ -38,8 +38,11 @@ export default function addLocaleKorean(locales) {
"update": "업데이트", "update": "업데이트",
"remove": "제거", "remove": "제거",
"search": "검색", "search": "검색",
"clear-search": "Clear search", // TODO
"assign": "할당", "assign": "할당",
"unassign": "비할당", "unassign": "비할당",
"share": "Share", // TODO
"make-private": "Make private", // TODO
"apply": "적용", "apply": "적용",
"apply-changes": "변경사항 적용", "apply-changes": "변경사항 적용",
"edit-mode": "수정 모드", "edit-mode": "수정 모드",
@ -57,8 +60,11 @@ export default function addLocaleKorean(locales) {
"undo": "취소", "undo": "취소",
"copy": "복사", "copy": "복사",
"paste": "붙여넣기", "paste": "붙여넣기",
"copy-reference": "Copy reference", // TODO
"paste-reference": "Paste reference", // TODO
"import": "가져오기", "import": "가져오기",
"export": "내보내기" "export": "내보내기",
"share-via": "Share via {{provider}}" // TODO
}, },
"aggregation": { "aggregation": {
"aggregation": "집합", "aggregation": "집합",
@ -95,6 +101,160 @@ export default function addLocaleKorean(locales) {
"enable-tls": "TLS 사용", "enable-tls": "TLS 사용",
"send-test-mail": "테스트 메일 보내기" "send-test-mail": "테스트 메일 보내기"
}, },
"alarm": { // TODO
"alarm": "Alarm",
"alarms": "Alarms",
"select-alarm": "Select alarm",
"no-alarms-matching": "No alarms matching '{{entity}}' were found.",
"alarm-required": "Alarm is required",
"alarm-status": "Alarm status",
"search-status": {
"ANY": "Any",
"ACTIVE": "Active",
"CLEARED": "Cleared",
"ACK": "Acknowledged",
"UNACK": "Unacknowledged"
},
"display-status": {
"ACTIVE_UNACK": "Active Unacknowledged",
"ACTIVE_ACK": "Active Acknowledged",
"CLEARED_UNACK": "Cleared Unacknowledged",
"CLEARED_ACK": "Cleared Acknowledged"
},
"no-alarms-prompt": "No alarms found",
"created-time": "Created time",
"type": "Type",
"severity": "Severity",
"originator": "Originator",
"originator-type": "Originator type",
"details": "Details",
"status": "Status",
"alarm-details": "Alarm details",
"start-time": "Start time",
"end-time": "End time",
"ack-time": "Acknowledged time",
"clear-time": "Cleared time",
"severity-critical": "Critical",
"severity-major": "Major",
"severity-minor": "Minor",
"severity-warning": "Warning",
"severity-indeterminate": "Indeterminate",
"acknowledge": "Acknowledge",
"clear": "Clear",
"search": "Search alarms",
"selected-alarms": "{ count, select, 1 {1 alarm} other {# alarms} } selected",
"no-data": "No data to display",
"polling-interval": "Alarms polling interval (sec)",
"polling-interval-required": "Alarms polling interval is required.",
"min-polling-interval-message": "At least 1 sec polling interval is allowed.",
"aknowledge-alarms-title": "Acknowledge { count, select, 1 {1 alarm} other {# alarms} }",
"aknowledge-alarms-text": "Are you sure you want to acknowledge { count, select, 1 {1 alarm} other {# alarms} }?",
"clear-alarms-title": "Clear { count, select, 1 {1 alarm} other {# alarms} }",
"clear-alarms-text": "Are you sure you want to clear { count, select, 1 {1 alarm} other {# alarms} }?"
},
"alias": { // TODO
"add": "Add alias",
"edit": "Edit alias",
"name": "Alias name",
"name-required": "Alias name is required",
"duplicate-alias": "Alias with same name is already exists.",
"filter-type-single-entity": "Single entity",
"filter-type-entity-list": "Entity list",
"filter-type-entity-name": "Entity name",
"filter-type-state-entity": "Entity from dashboard state",
"filter-type-state-entity-description": "Entity taken from dashboard state parameters",
"filter-type-asset-type": "Asset type",
"filter-type-asset-type-description": "Assets of type '{{assetType}}'",
"filter-type-asset-type-and-name-description": "Assets of type '{{assetType}}' and with name starting with '{{prefix}}'",
"filter-type-device-type": "Device type",
"filter-type-device-type-description": "Devices of type '{{deviceType}}'",
"filter-type-device-type-and-name-description": "Devices of type '{{deviceType}}' and with name starting with '{{prefix}}'",
"filter-type-relations-query": "Relations query",
"filter-type-relations-query-description": "{{entities}} that have {{relationType}} relation {{direction}} {{rootEntity}}",
"filter-type-asset-search-query": "Asset search query",
"filter-type-asset-search-query-description": "Assets with types {{assetTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}",
"filter-type-device-search-query": "Device search query",
"filter-type-device-search-query-description": "Devices with types {{deviceTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}",
"entity-filter": "Entity filter",
"resolve-multiple": "Resolve as multiple entities",
"filter-type": "Filter type",
"filter-type-required": "Filter type is required.",
"entity-filter-no-entity-matched": "No entities matching specified filter were found.",
"no-entity-filter-specified": "No entity filter specified",
"root-state-entity": "Use dashboard state entity as root",
"root-entity": "Root entity",
"state-entity-parameter-name": "State entity parameter name",
"default-state-entity": "Default state entity",
"default-entity-parameter-name": "By default",
"max-relation-level": "Max relation level",
"unlimited-level": "Unlimited level",
"state-entity": "Dashboard state entity",
"all-entities": "All entities",
"any-relation": "any"
},
"asset": { // TODO
"asset": "Asset",
"assets": "Assets",
"management": "Asset management",
"view-assets": "View Assets",
"add": "Add Asset",
"assign-to-customer": "Assign to customer",
"assign-asset-to-customer": "Assign Asset(s) To Customer",
"assign-asset-to-customer-text": "Please select the assets to assign to the customer",
"no-assets-text": "No assets found",
"assign-to-customer-text": "Please select the customer to assign the asset(s)",
"public": "Public",
"assignedToCustomer": "Assigned to customer",
"make-public": "Make asset public",
"make-private": "Make asset private",
"unassign-from-customer": "Unassign from customer",
"delete": "Delete asset",
"asset-public": "Asset is public",
"asset-type": "Asset type",
"asset-type-required": "Asset type is required.",
"select-asset-type": "Select asset type",
"enter-asset-type": "Enter asset type",
"any-asset": "Any asset",
"no-asset-types-matching": "No asset types matching '{{entitySubtype}}' were found.",
"asset-type-list-empty": "No asset types selected.",
"asset-types": "Asset types",
"name": "Name",
"name-required": "Name is required.",
"description": "Description",
"type": "Type",
"type-required": "Type is required.",
"details": "Details",
"events": "Events",
"add-asset-text": "Add new asset",
"asset-details": "Asset details",
"assign-assets": "Assign assets",
"assign-assets-text": "Assign { count, select, 1 {1 asset} other {# assets} } to customer",
"delete-assets": "Delete assets",
"unassign-assets": "Unassign assets",
"unassign-assets-action-title": "Unassign { count, select, 1 {1 asset} other {# assets} } from customer",
"assign-new-asset": "Assign new asset",
"delete-asset-title": "Are you sure you want to delete the asset '{{assetName}}'?",
"delete-asset-text": "Be careful, after the confirmation the asset and all related data will become unrecoverable.",
"delete-assets-title": "Are you sure you want to delete { count, select, 1 {1 asset} other {# assets} }?",
"delete-assets-action-title": "Delete { count, select, 1 {1 asset} other {# assets} }",
"delete-assets-text": "Be careful, after the confirmation all selected assets will be removed and all related data will become unrecoverable.",
"make-public-asset-title": "Are you sure you want to make the asset '{{assetName}}' public?",
"make-public-asset-text": "After the confirmation the asset and all its data will be made public and accessible by others.",
"make-private-asset-title": "Are you sure you want to make the asset '{{assetName}}' private?",
"make-private-asset-text": "After the confirmation the asset and all its data will be made private and won't be accessible by others.",
"unassign-asset-title": "Are you sure you want to unassign the asset '{{assetName}}'?",
"unassign-asset-text": "After the confirmation the asset will be unassigned and won't be accessible by the customer.",
"unassign-asset": "Unassign asset",
"unassign-assets-title": "Are you sure you want to unassign { count, select, 1 {1 asset} other {# assets} }?",
"unassign-assets-text": "After the confirmation all selected assets will be unassigned and won't be accessible by the customer.",
"copyId": "Copy asset Id",
"idCopiedMessage": "Asset Id has been copied to clipboard",
"select-asset": "Select asset",
"no-assets-matching": "No assets matching '{{entity}}' were found.",
"asset-required": "Asset is required",
"name-starts-with": "Asset name starts with"
},
"attribute": { "attribute": {
"attributes": "속성", "attributes": "속성",
"latest-telemetry": "최근 데이터", "latest-telemetry": "최근 데이터",
@ -104,9 +264,9 @@ export default function addLocaleKorean(locales) {
"scope-server": "서버 속성", "scope-server": "서버 속성",
"scope-shared": "공유 속성", "scope-shared": "공유 속성",
"add": "속성 추가", "add": "속성 추가",
"key": "Key", "key": "Key", // TODO
"key-required": "속성 key를 입력하세요.", "key-required": "속성 key를 입력하세요.",
"value": "Value", "value": "Value", // TODO
"value-required": "속성 value를 입력하세요.", "value-required": "속성 value를 입력하세요.",
"delete-attributes-title": "{ count, select, 1 {속성} other {여러 속성들을} } 삭제하시겠습니까??", "delete-attributes-title": "{ count, select, 1 {속성} other {여러 속성들을} } 삭제하시겠습니까??",
"delete-attributes-text": "모든 선택된 속성들이 제거 될 것이므로 주의하십시오.", "delete-attributes-text": "모든 선택된 속성들이 제거 될 것이므로 주의하십시오.",
@ -121,6 +281,38 @@ export default function addLocaleKorean(locales) {
"selected-attributes": "{ count, select, 1 {속성 1개} other {속성 #개} } 선택됨", "selected-attributes": "{ count, select, 1 {속성 1개} other {속성 #개} } 선택됨",
"selected-telemetry": "{ count, select, 1 {최근 데이터 1개} other {최근 데이터 #개} } 선택됨" "selected-telemetry": "{ count, select, 1 {최근 데이터 1개} other {최근 데이터 #개} } 선택됨"
}, },
"audit-log": { // TODO
"audit": "Audit",
"audit-logs": "Audit Logs",
"timestamp": "Timestamp",
"entity-type": "Entity Type",
"entity-name": "Entity Name",
"user": "User",
"type": "Type",
"status": "Status",
"details": "Details",
"type-added": "Added",
"type-deleted": "Deleted",
"type-updated": "Updated",
"type-attributes-updated": "Attributes updated",
"type-attributes-deleted": "Attributes deleted",
"type-rpc-call": "RPC call",
"type-credentials-updated": "Credentials updated",
"type-assigned-to-customer": "Assigned to Customer",
"type-unassigned-from-customer": "Unassigned from Customer",
"type-activated": "Activated",
"type-suspended": "Suspended",
"type-credentials-read": "Credentials read",
"type-attributes-read": "Attributes read",
"status-success": "Success",
"status-failure": "Failure",
"audit-log-details": "Audit log details",
"no-audit-logs-prompt": "No logs found",
"action-data": "Action data",
"failure-details": "Failure details",
"search": "Search audit logs",
"clear-search": "Clear search"
},
"confirm-on-exit": { "confirm-on-exit": {
"message": "변경 사항을 저장하지 않았습니다. 이 페이지를 나가시겠습니까?", "message": "변경 사항을 저장하지 않았습니다. 이 페이지를 나가시겠습니까?",
"html-message": "변경 사항을 저장하지 않았습니다.<br/>이 페이지를 나가시겠습니까?", "html-message": "변경 사항을 저장하지 않았습니다.<br/>이 페이지를 나가시겠습니까?",
@ -145,6 +337,11 @@ export default function addLocaleKorean(locales) {
"enter-password": "비밀번호를 입력하세요.", "enter-password": "비밀번호를 입력하세요.",
"enter-search": "검색어 입력" "enter-search": "검색어 입력"
}, },
"content-type": { // TODO
"json": "Json",
"text": "Text",
"binary": "Binary (Base64)"
},
"customer": { "customer": {
"customers": "커스터머", "customers": "커스터머",
"management": "커스터머 관리", "management": "커스터머 관리",
@ -156,6 +353,10 @@ export default function addLocaleKorean(locales) {
"manage-customer-users": "커스터머 사용자 관리", "manage-customer-users": "커스터머 사용자 관리",
"manage-customer-devices": "커스터머 디바이스 관리", "manage-customer-devices": "커스터머 디바이스 관리",
"manage-customer-dashboards": "커스터머 대시보드 관리", "manage-customer-dashboards": "커스터머 대시보드 관리",
"manage-public-devices": "Manage public devices", // TODO
"manage-public-dashboards": "Manage public dashboards", // TODO
"manage-customer-assets": "Manage customer assets", // TODO
"manage-public-assets": "Manage public assets", // TODO
"add-customer-text": "커스터머 추가", "add-customer-text": "커스터머 추가",
"no-customers-text": "커스터머가 없습니다.", "no-customers-text": "커스터머가 없습니다.",
"customer-details": "커스터머 상세정보", "customer-details": "커스터머 상세정보",
@ -169,7 +370,17 @@ export default function addLocaleKorean(locales) {
"manage-dashboards": "대시보드 관리", "manage-dashboards": "대시보드 관리",
"title": "타이틀", "title": "타이틀",
"title-required": "타이틀을 입력하세요.", "title-required": "타이틀을 입력하세요.",
"description": "설명" "description": "설명",
"details": "Details",
"events": "Events",
"copyId": "Copy customer Id",
"idCopiedMessage": "Customer Id has been copied to clipboard",
"select-customer": "Select customer",
"no-customers-matching": "No customers matching '{{entity}}' were found.",
"customer-required": "Customer is required",
"select-default-customer": "Select default customer",
"default-customer": "Default customer",
"default-customer-required": "Default customer is required in order to debug dashboard on Tenant level"
}, },
"datetime": { "datetime": {
"date-from": "시작 날짜", "date-from": "시작 날짜",
@ -277,11 +488,15 @@ export default function addLocaleKorean(locales) {
"attributes": "Attributes", "attributes": "Attributes",
"timeseries-required": "디바이스 timeseries 를 입력하세요.", "timeseries-required": "디바이스 timeseries 를 입력하세요.",
"timeseries-or-attributes-required": "디바이스 timeseries/attributes 를 입력하세요.", "timeseries-or-attributes-required": "디바이스 timeseries/attributes 를 입력하세요.",
"maximum-timeseries-or-attributes": "Maximum { count, select, 1 {1 timeseries/attribute is allowed.} other {# timeseries/attributes are allowed} }", // TODO
"alarm-fields-required": "Alarm fields are required.", // TODO
"function-types": "함수 유형", "function-types": "함수 유형",
"function-types-required": "함수 유형을 입력하세요." "function-types-required": "함수 유형을 입력하세요.",
"maximum-function-types": "Maximum { count, select, 1 {1 function type is allowed.} other {# function types are allowed} }" // TODO
}, },
"datasource": { "datasource": {
"type": "데이터소스 유형", "type": "데이터소스 유형",
"name": "Name", // TODO
"add-datasource-prompt": "데이터소스를 추가하세요." "add-datasource-prompt": "데이터소스를 추가하세요."
}, },
"details": { "details": {
@ -375,11 +590,101 @@ export default function addLocaleKorean(locales) {
"unhandled-error-code": "처리되지 않은 오류 코드: {{errorCode}}", "unhandled-error-code": "처리되지 않은 오류 코드: {{errorCode}}",
"unknown-error": "알 수 없는 오류" "unknown-error": "알 수 없는 오류"
}, },
"entity": { // TODO
"entity": "Entity",
"entities": "Entities",
"aliases": "Entity aliases",
"entity-alias": "Entity alias",
"unable-delete-entity-alias-title": "Unable to delete entity alias",
"unable-delete-entity-alias-text": "Entity alias '{{entityAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}",
"duplicate-alias-error": "Duplicate alias found '{{alias}}'.<br>Entity aliases must be unique whithin the dashboard.",
"missing-entity-filter-error": "Filter is missing for alias '{{alias}}'.",
"configure-alias": "Configure '{{alias}}' alias",
"alias": "Alias",
"alias-required": "Entity alias is required.",
"remove-alias": "Remove entity alias",
"add-alias": "Add entity alias",
"entity-list": "Entity list",
"entity-type": "Entity type",
"entity-types": "Entity types",
"entity-type-list": "Entity type list",
"any-entity": "Any entity",
"enter-entity-type": "Enter entity type",
"no-entities-matching": "No entities matching '{{entity}}' were found.",
"no-entity-types-matching": "No entity types matching '{{entityType}}' were found.",
"name-starts-with": "Name starts with",
"use-entity-name-filter": "Use filter",
"entity-list-empty": "No entities selected.",
"entity-type-list-empty": "No entity types selected.",
"entity-name-filter-required": "Entity name filter is required.",
"entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.",
"all-subtypes": "All",
"select-entities": "Select entities",
"no-aliases-found": "No aliases found.",
"no-alias-matching": "'{{alias}}' not found.",
"create-new-alias": "Create a new one!",
"key": "Key",
"key-name": "Key name",
"no-keys-found": "No keys found.",
"no-key-matching": "'{{key}}' not found.",
"create-new-key": "Create a new one!",
"type": "Type",
"type-required": "Entity type is required.",
"type-device": "Device",
"type-devices": "Devices",
"list-of-devices": "{ count, select, 1 {One device} other {List of # devices} }",
"device-name-starts-with": "Devices whose names start with '{{prefix}}'",
"type-asset": "Asset",
"type-assets": "Assets",
"list-of-assets": "{ count, select, 1 {One asset} other {List of # assets} }",
"asset-name-starts-with": "Assets whose names start with '{{prefix}}'",
"type-rule": "Rule",
"type-rules": "Rules",
"list-of-rules": "{ count, select, 1 {One rule} other {List of # rules} }",
"rule-name-starts-with": "Rules whose names start with '{{prefix}}'",
"type-plugin": "Plugin",
"type-plugins": "Plugins",
"list-of-plugins": "{ count, select, 1 {One plugin} other {List of # plugins} }",
"plugin-name-starts-with": "Plugins whose names start with '{{prefix}}'",
"type-tenant": "Tenant",
"type-tenants": "Tenants",
"list-of-tenants": "{ count, select, 1 {One tenant} other {List of # tenants} }",
"tenant-name-starts-with": "Tenants whose names start with '{{prefix}}'",
"type-customer": "Customer",
"type-customers": "Customers",
"list-of-customers": "{ count, select, 1 {One customer} other {List of # customers} }",
"customer-name-starts-with": "Customers whose names start with '{{prefix}}'",
"type-user": "User",
"type-users": "Users",
"list-of-users": "{ count, select, 1 {One user} other {List of # users} }",
"user-name-starts-with": "Users whose names start with '{{prefix}}'",
"type-dashboard": "Dashboard",
"type-dashboards": "Dashboards",
"list-of-dashboards": "{ count, select, 1 {One dashboard} other {List of # dashboards} }",
"dashboard-name-starts-with": "Dashboards whose names start with '{{prefix}}'",
"type-alarm": "Alarm",
"type-alarms": "Alarms",
"list-of-alarms": "{ count, select, 1 {One alarms} other {List of # alarms} }",
"alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'",
"type-rulechain": "Rule chain",
"type-rulechains": "Rule chains",
"list-of-rulechains": "{ count, select, 1 {One rule chain} other {List of # rule chains} }",
"rulechain-name-starts-with": "Rule chains whose names start with '{{prefix}}'",
"type-current-customer": "Current Customer",
"search": "Search entities",
"selected-entities": "{ count, select, 1 {1 entity} other {# entities} } selected",
"entity-name": "Entity name",
"details": "Entity details",
"no-entities-prompt": "No entities found",
"no-data": "No data to display"
},
"event": { "event": {
"event-type": "이벤트 타입", "event-type": "이벤트 타입",
"type-error": "에러", "type-error": "에러",
"type-lc-event": "주기적 이벤트", "type-lc-event": "주기적 이벤트",
"type-stats": "통계", "type-stats": "통계",
"type-debug-rule-node": "Debug", // TODO
"type-debug-rule-chain": "Debug", // TODO
"no-events-prompt": "이벤트 없음", "no-events-prompt": "이벤트 없음",
"error": "에러", "error": "에러",
"alarm": "알람", "alarm": "알람",
@ -387,6 +692,14 @@ export default function addLocaleKorean(locales) {
"server": "서버", "server": "서버",
"body": "Body", "body": "Body",
"method": "Method", "method": "Method",
"type": "Type", // TODO
"entity": "Entity", // TODO
"message-id": "Message Id", // TODO
"message-type": "Message Type", // TODO
"data-type": "Data Type", // TODO
"relation-type": "Relation Type", // TODO
"metadata": "Metadata", // TODO
"data": "Data", // TODO
"event": "이벤트", "event": "이벤트",
"status": "상태", "status": "상태",
"success": "성공", "success": "성공",
@ -394,6 +707,163 @@ export default function addLocaleKorean(locales) {
"messages-processed": "처리된 메시지", "messages-processed": "처리된 메시지",
"errors-occurred": "오류가 발생했습니다" "errors-occurred": "오류가 발생했습니다"
}, },
"extension": { // TODO
"extensions": "Extensions",
"selected-extensions": "{ count, select, 1 {1 extension} other {# extensions} } selected",
"type": "Type",
"key": "Key",
"value": "Value",
"id": "Id",
"extension-id": "Extension id",
"extension-type": "Extension type",
"transformer-json": "JSON *",
"unique-id-required": "Current extension id already exists.",
"delete": "Delete extension",
"add": "Add extension",
"edit": "Edit extension",
"delete-extension-title": "Are you sure you want to delete the extension '{{extensionId}}'?",
"delete-extension-text": "Be careful, after the confirmation the extension and all related data will become unrecoverable.",
"delete-extensions-title": "Are you sure you want to delete { count, select, 1 {1 extension} other {# extensions} }?",
"delete-extensions-text": "Be careful, after the confirmation all selected extensions will be removed.",
"converters": "Converters",
"converter-id": "Converter id",
"configuration": "Configuration",
"converter-configurations": "Converter configurations",
"token": "Security token",
"add-converter": "Add converter",
"add-config": "Add converter configuration",
"device-name-expression": "Device name expression",
"device-type-expression": "Device type expression",
"custom": "Custom",
"to-double": "To Double",
"transformer": "Transformer",
"json-required": "Transformer json is required.",
"json-parse": "Unable to parse transformer json.",
"attributes": "Attributes",
"add-attribute": "Add attribute",
"add-map": "Add mapping element",
"timeseries": "Timeseries",
"add-timeseries": "Add timeseries",
"field-required": "Field is required",
"brokers": "Brokers",
"add-broker": "Add broker",
"host": "Host",
"port": "Port",
"port-range": "Port should be in a range from 1 to 65535.",
"ssl": "Ssl",
"credentials": "Credentials",
"username": "Username",
"password": "Password",
"retry-interval": "Retry interval in milliseconds",
"anonymous": "Anonymous",
"basic": "Basic",
"pem": "PEM",
"ca-cert": "CA certificate file *",
"private-key": "Private key file *",
"cert": "Certificate file *",
"no-file": "No file selected.",
"drop-file": "Drop a file or click to select a file to upload.",
"mapping": "Mapping",
"topic-filter": "Topic filter",
"converter-type": "Converter type",
"converter-json": "Json",
"json-name-expression": "Device name json expression",
"topic-name-expression": "Device name topic expression",
"json-type-expression": "Device type json expression",
"topic-type-expression": "Device type topic expression",
"attribute-key-expression": "Attribute key expression",
"attr-json-key-expression": "Attribute key json expression",
"attr-topic-key-expression": "Attribute key topic expression",
"request-id-expression": "Request id expression",
"request-id-json-expression": "Request id json expression",
"request-id-topic-expression": "Request id topic expression",
"response-topic-expression": "Response topic expression",
"value-expression": "Value expression",
"topic": "Topic",
"timeout": "Timeout in milliseconds",
"converter-json-required": "Converter json is required.",
"converter-json-parse": "Unable to parse converter json.",
"filter-expression": "Filter expression",
"connect-requests": "Connect requests",
"add-connect-request": "Add connect request",
"disconnect-requests": "Disconnect requests",
"add-disconnect-request": "Add disconnect request",
"attribute-requests": "Attribute requests",
"add-attribute-request": "Add attribute request",
"attribute-updates": "Attribute updates",
"add-attribute-update": "Add attribute update",
"server-side-rpc": "Server side RPC",
"add-server-side-rpc-request": "Add server-side RPC request",
"device-name-filter": "Device name filter",
"attribute-filter": "Attribute filter",
"method-filter": "Method filter",
"request-topic-expression": "Request topic expression",
"response-timeout": "Response timeout in milliseconds",
"topic-expression": "Topic expression",
"client-scope": "Client scope",
"add-device": "Add device",
"opc-server": "Servers",
"opc-add-server": "Add server",
"opc-add-server-prompt": "Please add server",
"opc-application-name": "Application name",
"opc-application-uri": "Application uri",
"opc-scan-period-in-seconds": "Scan period in seconds",
"opc-security": "Security",
"opc-identity": "Identity",
"opc-keystore": "Keystore",
"opc-type": "Type",
"opc-keystore-type": "Type",
"opc-keystore-location": "Location *",
"opc-keystore-password": "Password",
"opc-keystore-alias": "Alias",
"opc-keystore-key-password": "Key password",
"opc-device-node-pattern": "Device node pattern",
"opc-device-name-pattern": "Device name pattern",
"modbus-server": "Servers/slaves",
"modbus-add-server": "Add server/slave",
"modbus-add-server-prompt": "Please add server/slave",
"modbus-transport": "Transport",
"modbus-port-name": "Serial port name",
"modbus-encoding": "Encoding",
"modbus-parity": "Parity",
"modbus-baudrate": "Baud rate",
"modbus-databits": "Data bits",
"modbus-stopbits": "Stop bits",
"modbus-databits-range": "Data bits should be in a range from 7 to 8.",
"modbus-stopbits-range": "Stop bits should be in a range from 1 to 2.",
"modbus-unit-id": "Unit ID",
"modbus-unit-id-range": "Unit ID should be in a range from 1 to 247.",
"modbus-device-name": "Device name",
"modbus-poll-period": "Poll period (ms)",
"modbus-attributes-poll-period": "Attributes poll period (ms)",
"modbus-timeseries-poll-period": "Timeseries poll period (ms)",
"modbus-poll-period-range": "Poll period should be positive value.",
"modbus-tag": "Tag",
"modbus-function": "Function",
"modbus-register-address": "Register address",
"modbus-register-address-range": "Register address should be in a range from 0 to 65535.",
"modbus-register-bit-index": "Bit index",
"modbus-register-bit-index-range": "Bit index should be in a range from 0 to 15.",
"modbus-register-count": "Register count",
"modbus-register-count-range": "Register count should be a positive value.",
"modbus-byte-order": "Byte order",
"sync": {
"status": "Status",
"sync": "Sync",
"not-sync": "Not sync",
"last-sync-time": "Last sync time",
"not-available": "Not available"
},
"export-extensions-configuration": "Export extensions configuration",
"import-extensions-configuration": "Import extensions configuration",
"import-extensions": "Import extensions",
"import-extension": "Import extension",
"export-extension": "Export extension",
"file": "Extensions file",
"invalid-file-error": "Invalid extension file"
},
"fullscreen": { "fullscreen": {
"expand": "전체화면으로 확장", "expand": "전체화면으로 확장",
"exit": "전체화면 종료", "exit": "전체화면 종료",
@ -436,7 +906,24 @@ export default function addLocaleKorean(locales) {
}, },
"js-func": { "js-func": {
"no-return-error": "함수는 값을 반환해야 합니다!", "no-return-error": "함수는 값을 반환해야 합니다!",
"return-type-mismatch": "함수는 '{{type}}' 유형의 값을 반환해야 합니다!" "return-type-mismatch": "함수는 '{{type}}' 유형의 값을 반환해야 합니다!",
"tidy": "Tidy" // TODO
},
"key-val": { // TODO
"key": "Key",
"value": "Value",
"remove-entry": "Remove entry",
"add-entry": "Add entry",
"no-data": "No entries"
},
"layout": { // TODO
"layout": "Layout",
"manage": "Manage layouts",
"settings": "Layout settings",
"color": "Color",
"main": "Main",
"right": "Right",
"select": "Select target layout"
}, },
"legend": { "legend": {
"position": "범례 위치", "position": "범례 위치",
@ -467,45 +954,6 @@ export default function addLocaleKorean(locales) {
"password-link-sent-message": "비밀번호 재설정 링크가 성공적으로 전송되었습니다!", "password-link-sent-message": "비밀번호 재설정 링크가 성공적으로 전송되었습니다!",
"email": "이메일" "email": "이메일"
}, },
"plugin": {
"plugins": "플러그인",
"delete": "플러그인 삭제",
"activate": "플러그인 활성화",
"suspend": "플러그인 비활성화",
"active": "활성화",
"suspended": "비활성화",
"name": "이름",
"name-required": "이름을 입력하세요.",
"description": "설명",
"add": "플러그인 추가",
"delete-plugin-title": "'{{pluginName}}' 플러그인을 삭제하시겠습니까?",
"delete-plugin-text": "플러그인과 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.",
"delete-plugins-title": "{ count, select, 1 {플러그인 1개} other {플러그인 #개} }를 삭제하시겠습니까?",
"delete-plugins-action-title": "{ count, select, 1 {플러그인 1개} other {플러그인 #개} } 삭제",
"delete-plugins-text": "선택된 플러그인이 삭제되고 관련된 모든 데이터가 없어지므로 주의하십시오.",
"add-plugin-text": "새로운 플러그인 추가",
"no-plugins-text": "플러그인이 없습니다.",
"plugin-details": "플러그인 상세정보",
"api-token": "API 토큰",
"api-token-required": "API 토큰을 입력하세요.",
"type": "플러그인 종류",
"type-required": "플러그인 종류를 선택해주세요.",
"configuration": "플러그인 구성",
"system": "시스템",
"select-plugin": "플러그인 선택",
"plugin": "플러그인",
"no-plugins-matching": "'{{entity}}'과 일치하는 플러그인을 찾을 수 없습니다.",
"plugin-required": "플러그인을 입력하세요.",
"plugin-require-match": "기존의 플러그인을 선택해주세요.",
"events": "이벤트",
"details": "상세",
"import": "플러그인 가져오기",
"export": "플러그인 내보내기",
"export-failed-error": "플러그인을 내보내기 할 수 없습니다.: {{error}}",
"create-new-plugin": "새로운 플러그인 생성",
"plugin-file": "플러그인 파일",
"invalid-plugin-file-error": "플러그인을 가져오기 할 수 없습니다.: 잘못된 플러그인 데이터 구조입니다."
},
"position": { "position": {
"top": "상단", "top": "상단",
"bottom": "하단", "bottom": "하단",
@ -517,60 +965,139 @@ export default function addLocaleKorean(locales) {
"change-password": "비밀번호 변경", "change-password": "비밀번호 변경",
"current-password": "현재 비밀번호" "current-password": "현재 비밀번호"
}, },
"rule": { "relation": { // TODO
"rules": "규칙", "relations": "Relations",
"delete": "규칙 삭제", "direction": "Direction",
"activate": "규칙 활성화", "search-direction": {
"suspend": "규칙 비활성화", "FROM": "From",
"active": "활성화", "TO": "To"
"suspended": "비활성화", },
"name": "이름", "direction-type": {
"name-required": "이름을 입력하세요.", "FROM": "from",
"description": "설명", "TO": "to"
"add": "규칙 추가", },
"delete-rule-title": "'{{ruleName}}' 규칙을 삭제하시겠습니까?", "from-relations": "Outbound relations",
"delete-rule-text": "규칙과 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", "to-relations": "Inbound relations",
"delete-rules-title": "{ count, select, 1 {규칙 1개} other {규칙 #개} }를 삭제하시겠습니까?", "selected-relations": "{ count, select, 1 {1 relation} other {# relations} } selected",
"delete-rules-action-title": "{ count, select, 1 {규칙 1개} other {규칙 #개} } 삭제", "type": "Type",
"delete-rules-text": "선택된 규칙이 삭제되고 관련된 모든 데이터를 복구할 수 없으므로 주의하십시오.", "to-entity-type": "To entity type",
"add-rule-text": "규칙 추가", "to-entity-name": "To entity name",
"no-rules-text": "규칙이 없습니다.", "from-entity-type": "From entity type",
"rule-details": "규칙 상세정보", "from-entity-name": "From entity name",
"filters": "필터", "to-entity": "To entity",
"filter": "필터", "from-entity": "From entity",
"add-filter-prompt": "필터를 추가해 주세요.", "delete": "Delete relation",
"remove-filter": "필터 삭제", "relation-type": "Relation type",
"add-filter": "필터 추가", "relation-type-required": "Relation type is required.",
"filter-name": "필터 이름", "any-relation-type": "Any type",
"filter-type": "필터 종류", "add": "Add relation",
"edit-filter": "필터 수정", "edit": "Edit relation",
"view-filter": "필터 보기", "delete-to-relation-title": "Are you sure you want to delete relation to the entity '{{entityName}}'?",
"component-name": "이름", "delete-to-relation-text": "Be careful, after the confirmation the entity '{{entityName}}' will be unrelated from the current entity.",
"component-name-required": "이름을 입력하세요.", "delete-to-relations-title": "Are you sure you want to delete { count, select, 1 {1 relation} other {# relations} }?",
"component-type": "종류", "delete-to-relations-text": "Be careful, after the confirmation all selected relations will be removed and corresponding entities will be unrelated from the current entity.",
"component-type-required": "타입을 입력하세요.", "delete-from-relation-title": "Are you sure you want to delete relation from the entity '{{entityName}}'?",
"processor": "프로세서", "delete-from-relation-text": "Be careful, after the confirmation current entity will be unrelated from the entity '{{entityName}}'.",
"no-processor-configured": "프로세서가 구성되지 않았습니다.", "delete-from-relations-title": "Are you sure you want to delete { count, select, 1 {1 relation} other {# relations} }?",
"create-processor": "프로세서 생성", "delete-from-relations-text": "Be careful, after the confirmation all selected relations will be removed and current entity will be unrelated from the corresponding entities.",
"processor-name": "프로세서 이름", "remove-relation-filter": "Remove relation filter",
"processor-type": "프로세서 종류", "add-relation-filter": "Add relation filter",
"plugin-action": "플러그인 액션", "any-relation": "Any relation",
"action-name": "액션 이름", "relation-filters": "Relation filters",
"action-type": "액션 종류", "additional-info": "Additional info (JSON)",
"create-action-prompt": "액션을 만들어 주세요", "invalid-additional-info": "Unable to parse additional info json."
"create-action": "액션 생성", },
"details": "상세", "rulechain": { // TODO
"events": "이벤트", "rulechain": "Rule chain",
"system": "시스템", "rulechains": "Rule chains",
"import": "규칙 가져오기", "root": "Root",
"export": "규칙 내보내기", "delete": "Delete rule chain",
"export-failed-error": "규칙을 내보내기 할 수 없습니다.: {{error}}", "name": "Name",
"create-new-rule": "새로운 규칙 생성", "name-required": "Name is required.",
"rule-file": "규칙 파일", "description": "Description",
"invalid-rule-file-error": "규칙을 가져오기 할 수 없습니다.: 잘못된 데이터 구조입니다." "add": "Add Rule Chain",
"set-root": "Make rule chain root",
"set-root-rulechain-title": "Are you sure you want to make the rule chain '{{ruleChainName}}' root?",
"set-root-rulechain-text": "After the confirmation the rule chain will become root and will handle all incoming transport messages.",
"delete-rulechain-title": "Are you sure you want to delete the rule chain '{{ruleChainName}}'?",
"delete-rulechain-text": "Be careful, after the confirmation the rule chain and all related data will become unrecoverable.",
"delete-rulechains-title": "Are you sure you want to delete { count, select, 1 {1 rule chain} other {# rule chains} }?",
"delete-rulechains-action-title": "Delete { count, select, 1 {1 rule chain} other {# rule chains} }",
"delete-rulechains-text": "Be careful, after the confirmation all selected rule chains will be removed and all related data will become unrecoverable.",
"add-rulechain-text": "Add new rule chain",
"no-rulechains-text": "No rule chains found",
"rulechain-details": "Rule chain details",
"details": "Details",
"events": "Events",
"system": "System",
"import": "Import rule chain",
"export": "Export rule chain",
"export-failed-error": "Unable to export rule chain: {{error}}",
"create-new-rulechain": "Create new rule chain",
"rulechain-file": "Rule chain file",
"invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.",
"copyId": "Copy rule chain Id",
"idCopiedMessage": "Rule chain Id has been copied to clipboard",
"select-rulechain": "Select rule chain",
"no-rulechains-matching": "No rule chains matching '{{entity}}' were found.",
"rulechain-required": "Rule chain is required",
"management": "Rules management",
"debug-mode": "Debug mode"
}, },
"rule-plugin": { "rulenode": { // TODO
"management": "규칙 및 플러그인 관리" "details": "Details",
"events": "Events",
"search": "Search nodes",
"open-node-library": "Open node library",
"add": "Add rule node",
"name": "Name",
"name-required": "Name is required.",
"type": "Type",
"description": "Description",
"delete": "Delete rule node",
"select-all-objects": "Select all nodes and connections",
"deselect-all-objects": "Deselect all nodes and connections",
"delete-selected-objects": "Delete selected nodes and connections",
"delete-selected": "Delete selected",
"select-all": "Select all",
"copy-selected": "Copy selected",
"deselect-all": "Deselect all",
"rulenode-details": "Rule node details",
"debug-mode": "Debug mode",
"configuration": "Configuration",
"link": "Link",
"link-details": "Rule node link details",
"add-link": "Add link",
"link-label": "Link label",
"link-label-required": "Link label is required.",
"custom-link-label": "Custom link label",
"custom-link-label-required": "Custom link label is required.",
"type-filter": "Filter",
"type-filter-details": "Filter incoming messages with configured conditions",
"type-enrichment": "Enrichment",
"type-enrichment-details": "Add additional information into Message Metadata",
"type-transformation": "Transformation",
"type-transformation-details": "Change Message payload and Metadata",
"type-action": "Action",
"type-action-details": "Perform special action",
"type-external": "External",
"type-external-details": "Interacts with external system",
"type-rule-chain": "Rule Chain",
"type-rule-chain-details": "Forwards incoming messages to specified Rule Chain",
"type-input": "Input",
"type-input-details": "Logical input of Rule Chain, forwards incoming messages to next related Rule Node",
"directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.",
"ui-resources-load-error": "Failed to load configuration ui resources.",
"invalid-target-rulechain": "Unable to resolve target rule chain!",
"test-script-function": "Test script function",
"message": "Message",
"message-type": "Message type",
"message-type-required": "Message type is required",
"metadata": "Metadata",
"metadata-required": "Metadata entries can't be empty.",
"output": "Output",
"test": "Test",
"help": "Help"
}, },
"tenant": { "tenant": {
"tenants": "테넌트", "tenants": "테넌트",
@ -589,7 +1116,14 @@ export default function addLocaleKorean(locales) {
"delete-tenants-text": "선택된 테넌트가 삭제되고 관련된 모든 정보를 복구할 수 없으므로 주의하십시오.", "delete-tenants-text": "선택된 테넌트가 삭제되고 관련된 모든 정보를 복구할 수 없으므로 주의하십시오.",
"title": "타이틀", "title": "타이틀",
"title-required": "타이틀을 입력하세요.", "title-required": "타이틀을 입력하세요.",
"description": "설명" "description": "설명",
"details": "Details", // TODO
"events": "Events", // TODO
"copyId": "Copy tenant Id", // TODO
"idCopiedMessage": "Tenant Id has been copied to clipboard", // TODO
"select-tenant": "Select tenant", // TODO
"no-tenants-matching": "No tenants matching '{{entity}}' were found.",
"tenant-required": "Tenant is required" // TODO
}, },
"timeinterval": { "timeinterval": {
"seconds-interval": "{ seconds, select, 1 {1 second} other {# seconds} }", "seconds-interval": "{ seconds, select, 1 {1 second} other {# seconds} }",
@ -642,7 +1176,18 @@ export default function addLocaleKorean(locales) {
"last-name": "성", "last-name": "성",
"description": "설명", "description": "설명",
"default-dashboard": "기본 대시보드", "default-dashboard": "기본 대시보드",
"always-fullscreen": "항상 전체화면" "always-fullscreen": "항상 전체화면",
"select-user": "Select user", // TODO
"no-users-matching": "No users matching '{{entity}}' were found.", // TODO
"user-required": "User is required", // TODO
"activation-method": "Activation method", // TODO
"display-activation-link": "Display activation link", // TODO
"send-activation-mail": "Send activation mail", // TODO
"activation-link": "User activation link", // TODO
"activation-link-text": "In order to activate user use the following <a href='{{activationLink}}' target='_blank'>activation link</a> :", // TODO
"copy-activation-link": "Copy activation link", // TODO
"activation-link-copied-message": "User activation link has been copied to clipboard", // TODO
"details": "Details" // TODO
}, },
"value": { "value": {
"type": "Value type", "type": "Value type",
@ -707,6 +1252,18 @@ export default function addLocaleKorean(locales) {
"undo": "위젯 변경사항 취소", "undo": "위젯 변경사항 취소",
"export": "위젯 내보내기" "export": "위젯 내보내기"
}, },
"widget-action": { // TODO
"header-button": "Widget header button",
"open-dashboard-state": "Navigate to new dashboard state",
"update-dashboard-state": "Update current dashboard state",
"open-dashboard": "Navigate to other dashboard",
"custom": "Custom action",
"target-dashboard-state": "Target dashboard state",
"target-dashboard-state-required": "Target dashboard state is required",
"set-entity-from-widget": "Set entity from widget",
"target-dashboard": "Target dashboard",
"open-right-layout": "Open right dashboard layout (mobile view)"
},
"widgets-bundle": { "widgets-bundle": {
"current": "현재 번들", "current": "현재 번들",
"widgets-bundles": "위젯 번들", "widgets-bundles": "위젯 번들",
@ -770,6 +1327,20 @@ export default function addLocaleKorean(locales) {
"widget-type-file": "위젯 타입 파일", "widget-type-file": "위젯 타입 파일",
"invalid-widget-type-file-error": "위젯 타입을 가져오기 할 수 없습니다.: 잘못된 위젯 타입 데이터 구조입니다." "invalid-widget-type-file-error": "위젯 타입을 가져오기 할 수 없습니다.: 잘못된 위젯 타입 데이터 구조입니다."
}, },
"icon": { // TODO
"icon": "Icon",
"select-icon": "Select icon",
"material-icons": "Material icons",
"show-all": "Show all icons"
},
"custom": {
"widget-action": {
"action-cell-button": "Action cell button",
"row-click": "On row click",
"marker-click": "On marker click",
"tooltip-tag-action": "Tooltip tag action"
}
},
"language": { "language": {
"language": "언어", "language": "언어",
"en_US": "영어", "en_US": "영어",
@ -779,5 +1350,5 @@ export default function addLocaleKorean(locales) {
"es_ES": "스페인어" "es_ES": "스페인어"
} }
}; };
angular.extend(locales, {'ko_KR': ko_KR}); angular.extend(locales, { 'ko_KR': ko_KR });
} }

760
ui/src/app/locale/locale.constant-ru.js

@ -98,6 +98,159 @@ export default function addLocaleRussian(locales) {
"enable-tls": "Включить TLS", "enable-tls": "Включить TLS",
"send-test-mail": "Отправить пробное письмо" "send-test-mail": "Отправить пробное письмо"
}, },
"alarm": { // TODO
"alarm": "Alarm",
"alarms": "Alarms",
"select-alarm": "Select alarm",
"no-alarms-matching": "No alarms matching '{{entity}}' were found.",
"alarm-required": "Alarm is required",
"alarm-status": "Alarm status",
"search-status": {
"ANY": "Any",
"ACTIVE": "Active",
"CLEARED": "Cleared",
"ACK": "Acknowledged",
"UNACK": "Unacknowledged"
},
"display-status": {
"ACTIVE_UNACK": "Active Unacknowledged",
"ACTIVE_ACK": "Active Acknowledged",
"CLEARED_UNACK": "Cleared Unacknowledged",
"CLEARED_ACK": "Cleared Acknowledged"
},
"no-alarms-prompt": "No alarms found",
"created-time": "Created time",
"type": "Type",
"severity": "Severity",
"originator": "Originator",
"originator-type": "Originator type",
"details": "Details",
"status": "Status",
"alarm-details": "Alarm details",
"start-time": "Start time",
"end-time": "End time",
"ack-time": "Acknowledged time",
"clear-time": "Cleared time",
"severity-critical": "Critical",
"severity-major": "Major",
"severity-minor": "Minor",
"severity-warning": "Warning",
"severity-indeterminate": "Indeterminate",
"acknowledge": "Acknowledge",
"clear": "Clear",
"search": "Search alarms",
"selected-alarms": "{ count, select, 1 {1 alarm} other {# alarms} } selected",
"no-data": "No data to display",
"polling-interval": "Alarms polling interval (sec)",
"polling-interval-required": "Alarms polling interval is required.",
"min-polling-interval-message": "At least 1 sec polling interval is allowed.",
"aknowledge-alarms-title": "Acknowledge { count, select, 1 {1 alarm} other {# alarms} }",
"aknowledge-alarms-text": "Are you sure you want to acknowledge { count, select, 1 {1 alarm} other {# alarms} }?",
"clear-alarms-title": "Clear { count, select, 1 {1 alarm} other {# alarms} }",
"clear-alarms-text": "Are you sure you want to clear { count, select, 1 {1 alarm} other {# alarms} }?"
},
"alias": { // TODO
"add": "Add alias",
"edit": "Edit alias",
"name": "Alias name",
"name-required": "Alias name is required",
"duplicate-alias": "Alias with same name is already exists.",
"filter-type-single-entity": "Single entity",
"filter-type-entity-list": "Entity list",
"filter-type-entity-name": "Entity name",
"filter-type-state-entity": "Entity from dashboard state",
"filter-type-state-entity-description": "Entity taken from dashboard state parameters",
"filter-type-asset-type": "Asset type",
"filter-type-asset-type-description": "Assets of type '{{assetType}}'",
"filter-type-asset-type-and-name-description": "Assets of type '{{assetType}}' and with name starting with '{{prefix}}'",
"filter-type-device-type": "Device type",
"filter-type-device-type-description": "Devices of type '{{deviceType}}'",
"filter-type-device-type-and-name-description": "Devices of type '{{deviceType}}' and with name starting with '{{prefix}}'",
"filter-type-relations-query": "Relations query",
"filter-type-relations-query-description": "{{entities}} that have {{relationType}} relation {{direction}} {{rootEntity}}",
"filter-type-asset-search-query": "Asset search query",
"filter-type-asset-search-query-description": "Assets with types {{assetTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}",
"filter-type-device-search-query": "Device search query",
"filter-type-device-search-query-description": "Devices with types {{deviceTypes}} that have {{relationType}} relation {{direction}} {{rootEntity}}",
"entity-filter": "Entity filter",
"resolve-multiple": "Resolve as multiple entities",
"filter-type": "Filter type",
"filter-type-required": "Filter type is required.",
"entity-filter-no-entity-matched": "No entities matching specified filter were found.",
"no-entity-filter-specified": "No entity filter specified",
"root-state-entity": "Use dashboard state entity as root",
"root-entity": "Root entity",
"state-entity-parameter-name": "State entity parameter name",
"default-state-entity": "Default state entity",
"default-entity-parameter-name": "By default",
"max-relation-level": "Max relation level",
"unlimited-level": "Unlimited level",
"state-entity": "Dashboard state entity",
"all-entities": "All entities",
"any-relation": "any"
},
"asset": { // TODO
"asset": "Asset",
"assets": "Assets",
"management": "Asset management",
"view-assets": "View Assets",
"add": "Add Asset",
"assign-to-customer": "Assign to customer",
"assign-asset-to-customer": "Assign Asset(s) To Customer",
"assign-asset-to-customer-text": "Please select the assets to assign to the customer",
"no-assets-text": "No assets found",
"assign-to-customer-text": "Please select the customer to assign the asset(s)",
"public": "Public",
"assignedToCustomer": "Assigned to customer",
"make-public": "Make asset public",
"make-private": "Make asset private",
"unassign-from-customer": "Unassign from customer",
"delete": "Delete asset",
"asset-public": "Asset is public",
"asset-type": "Asset type",
"asset-type-required": "Asset type is required.",
"select-asset-type": "Select asset type",
"enter-asset-type": "Enter asset type",
"any-asset": "Any asset",
"no-asset-types-matching": "No asset types matching '{{entitySubtype}}' were found.",
"asset-type-list-empty": "No asset types selected.",
"asset-types": "Asset types",
"name": "Name",
"name-required": "Name is required.",
"description": "Description",
"type": "Type",
"type-required": "Type is required.",
"details": "Details",
"events": "Events",
"add-asset-text": "Add new asset",
"asset-details": "Asset details",
"assign-assets": "Assign assets",
"assign-assets-text": "Assign { count, select, 1 {1 asset} other {# assets} } to customer",
"delete-assets": "Delete assets",
"unassign-assets": "Unassign assets",
"unassign-assets-action-title": "Unassign { count, select, 1 {1 asset} other {# assets} } from customer",
"assign-new-asset": "Assign new asset",
"delete-asset-title": "Are you sure you want to delete the asset '{{assetName}}'?",
"delete-asset-text": "Be careful, after the confirmation the asset and all related data will become unrecoverable.",
"delete-assets-title": "Are you sure you want to delete { count, select, 1 {1 asset} other {# assets} }?",
"delete-assets-action-title": "Delete { count, select, 1 {1 asset} other {# assets} }",
"delete-assets-text": "Be careful, after the confirmation all selected assets will be removed and all related data will become unrecoverable.",
"make-public-asset-title": "Are you sure you want to make the asset '{{assetName}}' public?",
"make-public-asset-text": "After the confirmation the asset and all its data will be made public and accessible by others.",
"make-private-asset-title": "Are you sure you want to make the asset '{{assetName}}' private?",
"make-private-asset-text": "After the confirmation the asset and all its data will be made private and won't be accessible by others.",
"unassign-asset-title": "Are you sure you want to unassign the asset '{{assetName}}'?",
"unassign-asset-text": "After the confirmation the asset will be unassigned and won't be accessible by the customer.",
"unassign-asset": "Unassign asset",
"unassign-assets-title": "Are you sure you want to unassign { count, select, 1 {1 asset} other {# assets} }?",
"unassign-assets-text": "After the confirmation all selected assets will be unassigned and won't be accessible by the customer.",
"copyId": "Copy asset Id",
"idCopiedMessage": "Asset Id has been copied to clipboard",
"select-asset": "Select asset",
"no-assets-matching": "No assets matching '{{entity}}' were found.",
"asset-required": "Asset is required",
"name-starts-with": "Asset name starts with"
},
"attribute": { "attribute": {
"attributes": "Атрибуты", "attributes": "Атрибуты",
"latest-telemetry": "Последняя телеметрия", "latest-telemetry": "Последняя телеметрия",
@ -124,6 +277,38 @@ export default function addLocaleRussian(locales) {
"selected-attributes": "{ count, plural, 1 {Выбран} other {Выбраны} } { count, plural, one {1 атрибут} few {# атрибута} other {# атрибутов} }", "selected-attributes": "{ count, plural, 1 {Выбран} other {Выбраны} } { count, plural, one {1 атрибут} few {# атрибута} other {# атрибутов} }",
"selected-telemetry": "{ count, plural, 1 {Выбран} other {Выбраны} } { count, plural, 1 {1 параметр} few {# параметра} other {# параметров} } телеметрии" "selected-telemetry": "{ count, plural, 1 {Выбран} other {Выбраны} } { count, plural, 1 {1 параметр} few {# параметра} other {# параметров} } телеметрии"
}, },
"audit-log": { // TODO
"audit": "Audit",
"audit-logs": "Audit Logs",
"timestamp": "Timestamp",
"entity-type": "Entity Type",
"entity-name": "Entity Name",
"user": "User",
"type": "Type",
"status": "Status",
"details": "Details",
"type-added": "Added",
"type-deleted": "Deleted",
"type-updated": "Updated",
"type-attributes-updated": "Attributes updated",
"type-attributes-deleted": "Attributes deleted",
"type-rpc-call": "RPC call",
"type-credentials-updated": "Credentials updated",
"type-assigned-to-customer": "Assigned to Customer",
"type-unassigned-from-customer": "Unassigned from Customer",
"type-activated": "Activated",
"type-suspended": "Suspended",
"type-credentials-read": "Credentials read",
"type-attributes-read": "Attributes read",
"status-success": "Success",
"status-failure": "Failure",
"audit-log-details": "Audit log details",
"no-audit-logs-prompt": "No logs found",
"action-data": "Action data",
"failure-details": "Failure details",
"search": "Search audit logs",
"clear-search": "Clear search"
},
"confirm-on-exit": { "confirm-on-exit": {
"message": "У вас есть несохраненные изменения. Вы точно хотите покинуть эту страницу?", "message": "У вас есть несохраненные изменения. Вы точно хотите покинуть эту страницу?",
"html-message": "У вас есть несохраненные изменения.<br/>Вы точно хотите покинуть эту страницу?", "html-message": "У вас есть несохраненные изменения.<br/>Вы точно хотите покинуть эту страницу?",
@ -148,6 +333,11 @@ export default function addLocaleRussian(locales) {
"enter-password": "Введите пароль", "enter-password": "Введите пароль",
"enter-search": "Введите условие поиска" "enter-search": "Введите условие поиска"
}, },
"content-type": { // TODO
"json": "Json",
"text": "Text",
"binary": "Binary (Base64)"
},
"customer": { "customer": {
"customers": "Клиенты", "customers": "Клиенты",
"management": "Управление клиентами", "management": "Управление клиентами",
@ -172,11 +362,22 @@ export default function addLocaleRussian(locales) {
"delete-customers-action-title": "Удалить { count, plural, one {1 клиента} other {# клиентов} } }", "delete-customers-action-title": "Удалить { count, plural, one {1 клиента} other {# клиентов} } }",
"delete-customers-text": "Внимание, после подтверждения клиенты и вся связанная с ними информация будут безвозвратно утеряны.", "delete-customers-text": "Внимание, после подтверждения клиенты и вся связанная с ними информация будут безвозвратно утеряны.",
"manage-users": "Управление пользователями", "manage-users": "Управление пользователями",
"manage-assets": "Manage assets", // TODO
"manage-devices": "Управление устройствами", "manage-devices": "Управление устройствами",
"manage-dashboards": "Управление дашбордами", "manage-dashboards": "Управление дашбордами",
"title": "Имя", "title": "Имя",
"title-required": "Название обязательно.", "title-required": "Название обязательно.",
"description": "Описание" "description": "Описание",
"details": "Details", // TODO
"events": "Events", // TODO
"copyId": "Copy customer Id", // TODO
"idCopiedMessage": "Customer Id has been copied to clipboard", // TODO
"select-customer": "Select customer", // TODO
"no-customers-matching": "No customers matching '{{entity}}' were found.", // TODO
"customer-required": "Customer is required", // TODO
"select-default-customer": "Select default customer", // TODO
"default-customer": "Default customer", // TODO
"default-customer-required": "Default customer is required in order to debug dashboard on Tenant level" // TODO
}, },
"datetime": { "datetime": {
"date-from": "Дата с", "date-from": "Дата с",
@ -282,19 +483,41 @@ export default function addLocaleRussian(locales) {
"configuration-error": "Ошибка конфигурирования", "configuration-error": "Ошибка конфигурирования",
"alias-resolution-error-title": "Ошибка конфигурирования псевдонимов дашборда", "alias-resolution-error-title": "Ошибка конфигурирования псевдонимов дашборда",
"invalid-aliases-config": "Не удалось найти устройства, соответствующие фильтру псевдонимов.<br/>" + "invalid-aliases-config": "Не удалось найти устройства, соответствующие фильтру псевдонимов.<br/>" +
"Пожалуйста, свяжитесь с администратором для устранения этой проблемы.", "Пожалуйста, свяжитесь с администратором для устранения этой проблемы.",
"select-devices": "Выберите устройства", "select-devices": "Выберите устройства",
"assignedToCustomer": "Прикреплен к клиенту", "assignedToCustomer": "Прикреплен к клиенту",
"public": "Общедоступный", "public": "Общедоступный",
"public-link": "Общедоступная ссылка", "public-link": "Общедоступная ссылка",
"copy-public-link": "Скопировать общедоступную ссылку", "copy-public-link": "Скопировать общедоступную ссылку",
"public-link-copied-message": "Общедоступная ссылка на дашборд скопирована в буфер обмена" "public-link-copied-message": "Общедоступная ссылка на дашборд скопирована в буфер обмена",
"manage-states": "Manage dashboard states", // TODO
"states": "Dashboard states", // TODO
"search-states": "Search dashboard states", // TODO
"selected-states": "{ count, select, 1 {1 dashboard state} other {# dashboard states} } selected", // TODO
"edit-state": "Edit dashboard state", // TODO
"delete-state": "Delete dashboard state", // TODO
"add-state": "Add dashboard state", // TODO
"state": "Dashboard state", // TODO
"state-name": "Name", // TODO
"state-name-required": "Dashboard state name is required.", // TODO
"state-id": "State Id", // TODO
"state-id-required": "Dashboard state id is required.", // TODO
"state-id-exists": "Dashboard state with the same id is already exists.", // TODO
"is-root-state": "Root state", // TODO
"delete-state-title": "Delete dashboard state", // TODO
"delete-state-text": "Are you sure you want delete dashboard state with name '{{stateName}}'?", // TODO
"show-details": "Show details", // TODO
"hide-details": "Hide details", // TODO
"select-state": "Select target state", // TODO
"state-controller": "State controller" // TODO
}, },
"datakey": { "datakey": {
"settings": "Настройки", "settings": "Настройки",
"advanced": "Дополнительно", "advanced": "Дополнительно",
"label": "Метка", "label": "Метка",
"color": "Цвет", "color": "Цвет",
"units": "Special symbol to show next to value", // TODO
"decimals": "Number of digits after floating point", // TODO
"data-generation-func": "Функция генерации данных", "data-generation-func": "Функция генерации данных",
"use-data-post-processing-func": "Использовать функцию пост-обработки данных", "use-data-post-processing-func": "Использовать функцию пост-обработки данных",
"configuration": "Конфигурация ключа данных", "configuration": "Конфигурация ключа данных",
@ -302,8 +525,11 @@ export default function addLocaleRussian(locales) {
"attributes": "Атрибуты", "attributes": "Атрибуты",
"timeseries-required": "Выборка по времени обязательна.", "timeseries-required": "Выборка по времени обязательна.",
"timeseries-or-attributes-required": "Выборка по времени/атрибуты обязательны.", "timeseries-or-attributes-required": "Выборка по времени/атрибуты обязательны.",
"maximum-timeseries-or-attributes": "Maximum { count, select, 1 {1 timeseries/attribute is allowed.} other {# timeseries/attributes are allowed} }", // TODO
"alarm-fields-required": "Alarm fields are required.", // TODO
"function-types": "Тип функции", "function-types": "Тип функции",
"function-types-required": "Тип функции обязателен." "function-types-required": "Тип функции обязателен.",
"maximum-function-types": "Maximum { count, select, 1 {1 function type is allowed.} other {# function types are allowed} }" // TODO
}, },
"datasource": { "datasource": {
"type": "Тип источника данных", "type": "Тип источника данных",
@ -408,11 +634,101 @@ export default function addLocaleRussian(locales) {
"unhandled-error-code": "Код необработанной ошибки: {{errorCode}}", "unhandled-error-code": "Код необработанной ошибки: {{errorCode}}",
"unknown-error": "Неизвестная ошибка" "unknown-error": "Неизвестная ошибка"
}, },
"entity": { // TODO
"entity": "Entity",
"entities": "Entities",
"aliases": "Entity aliases",
"entity-alias": "Entity alias",
"unable-delete-entity-alias-title": "Unable to delete entity alias",
"unable-delete-entity-alias-text": "Entity alias '{{entityAlias}}' can't be deleted as it used by the following widget(s):<br/>{{widgetsList}}",
"duplicate-alias-error": "Duplicate alias found '{{alias}}'.<br>Entity aliases must be unique whithin the dashboard.",
"missing-entity-filter-error": "Filter is missing for alias '{{alias}}'.",
"configure-alias": "Configure '{{alias}}' alias",
"alias": "Alias",
"alias-required": "Entity alias is required.",
"remove-alias": "Remove entity alias",
"add-alias": "Add entity alias",
"entity-list": "Entity list",
"entity-type": "Entity type",
"entity-types": "Entity types",
"entity-type-list": "Entity type list",
"any-entity": "Any entity",
"enter-entity-type": "Enter entity type",
"no-entities-matching": "No entities matching '{{entity}}' were found.",
"no-entity-types-matching": "No entity types matching '{{entityType}}' were found.",
"name-starts-with": "Name starts with",
"use-entity-name-filter": "Use filter",
"entity-list-empty": "No entities selected.",
"entity-type-list-empty": "No entity types selected.",
"entity-name-filter-required": "Entity name filter is required.",
"entity-name-filter-no-entity-matched": "No entities starting with '{{entity}}' were found.",
"all-subtypes": "All",
"select-entities": "Select entities",
"no-aliases-found": "No aliases found.",
"no-alias-matching": "'{{alias}}' not found.",
"create-new-alias": "Create a new one!",
"key": "Key",
"key-name": "Key name",
"no-keys-found": "No keys found.",
"no-key-matching": "'{{key}}' not found.",
"create-new-key": "Create a new one!",
"type": "Type",
"type-required": "Entity type is required.",
"type-device": "Device",
"type-devices": "Devices",
"list-of-devices": "{ count, select, 1 {One device} other {List of # devices} }",
"device-name-starts-with": "Devices whose names start with '{{prefix}}'",
"type-asset": "Asset",
"type-assets": "Assets",
"list-of-assets": "{ count, select, 1 {One asset} other {List of # assets} }",
"asset-name-starts-with": "Assets whose names start with '{{prefix}}'",
"type-rule": "Rule",
"type-rules": "Rules",
"list-of-rules": "{ count, select, 1 {One rule} other {List of # rules} }",
"rule-name-starts-with": "Rules whose names start with '{{prefix}}'",
"type-plugin": "Plugin",
"type-plugins": "Plugins",
"list-of-plugins": "{ count, select, 1 {One plugin} other {List of # plugins} }",
"plugin-name-starts-with": "Plugins whose names start with '{{prefix}}'",
"type-tenant": "Tenant",
"type-tenants": "Tenants",
"list-of-tenants": "{ count, select, 1 {One tenant} other {List of # tenants} }",
"tenant-name-starts-with": "Tenants whose names start with '{{prefix}}'",
"type-customer": "Customer",
"type-customers": "Customers",
"list-of-customers": "{ count, select, 1 {One customer} other {List of # customers} }",
"customer-name-starts-with": "Customers whose names start with '{{prefix}}'",
"type-user": "User",
"type-users": "Users",
"list-of-users": "{ count, select, 1 {One user} other {List of # users} }",
"user-name-starts-with": "Users whose names start with '{{prefix}}'",
"type-dashboard": "Dashboard",
"type-dashboards": "Dashboards",
"list-of-dashboards": "{ count, select, 1 {One dashboard} other {List of # dashboards} }",
"dashboard-name-starts-with": "Dashboards whose names start with '{{prefix}}'",
"type-alarm": "Alarm",
"type-alarms": "Alarms",
"list-of-alarms": "{ count, select, 1 {One alarms} other {List of # alarms} }",
"alarm-name-starts-with": "Alarms whose names start with '{{prefix}}'",
"type-rulechain": "Rule chain",
"type-rulechains": "Rule chains",
"list-of-rulechains": "{ count, select, 1 {One rule chain} other {List of # rule chains} }",
"rulechain-name-starts-with": "Rule chains whose names start with '{{prefix}}'",
"type-current-customer": "Current Customer",
"search": "Search entities",
"selected-entities": "{ count, select, 1 {1 entity} other {# entities} } selected",
"entity-name": "Entity name",
"details": "Entity details",
"no-entities-prompt": "No entities found",
"no-data": "No data to display"
},
"event": { "event": {
"event-type": "Тип события", "event-type": "Тип события",
"type-error": "Ошибка", "type-error": "Ошибка",
"type-lc-event": "Событие жизненного цикла", "type-lc-event": "Событие жизненного цикла",
"type-stats": "Статистика", "type-stats": "Статистика",
"type-debug-rule-node": "Debug", // TODO
"type-debug-rule-chain": "Debug", // TODO
"no-events-prompt": "События не найдены", "no-events-prompt": "События не найдены",
"error": "Ошибка", "error": "Ошибка",
"alarm": "Аварийное оповещение", "alarm": "Аварийное оповещение",
@ -420,6 +736,14 @@ export default function addLocaleRussian(locales) {
"server": "Сервер", "server": "Сервер",
"body": "Тело", "body": "Тело",
"method": "Метод", "method": "Метод",
"type": "Type", // TODO
"entity": "Entity", // TODO
"message-id": "Message Id", // TODO
"message-type": "Message Type", // TODO
"data-type": "Data Type", // TODO
"relation-type": "Relation Type", // TODO
"metadata": "Metadata", // TODO
"data": "Data", // TODO
"event": "Событие", "event": "Событие",
"status": "Статус", "status": "Статус",
"success": "Успех", "success": "Успех",
@ -427,6 +751,163 @@ export default function addLocaleRussian(locales) {
"messages-processed": "Сообщения обработаны", "messages-processed": "Сообщения обработаны",
"errors-occurred": "Возникли ошибки" "errors-occurred": "Возникли ошибки"
}, },
"extension": { // TODO
"extensions": "Extensions",
"selected-extensions": "{ count, select, 1 {1 extension} other {# extensions} } selected",
"type": "Type",
"key": "Key",
"value": "Value",
"id": "Id",
"extension-id": "Extension id",
"extension-type": "Extension type",
"transformer-json": "JSON *",
"unique-id-required": "Current extension id already exists.",
"delete": "Delete extension",
"add": "Add extension",
"edit": "Edit extension",
"delete-extension-title": "Are you sure you want to delete the extension '{{extensionId}}'?",
"delete-extension-text": "Be careful, after the confirmation the extension and all related data will become unrecoverable.",
"delete-extensions-title": "Are you sure you want to delete { count, select, 1 {1 extension} other {# extensions} }?",
"delete-extensions-text": "Be careful, after the confirmation all selected extensions will be removed.",
"converters": "Converters",
"converter-id": "Converter id",
"configuration": "Configuration",
"converter-configurations": "Converter configurations",
"token": "Security token",
"add-converter": "Add converter",
"add-config": "Add converter configuration",
"device-name-expression": "Device name expression",
"device-type-expression": "Device type expression",
"custom": "Custom",
"to-double": "To Double",
"transformer": "Transformer",
"json-required": "Transformer json is required.",
"json-parse": "Unable to parse transformer json.",
"attributes": "Attributes",
"add-attribute": "Add attribute",
"add-map": "Add mapping element",
"timeseries": "Timeseries",
"add-timeseries": "Add timeseries",
"field-required": "Field is required",
"brokers": "Brokers",
"add-broker": "Add broker",
"host": "Host",
"port": "Port",
"port-range": "Port should be in a range from 1 to 65535.",
"ssl": "Ssl",
"credentials": "Credentials",
"username": "Username",
"password": "Password",
"retry-interval": "Retry interval in milliseconds",
"anonymous": "Anonymous",
"basic": "Basic",
"pem": "PEM",
"ca-cert": "CA certificate file *",
"private-key": "Private key file *",
"cert": "Certificate file *",
"no-file": "No file selected.",
"drop-file": "Drop a file or click to select a file to upload.",
"mapping": "Mapping",
"topic-filter": "Topic filter",
"converter-type": "Converter type",
"converter-json": "Json",
"json-name-expression": "Device name json expression",
"topic-name-expression": "Device name topic expression",
"json-type-expression": "Device type json expression",
"topic-type-expression": "Device type topic expression",
"attribute-key-expression": "Attribute key expression",
"attr-json-key-expression": "Attribute key json expression",
"attr-topic-key-expression": "Attribute key topic expression",
"request-id-expression": "Request id expression",
"request-id-json-expression": "Request id json expression",
"request-id-topic-expression": "Request id topic expression",
"response-topic-expression": "Response topic expression",
"value-expression": "Value expression",
"topic": "Topic",
"timeout": "Timeout in milliseconds",
"converter-json-required": "Converter json is required.",
"converter-json-parse": "Unable to parse converter json.",
"filter-expression": "Filter expression",
"connect-requests": "Connect requests",
"add-connect-request": "Add connect request",
"disconnect-requests": "Disconnect requests",
"add-disconnect-request": "Add disconnect request",
"attribute-requests": "Attribute requests",
"add-attribute-request": "Add attribute request",
"attribute-updates": "Attribute updates",
"add-attribute-update": "Add attribute update",
"server-side-rpc": "Server side RPC",
"add-server-side-rpc-request": "Add server-side RPC request",
"device-name-filter": "Device name filter",
"attribute-filter": "Attribute filter",
"method-filter": "Method filter",
"request-topic-expression": "Request topic expression",
"response-timeout": "Response timeout in milliseconds",
"topic-expression": "Topic expression",
"client-scope": "Client scope",
"add-device": "Add device",
"opc-server": "Servers",
"opc-add-server": "Add server",
"opc-add-server-prompt": "Please add server",
"opc-application-name": "Application name",
"opc-application-uri": "Application uri",
"opc-scan-period-in-seconds": "Scan period in seconds",
"opc-security": "Security",
"opc-identity": "Identity",
"opc-keystore": "Keystore",
"opc-type": "Type",
"opc-keystore-type": "Type",
"opc-keystore-location": "Location *",
"opc-keystore-password": "Password",
"opc-keystore-alias": "Alias",
"opc-keystore-key-password": "Key password",
"opc-device-node-pattern": "Device node pattern",
"opc-device-name-pattern": "Device name pattern",
"modbus-server": "Servers/slaves",
"modbus-add-server": "Add server/slave",
"modbus-add-server-prompt": "Please add server/slave",
"modbus-transport": "Transport",
"modbus-port-name": "Serial port name",
"modbus-encoding": "Encoding",
"modbus-parity": "Parity",
"modbus-baudrate": "Baud rate",
"modbus-databits": "Data bits",
"modbus-stopbits": "Stop bits",
"modbus-databits-range": "Data bits should be in a range from 7 to 8.",
"modbus-stopbits-range": "Stop bits should be in a range from 1 to 2.",
"modbus-unit-id": "Unit ID",
"modbus-unit-id-range": "Unit ID should be in a range from 1 to 247.",
"modbus-device-name": "Device name",
"modbus-poll-period": "Poll period (ms)",
"modbus-attributes-poll-period": "Attributes poll period (ms)",
"modbus-timeseries-poll-period": "Timeseries poll period (ms)",
"modbus-poll-period-range": "Poll period should be positive value.",
"modbus-tag": "Tag",
"modbus-function": "Function",
"modbus-register-address": "Register address",
"modbus-register-address-range": "Register address should be in a range from 0 to 65535.",
"modbus-register-bit-index": "Bit index",
"modbus-register-bit-index-range": "Bit index should be in a range from 0 to 15.",
"modbus-register-count": "Register count",
"modbus-register-count-range": "Register count should be a positive value.",
"modbus-byte-order": "Byte order",
"sync": {
"status": "Status",
"sync": "Sync",
"not-sync": "Not sync",
"last-sync-time": "Last sync time",
"not-available": "Not available"
},
"export-extensions-configuration": "Export extensions configuration",
"import-extensions-configuration": "Import extensions configuration",
"import-extensions": "Import extensions",
"import-extension": "Import extension",
"export-extension": "Export extension",
"file": "Extensions file",
"invalid-file-error": "Invalid extension file"
},
"fullscreen": { "fullscreen": {
"expand": "Во весь экран", "expand": "Во весь экран",
"exit": "Выйти из полноэкранного режима", "exit": "Выйти из полноэкранного режима",
@ -469,7 +950,24 @@ export default function addLocaleRussian(locales) {
}, },
"js-func": { "js-func": {
"no-return-error": "Функция должна возвращать значение!", "no-return-error": "Функция должна возвращать значение!",
"return-type-mismatch": "Функция должна возвращать значение типа '{{type}}'!" "return-type-mismatch": "Функция должна возвращать значение типа '{{type}}'!",
"tidy": "Tidy" // TODO
},
"key-val": { // TODO
"key": "Key",
"value": "Value",
"remove-entry": "Remove entry",
"add-entry": "Add entry",
"no-data": "No entries"
},
"layout": { // TODO
"layout": "Layout",
"manage": "Manage layouts",
"settings": "Layout settings",
"color": "Color",
"main": "Main",
"right": "Right",
"select": "Select target layout"
}, },
"legend": { "legend": {
"position": "Расположение легенды", "position": "Расположение легенды",
@ -500,45 +998,6 @@ export default function addLocaleRussian(locales) {
"password-link-sent-message": "Ссылка для сброса пароля была успешно отправлена!", "password-link-sent-message": "Ссылка для сброса пароля была успешно отправлена!",
"email": "Эл. адрес" "email": "Эл. адрес"
}, },
"plugin": {
"plugins": "Плагины",
"delete": "Удалить плагин",
"activate": "Активировать плагин",
"suspend": "Приостановить плагин",
"active": "Активный",
"suspended": "Приостановлен",
"name": "Название",
"name-required": "Название обязательно.",
"description": "Описание",
"add": "Добавить плагин",
"delete-plugin-title": "Вы точно хотите удалить плагин '{{pluginName}}'?",
"delete-plugin-text": "Внимание, после подтверждения плагин и все связанные с ним данные будут безвозвратно утеряны.",
"delete-plugins-title": "Вы точно хотите удалить { count, plural, one {1 плагин} few {# плагина} other {# плагинов} }?",
"delete-plugins-action-title": "Удалить { count, plural, one {1 плагин} few {# плагина} other {# плагинов} } }",
"delete-plugins-text": "Внимание, после подтверждения выбранные плагины и все связанные с ними данные будут безвозвратно утеряны.",
"add-plugin-text": "Добавить новый плагин",
"no-plugins-text": "Плагины не найдены",
"plugin-details": "Подробности о плагине",
"api-token": "API токен",
"api-token-required": "API токен обязателен.",
"type": "Тип плагина",
"type-required": "Тип плагина обязателен.",
"configuration": "Настройки плагина",
"system": "Системный",
"select-plugin": "Выберите плагин",
"plugin": "Плагин",
"no-plugins-matching": "Плагин '{{entity}}' не найден.",
"plugin-required": "Плагин обязателен.",
"plugin-require-match": "Пожалуйста, выберите существующий плагин.",
"events": "События",
"details": "Подробности",
"import": "Импортировать плагин",
"export": "Экспортировать плагин",
"export-failed-error": "Не удалось экспортировать плагин: {{error}}",
"create-new-plugin": "Создать новый плагин",
"plugin-file": "Файл плагина",
"invalid-plugin-file-error": "Не удалось импортировать плагин: неизвестная схема данных плагина."
},
"position": { "position": {
"top": "Верх", "top": "Верх",
"bottom": "Низ", "bottom": "Низ",
@ -550,60 +1009,139 @@ export default function addLocaleRussian(locales) {
"change-password": "Изменить пароль", "change-password": "Изменить пароль",
"current-password": "Текущий пароль" "current-password": "Текущий пароль"
}, },
"rule": { "relation": { // TODO
"rules": "Правила", "relations": "Relations",
"delete": "Удалить правило", "direction": "Direction",
"activate": "Активировать правило", "search-direction": {
"suspend": "Приостановить правило", "FROM": "From",
"active": "Активное", "TO": "To"
"suspended": "Приостановлены", },
"name": "Название", "direction-type": {
"name-required": "Название обязательно.", "FROM": "from",
"description": "Описание", "TO": "to"
"add": "Добавить правило", },
"delete-rule-title": "Вы точно хотите удалить правило '{{ruleName}}'?", "from-relations": "Outbound relations",
"delete-rule-text": "Внимание, после подтверждения правило и все связанные с ним данные будут безвозвратно утеряны.", "to-relations": "Inbound relations",
"delete-rules-title": "Вы точно хотите удалить { count, plural, one {1 правило} few {# правила} other {# правил} }?", "selected-relations": "{ count, select, 1 {1 relation} other {# relations} } selected",
"delete-rules-action-title": "Удалить { count, plural, one {1 правило} few {# правила} other {# правил} }", "type": "Type",
"delete-rules-text": "Внимание, после подтверждения выбранные правила и все связанные с ними данные будут безвозвратно утеряны.", "to-entity-type": "To entity type",
"add-rule-text": "Добавить новое правило", "to-entity-name": "To entity name",
"no-rules-text": "Правила не найдены", "from-entity-type": "From entity type",
"rule-details": "Подробности о правиле", "from-entity-name": "From entity name",
"filters": "Фильтры", "to-entity": "To entity",
"filter": "Фильтр", "from-entity": "From entity",
"add-filter-prompt": "Пожалуйста, добавьте фильтр", "delete": "Delete relation",
"remove-filter": "Удалить фильтр", "relation-type": "Relation type",
"add-filter": "Добавить фильтр", "relation-type-required": "Relation type is required.",
"filter-name": "Название фильтра", "any-relation-type": "Any type",
"filter-type": "Тип фильтра", "add": "Add relation",
"edit-filter": "Редактировать фильтр", "edit": "Edit relation",
"view-filter": "Просмотреть фильтр", "delete-to-relation-title": "Are you sure you want to delete relation to the entity '{{entityName}}'?",
"component-name": "Название", "delete-to-relation-text": "Be careful, after the confirmation the entity '{{entityName}}' will be unrelated from the current entity.",
"component-name-required": "Название обязательно.", "delete-to-relations-title": "Are you sure you want to delete { count, select, 1 {1 relation} other {# relations} }?",
"component-type": "Тип", "delete-to-relations-text": "Be careful, after the confirmation all selected relations will be removed and corresponding entities will be unrelated from the current entity.",
"component-type-required": "Тип обязателен.", "delete-from-relation-title": "Are you sure you want to delete relation from the entity '{{entityName}}'?",
"processor": "Обработчик", "delete-from-relation-text": "Be careful, after the confirmation current entity will be unrelated from the entity '{{entityName}}'.",
"no-processor-configured": "Обработчики не сконфигурированы", "delete-from-relations-title": "Are you sure you want to delete { count, select, 1 {1 relation} other {# relations} }?",
"create-processor": "Создать обработчик", "delete-from-relations-text": "Be careful, after the confirmation all selected relations will be removed and current entity will be unrelated from the corresponding entities.",
"processor-name": "Название обработчика", "remove-relation-filter": "Remove relation filter",
"processor-type": "Тип обработчика", "add-relation-filter": "Add relation filter",
"plugin-action": "Действие плагина", "any-relation": "Any relation",
"action-name": "Название действия", "relation-filters": "Relation filters",
"action-type": "Тип действия", "additional-info": "Additional info (JSON)",
"create-action-prompt": "Пожалуйста, создайте действие", "invalid-additional-info": "Unable to parse additional info json."
"create-action": "Создать действие", },
"details": "Подробности", "rulechain": { // TODO
"events": "События", "rulechain": "Rule chain",
"system": "Системное", "rulechains": "Rule chains",
"import": "Импортировать правило", "root": "Root",
"export": "Экспортировать правило", "delete": "Delete rule chain",
"export-failed-error": "Не удалось экспортировать правило: {{error}}", "name": "Name",
"create-new-rule": "Создать новое правило", "name-required": "Name is required.",
"rule-file": "Файл правила", "description": "Description",
"invalid-rule-file-error": "Не удалось импортировать правило: неизвестная схема данных правила." "add": "Add Rule Chain",
"set-root": "Make rule chain root",
"set-root-rulechain-title": "Are you sure you want to make the rule chain '{{ruleChainName}}' root?",
"set-root-rulechain-text": "After the confirmation the rule chain will become root and will handle all incoming transport messages.",
"delete-rulechain-title": "Are you sure you want to delete the rule chain '{{ruleChainName}}'?",
"delete-rulechain-text": "Be careful, after the confirmation the rule chain and all related data will become unrecoverable.",
"delete-rulechains-title": "Are you sure you want to delete { count, select, 1 {1 rule chain} other {# rule chains} }?",
"delete-rulechains-action-title": "Delete { count, select, 1 {1 rule chain} other {# rule chains} }",
"delete-rulechains-text": "Be careful, after the confirmation all selected rule chains will be removed and all related data will become unrecoverable.",
"add-rulechain-text": "Add new rule chain",
"no-rulechains-text": "No rule chains found",
"rulechain-details": "Rule chain details",
"details": "Details",
"events": "Events",
"system": "System",
"import": "Import rule chain",
"export": "Export rule chain",
"export-failed-error": "Unable to export rule chain: {{error}}",
"create-new-rulechain": "Create new rule chain",
"rulechain-file": "Rule chain file",
"invalid-rulechain-file-error": "Unable to import rule chain: Invalid rule chain data structure.",
"copyId": "Copy rule chain Id",
"idCopiedMessage": "Rule chain Id has been copied to clipboard",
"select-rulechain": "Select rule chain",
"no-rulechains-matching": "No rule chains matching '{{entity}}' were found.",
"rulechain-required": "Rule chain is required",
"management": "Rules management",
"debug-mode": "Debug mode"
}, },
"rule-plugin": { "rulenode": { // TODO
"management": "Управление плагинами и правилами" "details": "Details",
"events": "Events",
"search": "Search nodes",
"open-node-library": "Open node library",
"add": "Add rule node",
"name": "Name",
"name-required": "Name is required.",
"type": "Type",
"description": "Description",
"delete": "Delete rule node",
"select-all-objects": "Select all nodes and connections",
"deselect-all-objects": "Deselect all nodes and connections",
"delete-selected-objects": "Delete selected nodes and connections",
"delete-selected": "Delete selected",
"select-all": "Select all",
"copy-selected": "Copy selected",
"deselect-all": "Deselect all",
"rulenode-details": "Rule node details",
"debug-mode": "Debug mode",
"configuration": "Configuration",
"link": "Link",
"link-details": "Rule node link details",
"add-link": "Add link",
"link-label": "Link label",
"link-label-required": "Link label is required.",
"custom-link-label": "Custom link label",
"custom-link-label-required": "Custom link label is required.",
"type-filter": "Filter",
"type-filter-details": "Filter incoming messages with configured conditions",
"type-enrichment": "Enrichment",
"type-enrichment-details": "Add additional information into Message Metadata",
"type-transformation": "Transformation",
"type-transformation-details": "Change Message payload and Metadata",
"type-action": "Action",
"type-action-details": "Perform special action",
"type-external": "External",
"type-external-details": "Interacts with external system",
"type-rule-chain": "Rule Chain",
"type-rule-chain-details": "Forwards incoming messages to specified Rule Chain",
"type-input": "Input",
"type-input-details": "Logical input of Rule Chain, forwards incoming messages to next related Rule Node",
"directive-is-not-loaded": "Defined configuration directive '{{directiveName}}' is not available.",
"ui-resources-load-error": "Failed to load configuration ui resources.",
"invalid-target-rulechain": "Unable to resolve target rule chain!",
"test-script-function": "Test script function",
"message": "Message",
"message-type": "Message type",
"message-type-required": "Message type is required",
"metadata": "Metadata",
"metadata-required": "Metadata entries can't be empty.",
"output": "Output",
"test": "Test",
"help": "Help"
}, },
"tenant": { "tenant": {
"tenants": "Владельцы", "tenants": "Владельцы",
@ -740,6 +1278,18 @@ export default function addLocaleRussian(locales) {
"undo": "Откатить изменения в виджете", "undo": "Откатить изменения в виджете",
"export": "Экспортировать виджет" "export": "Экспортировать виджет"
}, },
"widget-action": { // TODO
"header-button": "Widget header button",
"open-dashboard-state": "Navigate to new dashboard state",
"update-dashboard-state": "Update current dashboard state",
"open-dashboard": "Navigate to other dashboard",
"custom": "Custom action",
"target-dashboard-state": "Target dashboard state",
"target-dashboard-state-required": "Target dashboard state is required",
"set-entity-from-widget": "Set entity from widget",
"target-dashboard": "Target dashboard",
"open-right-layout": "Open right dashboard layout (mobile view)"
},
"widgets-bundle": { "widgets-bundle": {
"current": "Текущий набор", "current": "Текущий набор",
"widgets-bundles": "Наборы виджетов", "widgets-bundles": "Наборы виджетов",
@ -803,6 +1353,20 @@ export default function addLocaleRussian(locales) {
"widget-type-file": "Файл типа виджета", "widget-type-file": "Файл типа виджета",
"invalid-widget-type-file-error": "Не удалось импортировать виджет: неизвестная схема данных типа виджета." "invalid-widget-type-file-error": "Не удалось импортировать виджет: неизвестная схема данных типа виджета."
}, },
"icon": { // TODO
"icon": "Icon",
"select-icon": "Select icon",
"material-icons": "Material icons",
"show-all": "Show all icons"
},
"custom": { // TODO
"widget-action": {
"action-cell-button": "Action cell button",
"row-click": "On row click",
"marker-click": "On marker click",
"tooltip-tag-action": "Tooltip tag action"
}
},
"language": { "language": {
"language": "Язык", "language": "Язык",
"en_US": "Английский", "en_US": "Английский",
@ -813,5 +1377,5 @@ export default function addLocaleRussian(locales) {
} }
}; };
angular.extend(locales, {'ru_RU': ru_RU}); angular.extend(locales, { 'ru_RU': ru_RU });
} }

38
ui/src/app/locale/locale.constant.js

@ -512,7 +512,7 @@ export default angular.module('thingsboard.locale', [])
"configuration-error": "Configuration error", "configuration-error": "Configuration error",
"alias-resolution-error-title": "Dashboard aliases configuration error", "alias-resolution-error-title": "Dashboard aliases configuration error",
"invalid-aliases-config": "Unable to find any devices matching to some of the aliases filter.<br/>" + "invalid-aliases-config": "Unable to find any devices matching to some of the aliases filter.<br/>" +
"Please contact your administrator in order to resolve this issue.", "Please contact your administrator in order to resolve this issue.",
"select-devices": "Select devices", "select-devices": "Select devices",
"assignedToCustomer": "Assigned to customer", "assignedToCustomer": "Assigned to customer",
"assignedToCustomers": "Assigned to customers", "assignedToCustomers": "Assigned to customers",
@ -897,13 +897,13 @@ export default angular.module('thingsboard.locale', [])
"opc-identity": "Identity", "opc-identity": "Identity",
"opc-keystore": "Keystore", "opc-keystore": "Keystore",
"opc-type": "Type", "opc-type": "Type",
"opc-keystore-type":"Type", "opc-keystore-type": "Type",
"opc-keystore-location":"Location *", "opc-keystore-location": "Location *",
"opc-keystore-password":"Password", "opc-keystore-password": "Password",
"opc-keystore-alias":"Alias", "opc-keystore-alias": "Alias",
"opc-keystore-key-password":"Key password", "opc-keystore-key-password": "Key password",
"opc-device-node-pattern":"Device node pattern", "opc-device-node-pattern": "Device node pattern",
"opc-device-name-pattern":"Device name pattern", "opc-device-name-pattern": "Device name pattern",
"modbus-server": "Servers/slaves", "modbus-server": "Servers/slaves",
"modbus-add-server": "Add server/slave", "modbus-add-server": "Add server/slave",
"modbus-add-server-prompt": "Please add server/slave", "modbus-add-server-prompt": "Please add server/slave",
@ -918,7 +918,7 @@ export default angular.module('thingsboard.locale', [])
"modbus-stopbits-range": "Stop bits should be in a range from 1 to 2.", "modbus-stopbits-range": "Stop bits should be in a range from 1 to 2.",
"modbus-unit-id": "Unit ID", "modbus-unit-id": "Unit ID",
"modbus-unit-id-range": "Unit ID should be in a range from 1 to 247.", "modbus-unit-id-range": "Unit ID should be in a range from 1 to 247.",
"modbus-device-name":"Device name", "modbus-device-name": "Device name",
"modbus-poll-period": "Poll period (ms)", "modbus-poll-period": "Poll period (ms)",
"modbus-attributes-poll-period": "Attributes poll period (ms)", "modbus-attributes-poll-period": "Attributes poll period (ms)",
"modbus-timeseries-poll-period": "Timeseries poll period (ms)", "modbus-timeseries-poll-period": "Timeseries poll period (ms)",
@ -941,8 +941,8 @@ export default angular.module('thingsboard.locale', [])
"not-available": "Not available" "not-available": "Not available"
}, },
"export-extensions-configuration":"Export extensions configuration", "export-extensions-configuration": "Export extensions configuration",
"import-extensions-configuration":"Import extensions configuration", "import-extensions-configuration": "Import extensions configuration",
"import-extensions": "Import extensions", "import-extensions": "Import extensions",
"import-extension": "Import extension", "import-extension": "Import extension",
"export-extension": "Export extension", "export-extension": "Export extension",
@ -1445,14 +1445,6 @@ export default angular.module('thingsboard.locale', [])
"material-icons": "Material icons", "material-icons": "Material icons",
"show-all": "Show all icons" "show-all": "Show all icons"
}, },
"language": {
"language": "Language",
"en_US": "English",
"ko_KR": "Korean",
"zh_CN": "Chinese",
"ru_RU": "Russian",
"es_ES": "Spanish"
},
"custom": { "custom": {
"widget-action": { "widget-action": {
"action-cell-button": "Action cell button", "action-cell-button": "Action cell button",
@ -1460,6 +1452,14 @@ export default angular.module('thingsboard.locale', [])
"marker-click": "On marker click", "marker-click": "On marker click",
"tooltip-tag-action": "Tooltip tag action" "tooltip-tag-action": "Tooltip tag action"
} }
},
"language": {
"language": "Language",
"en_US": "English",
"ko_KR": "Korean",
"zh_CN": "Chinese",
"ru_RU": "Russian",
"es_ES": "Spanish"
} }
} }
} }

Loading…
Cancel
Save