Browse Source

Merged with master

pull/804/head
Igor Kulikov 8 years ago
parent
commit
7d179c6af8
  1. 1
      application/src/main/resources/thingsboard.yml
  2. 8
      dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java
  3. 72
      dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java
  4. 93
      docker/k8s/redis.yaml
  5. 12
      docker/k8s/tb.yaml
  6. 8
      transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java
  7. 5
      transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java
  8. 2
      ui/src/app/app.js
  9. 23
      ui/src/app/common/types.constant.js
  10. 2
      ui/src/app/extension/extension-dialog.controller.js
  11. 1
      ui/src/app/extension/extension-dialog.tpl.html
  12. 140
      ui/src/app/extension/extensions-forms/extension-form-modbus.directive.js
  13. 774
      ui/src/app/extension/extensions-forms/extension-form-modbus.tpl.html
  14. 2
      ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html
  15. 3
      ui/src/app/extension/index.js
  16. 30
      ui/src/app/locale/locale.constant.js
  17. 4
      ui/src/app/widget/lib/flot-widget.js

1
application/src/main/resources/thingsboard.yml

@ -87,6 +87,7 @@ mqtt:
leak_detector_level: "${NETTY_LEASK_DETECTOR_LVL:DISABLED}" leak_detector_level: "${NETTY_LEASK_DETECTOR_LVL:DISABLED}"
boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}" boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}"
worker_group_thread_count: "${NETTY_WORKER_GROUP_THREADS:12}" worker_group_thread_count: "${NETTY_WORKER_GROUP_THREADS:12}"
max_payload_size: "${NETTY_MAX_PAYLOAD_SIZE:65536}"
# MQTT SSL configuration # MQTT SSL configuration
ssl: ssl:
# Enable/disable SSL support # Enable/disable SSL support

8
dao/src/main/java/org/thingsboard/server/dao/cassandra/AbstractCassandraCluster.java

@ -60,6 +60,10 @@ public abstract class AbstractCassandraCluster {
private long initTimeout; private long initTimeout;
@Value("${cassandra.init_retry_interval_ms}") @Value("${cassandra.init_retry_interval_ms}")
private long initRetryInterval; private long initRetryInterval;
@Value("${cassandra.max_requests_per_connection_local:128}")
private int max_requests_local;
@Value("${cassandra.max_requests_per_connection_remote:128}")
private int max_requests_remote;
@Autowired @Autowired
private CassandraSocketOptions socketOpts; private CassandraSocketOptions socketOpts;
@ -90,8 +94,8 @@ public abstract class AbstractCassandraCluster {
.withClusterName(clusterName) .withClusterName(clusterName)
.withSocketOptions(socketOpts.getOpts()) .withSocketOptions(socketOpts.getOpts())
.withPoolingOptions(new PoolingOptions() .withPoolingOptions(new PoolingOptions()
.setMaxRequestsPerConnection(HostDistance.LOCAL, 32768) .setMaxRequestsPerConnection(HostDistance.LOCAL, max_requests_local)
.setMaxRequestsPerConnection(HostDistance.REMOTE, 32768)); .setMaxRequestsPerConnection(HostDistance.REMOTE, max_requests_remote));
this.clusterBuilder.withQueryOptions(queryOpts.getOpts()); this.clusterBuilder.withQueryOptions(queryOpts.getOpts());
this.clusterBuilder.withCompression(StringUtils.isEmpty(compression) ? Compression.NONE : Compression.valueOf(compression.toUpperCase())); this.clusterBuilder.withCompression(StringUtils.isEmpty(compression) ? Compression.NONE : Compression.valueOf(compression.toUpperCase()));
if (ssl) { if (ssl) {

72
dao/src/main/java/org/thingsboard/server/dao/relation/BaseRelationService.java

@ -170,12 +170,12 @@ public class BaseRelationService implements RelationService {
Cache cache = cacheManager.getCache(RELATIONS_CACHE); Cache cache = cacheManager.getCache(RELATIONS_CACHE);
log.trace("Executing deleteEntityRelations [{}]", entity); log.trace("Executing deleteEntityRelations [{}]", entity);
validate(entity); validate(entity);
List<ListenableFuture<List<EntityRelation>>> inboundRelationsListTo = new ArrayList<>(); List<ListenableFuture<List<EntityRelation>>> inboundRelationsList = new ArrayList<>();
for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) { for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) {
inboundRelationsListTo.add(relationDao.findAllByTo(entity, typeGroup)); inboundRelationsList.add(relationDao.findAllByTo(entity, typeGroup));
} }
ListenableFuture<List<List<EntityRelation>>> inboundRelationsTo = Futures.allAsList(inboundRelationsListTo); ListenableFuture<List<List<EntityRelation>>> inboundRelations = Futures.allAsList(inboundRelationsList);
ListenableFuture<List<Boolean>> inboundDeletions = Futures.transform(inboundRelationsTo, (List<List<EntityRelation>> relations) -> ListenableFuture<List<Boolean>> inboundDeletions = Futures.transform(inboundRelations, (List<List<EntityRelation>> relations) ->
getBooleans(relations, cache, true)); getBooleans(relations, cache, true));
ListenableFuture<Boolean> inboundFuture = Futures.transform(inboundDeletions, getListToBooleanFunction()); ListenableFuture<Boolean> inboundFuture = Futures.transform(inboundDeletions, getListToBooleanFunction());
@ -186,12 +186,12 @@ public class BaseRelationService implements RelationService {
log.error("Error deleting entity inbound relations", e); log.error("Error deleting entity inbound relations", e);
} }
List<ListenableFuture<List<EntityRelation>>> inboundRelationsListFrom = new ArrayList<>(); List<ListenableFuture<List<EntityRelation>>> outboundRelationsList = new ArrayList<>();
for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) { for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) {
inboundRelationsListFrom.add(relationDao.findAllByFrom(entity, typeGroup)); outboundRelationsList.add(relationDao.findAllByFrom(entity, typeGroup));
} }
ListenableFuture<List<List<EntityRelation>>> inboundRelationsFrom = Futures.allAsList(inboundRelationsListFrom); ListenableFuture<List<List<EntityRelation>>> outboundRelations = Futures.allAsList(outboundRelationsList);
Futures.transform(inboundRelationsFrom, (Function<List<List<EntityRelation>>, List<Boolean>>) relations -> Futures.transform(outboundRelations, (Function<List<List<EntityRelation>>, List<Boolean>>) relations ->
getBooleans(relations, cache, false)); getBooleans(relations, cache, false));
boolean outboundDeleteResult = relationDao.deleteOutboundRelations(entity); boolean outboundDeleteResult = relationDao.deleteOutboundRelations(entity);
@ -201,9 +201,7 @@ public class BaseRelationService implements RelationService {
private List<Boolean> getBooleans(List<List<EntityRelation>> relations, Cache cache, boolean isRemove) { private List<Boolean> getBooleans(List<List<EntityRelation>> relations, Cache cache, boolean isRemove) {
List<Boolean> results = new ArrayList<>(); List<Boolean> results = new ArrayList<>();
for (List<EntityRelation> relationList : relations) { for (List<EntityRelation> relationList : relations) {
relationList.stream().forEach(relation -> { relationList.stream().forEach(relation -> checkFromDeleteSync(cache, results, relation, isRemove));
checkFromDeleteSync(cache, results, relation, isRemove);
});
} }
return results; return results;
} }
@ -211,10 +209,8 @@ public class BaseRelationService implements RelationService {
private void checkFromDeleteSync(Cache cache, List<Boolean> results, EntityRelation relation, boolean isRemove) { private void checkFromDeleteSync(Cache cache, List<Boolean> results, EntityRelation relation, boolean isRemove) {
if (isRemove) { if (isRemove) {
results.add(relationDao.deleteRelation(relation)); results.add(relationDao.deleteRelation(relation));
cacheEviction(relation, false, cache);
} else {
cacheEviction(relation, true, cache);
} }
cacheEviction(relation, cache);
} }
@Override @Override
@ -222,12 +218,12 @@ public class BaseRelationService implements RelationService {
Cache cache = cacheManager.getCache(RELATIONS_CACHE); Cache cache = cacheManager.getCache(RELATIONS_CACHE);
log.trace("Executing deleteEntityRelationsAsync [{}]", entity); log.trace("Executing deleteEntityRelationsAsync [{}]", entity);
validate(entity); validate(entity);
List<ListenableFuture<List<EntityRelation>>> inboundRelationsListTo = new ArrayList<>(); List<ListenableFuture<List<EntityRelation>>> inboundRelationsList = new ArrayList<>();
for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) { for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) {
inboundRelationsListTo.add(relationDao.findAllByTo(entity, typeGroup)); inboundRelationsList.add(relationDao.findAllByTo(entity, typeGroup));
} }
ListenableFuture<List<List<EntityRelation>>> inboundRelationsTo = Futures.allAsList(inboundRelationsListTo); ListenableFuture<List<List<EntityRelation>>> inboundRelations = Futures.allAsList(inboundRelationsList);
ListenableFuture<List<Boolean>> inboundDeletions = Futures.transform(inboundRelationsTo, ListenableFuture<List<Boolean>> inboundDeletions = Futures.transform(inboundRelations,
(AsyncFunction<List<List<EntityRelation>>, List<Boolean>>) relations -> { (AsyncFunction<List<List<EntityRelation>>, List<Boolean>>) relations -> {
List<ListenableFuture<Boolean>> results = getListenableFutures(relations, cache, true); List<ListenableFuture<Boolean>> results = getListenableFutures(relations, cache, true);
return Futures.allAsList(results); return Futures.allAsList(results);
@ -235,12 +231,12 @@ public class BaseRelationService implements RelationService {
ListenableFuture<Boolean> inboundFuture = Futures.transform(inboundDeletions, getListToBooleanFunction()); ListenableFuture<Boolean> inboundFuture = Futures.transform(inboundDeletions, getListToBooleanFunction());
List<ListenableFuture<List<EntityRelation>>> inboundRelationsListFrom = new ArrayList<>(); List<ListenableFuture<List<EntityRelation>>> outboundRelationsList = new ArrayList<>();
for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) { for (RelationTypeGroup typeGroup : RelationTypeGroup.values()) {
inboundRelationsListFrom.add(relationDao.findAllByTo(entity, typeGroup)); outboundRelationsList.add(relationDao.findAllByFrom(entity, typeGroup));
} }
ListenableFuture<List<List<EntityRelation>>> inboundRelationsFrom = Futures.allAsList(inboundRelationsListFrom); ListenableFuture<List<List<EntityRelation>>> outboundRelations = Futures.allAsList(outboundRelationsList);
Futures.transform(inboundRelationsFrom, (AsyncFunction<List<List<EntityRelation>>, List<Boolean>>) relations -> { Futures.transform(outboundRelations, (AsyncFunction<List<List<EntityRelation>>, List<Boolean>>) relations -> {
List<ListenableFuture<Boolean>> results = getListenableFutures(relations, cache, false); List<ListenableFuture<Boolean>> results = getListenableFutures(relations, cache, false);
return Futures.allAsList(results); return Futures.allAsList(results);
}); });
@ -252,9 +248,7 @@ public class BaseRelationService implements RelationService {
private List<ListenableFuture<Boolean>> getListenableFutures(List<List<EntityRelation>> relations, Cache cache, boolean isRemove) { private List<ListenableFuture<Boolean>> getListenableFutures(List<List<EntityRelation>> relations, Cache cache, boolean isRemove) {
List<ListenableFuture<Boolean>> results = new ArrayList<>(); List<ListenableFuture<Boolean>> results = new ArrayList<>();
for (List<EntityRelation> relationList : relations) { for (List<EntityRelation> relationList : relations) {
relationList.stream().forEach(relation -> { relationList.stream().forEach(relation -> checkFromDeleteAsync(cache, results, relation, isRemove));
checkFromDeleteAsync(cache, results, relation, isRemove);
});
} }
return results; return results;
} }
@ -262,13 +256,11 @@ public class BaseRelationService implements RelationService {
private void checkFromDeleteAsync(Cache cache, List<ListenableFuture<Boolean>> results, EntityRelation relation, boolean isRemove) { private void checkFromDeleteAsync(Cache cache, List<ListenableFuture<Boolean>> results, EntityRelation relation, boolean isRemove) {
if (isRemove) { if (isRemove) {
results.add(relationDao.deleteRelationAsync(relation)); results.add(relationDao.deleteRelationAsync(relation));
cacheEviction(relation, false, cache);
} else {
cacheEviction(relation, true, cache);
} }
cacheEviction(relation, cache);
} }
private void cacheEviction(EntityRelation relation, boolean outboundOnly, Cache cache) { private void cacheEviction(EntityRelation relation, Cache cache) {
List<Object> fromToTypeAndTypeGroup = new ArrayList<>(); List<Object> fromToTypeAndTypeGroup = new ArrayList<>();
fromToTypeAndTypeGroup.add(relation.getFrom()); fromToTypeAndTypeGroup.add(relation.getFrom());
fromToTypeAndTypeGroup.add(relation.getTo()); fromToTypeAndTypeGroup.add(relation.getTo());
@ -287,18 +279,16 @@ public class BaseRelationService implements RelationService {
fromAndTypeGroup.add(relation.getTypeGroup()); fromAndTypeGroup.add(relation.getTypeGroup());
cache.evict(fromAndTypeGroup); cache.evict(fromAndTypeGroup);
if (!outboundOnly) { List<Object> toAndTypeGroup = new ArrayList<>();
List<Object> toAndTypeGroup = new ArrayList<>(); toAndTypeGroup.add(relation.getTo());
toAndTypeGroup.add(relation.getTo()); toAndTypeGroup.add(relation.getTypeGroup());
toAndTypeGroup.add(relation.getTypeGroup()); cache.evict(toAndTypeGroup);
cache.evict(toAndTypeGroup);
List<Object> toTypeAndTypeGroup = new ArrayList<>();
List<Object> toTypeAndTypeGroup = new ArrayList<>(); fromTypeAndTypeGroup.add(relation.getTo());
fromTypeAndTypeGroup.add(relation.getTo()); fromTypeAndTypeGroup.add(relation.getType());
fromTypeAndTypeGroup.add(relation.getType()); fromTypeAndTypeGroup.add(relation.getTypeGroup());
fromTypeAndTypeGroup.add(relation.getTypeGroup()); cache.evict(toTypeAndTypeGroup);
cache.evict(toTypeAndTypeGroup);
}
} }
@Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup}") @Cacheable(cacheNames = RELATIONS_CACHE, key = "{#from, #typeGroup}")

93
docker/k8s/redis.yaml

@ -0,0 +1,93 @@
#
# Copyright © 2016-2018 The Thingsboard Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
---
apiVersion: v1
kind: Service
metadata:
labels:
name: redis-service
name: redis-service
spec:
ports:
- name: redis-service
protocol: TCP
port: 6379
targetPort: 6379
selector:
app: redis
---
apiVersion: v1
kind: ConfigMap
metadata:
name: redis-conf
data:
redis.conf: |
appendonly yes
protected-mode no
bind 0.0.0.0
port 6379
dir /var/lib/redis
---
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
name: redis
spec:
serviceName: redis-service
replicas: 1
template:
metadata:
labels:
app: redis
spec:
terminationGracePeriodSeconds: 10
containers:
- name: redis
image: redis:4.0.9
command:
- redis-server
args:
- /etc/redis/redis.conf
resources:
requests:
cpu: 100m
memory: 100Mi
ports:
- containerPort: 6379
name: redis
volumeMounts:
- name: redis-data
mountPath: /var/lib/redis
- name: redis-conf
mountPath: /etc/redis
volumes:
- name: redis-conf
configMap:
name: redis-conf
items:
- key: redis.conf
path: redis.conf
volumeClaimTemplates:
- metadata:
name: redis-data
annotations:
volume.beta.kubernetes.io/storage-class: fast
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 1Gi

12
docker/k8s/tb.yaml

@ -54,6 +54,8 @@ data:
cassandra.host: "cassandra-headless" cassandra.host: "cassandra-headless"
cassandra.port: "9042" cassandra.port: "9042"
database.type: "cassandra" database.type: "cassandra"
cache.type: "redis"
redis.host: "redis-service"
--- ---
apiVersion: apps/v1beta1 apiVersion: apps/v1beta1
kind: StatefulSet kind: StatefulSet
@ -127,6 +129,16 @@ spec:
valueFrom: valueFrom:
fieldRef: fieldRef:
fieldPath: status.podIP fieldPath: status.podIP
- name: CACHE_TYPE
valueFrom:
configMapKeyRef:
name: tb-config
key: cache.type
- name: REDIS_HOST
valueFrom:
configMapKeyRef:
name: tb-config
key: redis.host
command: command:
- sh - sh
- -c - -c

8
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java

@ -33,8 +33,6 @@ import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
*/ */
public class MqttTransportServerInitializer extends ChannelInitializer<SocketChannel> { public class MqttTransportServerInitializer extends ChannelInitializer<SocketChannel> {
private static final int MAX_PAYLOAD_SIZE = 64 * 1024 * 1024;
private final SessionMsgProcessor processor; private final SessionMsgProcessor processor;
private final DeviceService deviceService; private final DeviceService deviceService;
private final DeviceAuthService authService; private final DeviceAuthService authService;
@ -42,10 +40,11 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
private final MqttTransportAdaptor adaptor; private final MqttTransportAdaptor adaptor;
private final MqttSslHandlerProvider sslHandlerProvider; private final MqttSslHandlerProvider sslHandlerProvider;
private final QuotaService quotaService; private final QuotaService quotaService;
private final int maxPayloadSize;
public MqttTransportServerInitializer(SessionMsgProcessor processor, DeviceService deviceService, DeviceAuthService authService, RelationService relationService, public MqttTransportServerInitializer(SessionMsgProcessor processor, DeviceService deviceService, DeviceAuthService authService, RelationService relationService,
MqttTransportAdaptor adaptor, MqttSslHandlerProvider sslHandlerProvider, MqttTransportAdaptor adaptor, MqttSslHandlerProvider sslHandlerProvider,
QuotaService quotaService) { QuotaService quotaService, int maxPayloadSize) {
this.processor = processor; this.processor = processor;
this.deviceService = deviceService; this.deviceService = deviceService;
this.authService = authService; this.authService = authService;
@ -53,6 +52,7 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
this.adaptor = adaptor; this.adaptor = adaptor;
this.sslHandlerProvider = sslHandlerProvider; this.sslHandlerProvider = sslHandlerProvider;
this.quotaService = quotaService; this.quotaService = quotaService;
this.maxPayloadSize = maxPayloadSize;
} }
@Override @Override
@ -63,7 +63,7 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
sslHandler = sslHandlerProvider.getSslHandler(); sslHandler = sslHandlerProvider.getSslHandler();
pipeline.addLast(sslHandler); pipeline.addLast(sslHandler);
} }
pipeline.addLast("decoder", new MqttDecoder(MAX_PAYLOAD_SIZE)); pipeline.addLast("decoder", new MqttDecoder(maxPayloadSize));
pipeline.addLast("encoder", MqttEncoder.INSTANCE); pipeline.addLast("encoder", MqttEncoder.INSTANCE);
MqttTransportHandler handler = new MqttTransportHandler(processor, deviceService, authService, relationService, MqttTransportHandler handler = new MqttTransportHandler(processor, deviceService, authService, relationService,

5
transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java

@ -82,7 +82,8 @@ public class MqttTransportService {
private Integer bossGroupThreadCount; private Integer bossGroupThreadCount;
@Value("${mqtt.netty.worker_group_thread_count}") @Value("${mqtt.netty.worker_group_thread_count}")
private Integer workerGroupThreadCount; private Integer workerGroupThreadCount;
@Value("${mqtt.netty.max_payload_size}")
private Integer maxPayloadSize;
private MqttTransportAdaptor adaptor; private MqttTransportAdaptor adaptor;
@ -106,7 +107,7 @@ public class MqttTransportService {
b.group(bossGroup, workerGroup) b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class) .channel(NioServerSocketChannel.class)
.childHandler(new MqttTransportServerInitializer(processor, deviceService, authService, relationService, .childHandler(new MqttTransportServerInitializer(processor, deviceService, authService, relationService,
adaptor, sslHandlerProvider, quotaService)); adaptor, sslHandlerProvider, quotaService, maxPayloadSize));
serverChannel = b.bind(host, port).sync().channel(); serverChannel = b.bind(host, port).sync().channel();
log.info("Mqtt transport started!"); log.info("Mqtt transport started!");

2
ui/src/app/app.js

@ -82,7 +82,7 @@ import 'font-awesome/css/font-awesome.min.css';
import 'angular-material/angular-material.min.css'; import 'angular-material/angular-material.min.css';
import 'angular-material-icons/angular-material-icons.css'; import 'angular-material-icons/angular-material-icons.css';
import 'angular-gridster/dist/angular-gridster.min.css'; import 'angular-gridster/dist/angular-gridster.min.css';
import 'v-accordion/dist/v-accordion.min.css' import 'v-accordion/dist/v-accordion.min.css';
import 'md-color-picker/dist/mdColorPicker.min.css'; import 'md-color-picker/dist/mdColorPicker.min.css';
import 'mdPickers/dist/mdPickers.min.css'; import 'mdPickers/dist/mdPickers.min.css';
import 'angular-hotkeys/build/hotkeys.min.css'; import 'angular-hotkeys/build/hotkeys.min.css';

23
ui/src/app/common/types.constant.js

@ -406,7 +406,8 @@ export default angular.module('thingsboard.types', [])
extensionType: { extensionType: {
http: "HTTP", http: "HTTP",
mqtt: "MQTT", mqtt: "MQTT",
opc: "OPC UA" opc: "OPC UA",
modbus: "MODBUS"
}, },
extensionValueType: { extensionValueType: {
string: 'value.string', string: 'value.string',
@ -450,6 +451,26 @@ export default angular.module('thingsboard.types', [])
PKCS12: "PKCS12", PKCS12: "PKCS12",
JKS: "JKS" JKS: "JKS"
}, },
extensionModbusFunctionCodes: {
1: "Read Coils (1)",
2: "Read Discrete Inputs (2)",
3: "Read Multiple Holding Registers (3)",
4: "Read Input Registers (4)"
},
extensionModbusTransports: {
tcp: "TCP",
udp: "UDP",
rtu: "RTU"
},
extensionModbusRtuParities: {
none: "none",
even: "even",
odd: "odd"
},
extensionModbusRtuEncodings: {
ascii: "ascii",
rtu: "rtu"
},
latestTelemetry: { latestTelemetry: {
value: "LATEST_TELEMETRY", value: "LATEST_TELEMETRY",
name: "attribute.scope-latest-telemetry", name: "attribute.scope-latest-telemetry",

2
ui/src/app/extension/extension-dialog.controller.js

@ -49,7 +49,7 @@ export default function ExtensionDialogController($scope, $mdDialog, $translate,
"brokers": [] "brokers": []
}; };
} }
if (vm.extension.type === "OPC UA") { if (vm.extension.type === "OPC UA" || vm.extension.type === "MODBUS") {
vm.extension.configuration = { vm.extension.configuration = {
"servers": [] "servers": []
}; };

1
ui/src/app/extension/extension-dialog.tpl.html

@ -62,6 +62,7 @@
<div tb-extension-form-http config="vm.extension.configuration" is-add="vm.isAdd" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.http"></div> <div tb-extension-form-http config="vm.extension.configuration" is-add="vm.isAdd" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.http"></div>
<div tb-extension-form-mqtt config="vm.extension.configuration" is-add="vm.isAdd" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.mqtt"></div> <div tb-extension-form-mqtt config="vm.extension.configuration" is-add="vm.isAdd" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.mqtt"></div>
<div tb-extension-form-opc configuration="vm.extension.configuration" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.opc"></div> <div tb-extension-form-opc configuration="vm.extension.configuration" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.opc"></div>
<div tb-extension-form-modbus configuration="vm.extension.configuration" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.modbus"></div>
</fieldset> </fieldset>
</md-content> </md-content>
</div> </div>

140
ui/src/app/extension/extensions-forms/extension-form-modbus.directive.js

@ -0,0 +1,140 @@
/*
* Copyright © 2016-2018 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import 'brace/ext/language_tools';
import 'brace/mode/json';
import 'brace/theme/github';
import './extension-form.scss';
/* eslint-disable angular/log */
import extensionFormModbusTemplate from './extension-form-modbus.tpl.html';
/* eslint-enable import/no-unresolved, import/default */
/*@ngInject*/
export default function ExtensionFormModbusDirective($compile, $templateCache, $translate, types) {
var linker = function(scope, element) {
function Server() {
this.transport = {
"type": "tcp",
"host": "localhost",
"port": 502,
"timeout": 3000
};
this.devices = []
}
function Device() {
this.unitId = 1;
this.deviceName = "";
this.attributesPollPeriod = 1000;
this.timeseriesPollPeriod = 1000;
this.attributes = [];
this.timeseries = [];
}
function Tag(globalPollPeriod) {
this.tag = "";
this.type = "long";
this.pollPeriod = globalPollPeriod;
this.functionCode = 3;
this.address = 0;
this.registerCount = 1;
this.bit = 0;
this.byteOrder = "BIG";
}
var template = $templateCache.get(extensionFormModbusTemplate);
element.html(template);
scope.types = types;
scope.theForm = scope.$parent.theForm;
if (!scope.configuration.servers.length) {
scope.configuration.servers.push(new Server());
}
scope.addServer = function(serversList) {
serversList.push(new Server());
scope.theForm.$setDirty();
};
scope.addDevice = function(deviceList) {
deviceList.push(new Device());
scope.theForm.$setDirty();
};
scope.addNewAttribute = function(device) {
device.attributes.push(new Tag(device.attributesPollPeriod));
scope.theForm.$setDirty();
};
scope.addNewTimeseries = function(device) {
device.timeseries.push(new Tag(device.timeseriesPollPeriod));
scope.theForm.$setDirty();
};
scope.removeItem = (item, itemList) => {
var index = itemList.indexOf(item);
if (index > -1) {
itemList.splice(index, 1);
}
scope.theForm.$setDirty();
};
scope.onTransportChanged = function(server) {
var type = server.transport.type;
server.transport = {};
server.transport.type = type;
server.transport.timeout = 3000;
scope.theForm.$setDirty();
};
$compile(element.contents())(scope);
scope.collapseValidation = function(index, id) {
var invalidState = angular.element('#'+id+':has(.ng-invalid)');
if(invalidState.length) {
invalidState.addClass('inner-invalid');
}
};
scope.expandValidation = function (index, id) {
var invalidState = angular.element('#'+id);
invalidState.removeClass('inner-invalid');
};
};
return {
restrict: "A",
link: linker,
scope: {
configuration: "=",
isAdd: "="
}
}
}

774
ui/src/app/extension/extensions-forms/extension-form-modbus.tpl.html

@ -0,0 +1,774 @@
<!--
Copyright © 2016-2018 The Thingsboard Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<md-card class="extension-form extension-modbus">
<md-card-title>
<md-card-title-text>
<span translate class="md-headline">extension.configuration</span>
</md-card-title-text>
</md-card-title>
<md-card-content>
<v-accordion id="modbus-server-configs-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="modbus-servers-pane" expanded="true">
<v-pane-header>
{{ 'extension.modbus-server' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="configuration.servers.length === 0">
<span translate layout-align="center center" class="tb-prompt">extension.modbus-add-server-prompt</span>
</div>
<div ng-if="configuration.servers.length > 0">
<ol class="list-group">
<li class="list-group-item" ng-repeat="(serverIndex, server) in configuration.servers">
<md-button aria-label="{{ 'action.remove' | translate }}"
class="md-icon-button"
ng-click="removeItem(server, configuration.servers)"
ng-hide="configuration.servers.length < 2"
>
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<div layout="row">
<md-input-container flex="50" class="md-block tb-container-for-select">
<label translate>extension.modbus-transport</label>
<md-select required
name="transportType_{{serverIndex}}"
ng-model="server.transport.type"
ng-change="onTransportChanged(server)"
>
<md-option ng-value="transportType"
ng-repeat="(transportType, transportValue) in types.extensionModbusTransports"
><span ng-bind="transportValue"></span></md-option>
</md-select>
<div ng-messages="theForm['transportType_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</div>
<div layout="row" ng-if="server.transport.type == 'tcp' || server.transport.type == 'udp'">
<md-input-container flex="33" class="md-block">
<label translate>extension.host</label>
<input required name="transportHost_{{serverIndex}}" ng-model="server.transport.host">
<div ng-messages="theForm['transportHost_' + serverIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="33" class="md-block">
<label translate>extension.port</label>
<input type="number"
required
name="transportPort_{{serverIndex}}"
ng-model="server.transport.port"
min="1"
max="65535"
>
<div ng-messages="theForm['transportPort_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
<div translate
ng-message="min"
>extension.port-range</div>
<div translate
ng-message="max"
>extension.port-range</div>
</div>
</md-input-container>
<md-input-container flex="33" class="md-block">
<label translate>extension.timeout</label>
<input type="number"
required name="transportTimeout_{{serverIndex}}"
ng-model="server.transport.timeout"
>
<div ng-messages="theForm['transportTimeout_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</div>
<div ng-if="server.transport.type == 'rtu'">
<div layout="row">
<md-input-container flex="70" class="md-block">
<label translate>extension.modbus-port-name</label>
<input required name="transportPortName_{{serverIndex}}" ng-model="server.transport.portName">
<div ng-messages="theForm['transportPortName_' + serverIndex].$error">
<div translate ng-message="required">extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="30" class="md-block">
<label translate>extension.timeout</label>
<input type="number"
required name="transportTimeout_{{serverIndex}}"
ng-model="server.transport.timeout"
>
<div ng-messages="theForm['transportTimeout_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</div>
<div layout="row">
<md-input-container flex="50" class="md-block tb-container-for-select">
<label translate>extension.modbus-encoding</label>
<md-select required
name="transportEncoding_{{serverIndex}}"
ng-model="server.transport.encoding"
>
<md-option ng-value="encodingType"
ng-repeat="(encodingType, encodingValue) in types.extensionModbusRtuEncodings"
><span ng-bind="encodingValue"></span></md-option>
</md-select>
<div ng-messages="theForm['transportEncoding_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block tb-container-for-select">
<label translate>extension.modbus-parity</label>
<md-select name="transportParity_{{serverIndex}}" ng-model="server.transport.parity">
<md-option ng-repeat="(parityKey, parityValue) in types.extensionModbusRtuParities"
ng-value="parityValue"
>
{{parityValue}}
</md-option>
</md-select>
<div ng-messages="theForm['transportParity_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</div>
<div layout="row">
<md-input-container flex="33" class="md-block">
<label translate>extension.modbus-baudrate</label>
<input type="number"
required name="transportBaudRate_{{serverIndex}}"
ng-model="server.transport.baudRate"
>
<div ng-messages="theForm['transportBaudRate_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="33" class="md-block">
<label translate>extension.modbus-databits</label>
<input type="number"
required
name="transportDataBits_{{serverIndex}}"
ng-model="server.transport.dataBits"
min="7"
max="8"
>
<div ng-messages="theForm['transportDataBits_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
<div translate
ng-message="min"
>extension.modbus-databits-range</div>
<div translate
ng-message="max"
>extension.modbus-databits-range</div>
</div>
</md-input-container>
<md-input-container flex="33" class="md-block">
<label translate>extension.modbus-stopbits</label>
<input type="number"
required
name="transportStopBits_{{serverIndex}}"
ng-model="server.transport.stopBits"
min="1"
max="2"
>
<div ng-messages="theForm['transportStopBits_' + serverIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
<div translate
ng-message="min"
>extension.modbus-stopbits-range</div>
<div translate
ng-message="max"
>extension.modbus-stopbits-range</div>
</div>
</md-input-container>
</div>
</div>
<v-accordion id="modbus-mapping-accordion"
class="vAccordion--default"
onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="modbus-mapping-pane_{{serverIndex}}">
<v-pane-header>
{{ 'extension.mapping' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-if="server.devices.length > 0">
<ol class="list-group">
<li class="list-group-item"
ng-repeat="(deviceIndex, device) in server.devices"
>
<md-button aria-label="{{ 'action.remove' | translate }}"
class="md-icon-button"
ng-click="removeItem(device, server.devices)"
>
<ng-md-icon icon="close" aria-label="{{ 'action.remove' | translate }}"></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<div flex layout="row">
<md-input-container flex="80" class="md-block">
<label translate>extension.modbus-device-name</label>
<input required
name="deviceName_{{serverIndex}}{{deviceIndex}}"
ng-model="device.deviceName"
>
<div ng-messages="theForm['deviceName_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="20" class="md-block">
<label translate>extension.modbus-unit-id</label>
<input type="number"
required
name="unitId_{{serverIndex}}{{deviceIndex}}"
ng-model="device.unitId"
min="1"
max="247"
>
<div ng-messages="theForm['unitId_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
<div translate
ng-message="min"
>extension.modbus-unit-id-range</div>
<div translate
ng-message="max"
>extension.modbus-unit-id-range</div>
</div>
</md-input-container>
</div>
<div flex layout="row">
<md-input-container flex="50" class="md-block">
<label translate>extension.modbus-attributes-poll-period</label>
<input type="number"
required
name="attributesPollPeriod_{{serverIndex}}{{deviceIndex}}"
ng-model="device.attributesPollPeriod"
min="1"
>
<div ng-messages="theForm['attributesPollPeriod_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
<div translate
ng-message="min"
>extension.modbus-poll-period-range</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.modbus-timeseries-poll-period</label>
<input type="number"
required
name="timeseriesPollPeriod_{{serverIndex}}{{deviceIndex}}"
ng-model="device.timeseriesPollPeriod"
min="1"
>
<div ng-messages="theForm['timeseriesPollPeriod_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
<div translate
ng-message="min"
>extension.modbus-poll-period-range</div>
</div>
</md-input-container>
</div>
<v-accordion id="modbus-attributes-accordion"
class="vAccordion--default"
onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="modbus-attributes-pane_{{serverIndex}}{{deviceIndex}}">
<v-pane-header>
{{ 'extension.attributes' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-show="device.attributes.length > 0">
<ol class="list-group">
<li class="list-group-item"
ng-repeat="(attributeIndex, attribute) in device.attributes"
>
<md-button aria-label="{{ 'action.remove' | translate }}"
class="md-icon-button"
ng-click="removeItem(attribute, device.attributes)">
<ng-md-icon icon="close"
aria-label="{{ 'action.remove' | translate }}"
></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="50" class="md-block">
<label translate>extension.modbus-tag</label>
<input required
name="modbusAttributeTag_{{serverIndex}}{{deviceIndex}}{{attributeIndex}}"
ng-model="attribute.tag"
>
<div ng-messages="theForm['modbusAttributeTag_' + serverIndex + deviceIndex + attributeIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="30" class="md-block tb-container-for-select">
<label translate>extension.type</label>
<md-select required name="modbusAttributeType_{{serverIndex}}{{deviceIndex}}{{attributeIndex}}"
ng-model="attribute.type"
>
<md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType"
ng-value="attrType"
>
{{attrTypeValue | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['modbusAttributeType_' + serverIndex + deviceIndex + attributeIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="20" class="md-block">
<label translate>extension.modbus-poll-period</label>
<input type="number"
name="pollPeriod_{{serverIndex}}{{deviceIndex}}"
ng-model="attribute.pollPeriod"
min="1"
>
<div ng-messages="theForm['pollPeriod_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="min"
>extension.modbus-poll-period-range</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="50" class="md-block tb-container-for-select">
<label translate>extension.type</label>
<md-select required name="modbusAttributeFunctionCode_{{serverIndex}}{{deviceIndex}}{{attributeIndex}}"
ng-model="attribute.functionCode"
>
<md-option ng-repeat="(functionCode, functionName) in types.extensionModbusFunctionCodes"
ng-value="functionCode"
>
{{functionName}}
</md-option>
</md-select>
<div ng-messages="theForm['modbusAttributeFunctionCode_' + serverIndex + deviceIndex + attributeIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.modbus-register-address</label>
<input type="number"
required
name="address_{{serverIndex}}{{deviceIndex}}"
ng-model="attribute.address"
min="0"
max="65535"
>
<div ng-messages="theForm['address_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
<div translate
ng-message="min"
>extension.modbus-register-address-range</div>
<div translate
ng-message="max"
>extension.modbus-register-address-range</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="50" class="md-block" ng-if="attribute.type != 'boolean'">
<label translate>extension.modbus-register-count</label>
<input type="number"
name="registerCount_{{serverIndex}}{{deviceIndex}}"
ng-model="attribute.registerCount"
min="1"
>
<div ng-messages="theForm['registerCount_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="min"
>extension.modbus-register-count-range</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block" ng-if="attribute.type == 'boolean' && attribute.functionCode >= 3">
<label translate>extension.modbus-register-bit-index</label>
<input type="number"
required
name="bit_{{serverIndex}}{{deviceIndex}}"
ng-model="attribute.bit"
min="0"
max="15"
>
<div ng-messages="theForm['bit_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
<div translate
ng-message="min"
>extension.modbus-register-bit-index-range</div>
<div translate
ng-message="max"
>extension.modbus-register-bit-index-range</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="100" class="md-block" ng-if="attribute.functionCode >= 3">
<label translate>extension.modbus-byte-order</label>
<input required
name="modbusByteOrder_{{serverIndex}}{{deviceIndex}}{{attributeIndex}}"
ng-model="attribute.byteOrder"
>
<div ng-messages="theForm['modbusByteOrder_' + serverIndex + deviceIndex + attributeIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</section>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addNewAttribute(device)"
aria-label="{{ 'action.add' | translate }}"
>
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-attribute</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<v-accordion id="modbus-timeseries-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="modbus-timeseries-pane_{{serverIndex}}{{deviceIndex}}">
<v-pane-header>
{{ 'extension.timeseries' | translate }}
</v-pane-header>
<v-pane-content>
<div ng-show="device.timeseries.length > 0">
<ol class="list-group">
<li class="list-group-item"
ng-repeat="(timeseriesIndex, timeserie) in device.timeseries"
>
<md-button aria-label="{{ 'action.remove' | translate }}"
class="md-icon-button"
ng-click="removeItem(timeserie, device.timeseries)">
<ng-md-icon icon="close"
aria-label="{{ 'action.remove' | translate }}"
></ng-md-icon>
<md-tooltip md-direction="top">
{{ 'action.remove' | translate }}
</md-tooltip>
</md-button>
<md-card>
<md-card-content>
<section flex layout="row">
<md-input-container flex="50" class="md-block">
<label translate>extension.modbus-tag</label>
<input required
name="modbusTimeserieTag_{{serverIndex}}{{deviceIndex}}{{timeseriesIndex}}"
ng-model="timeserie.tag"
>
<div ng-messages="theForm['modbusTimeserieTag_' + serverIndex + deviceIndex + timeseriesIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="30" class="md-block tb-container-for-select">
<label translate>extension.type</label>
<md-select required name="modbusTimeserieType_{{serverIndex}}{{deviceIndex}}{{timeseriesIndex}}"
ng-model="timeserie.type"
>
<md-option ng-repeat="(attrType, attrTypeValue) in types.extensionValueType"
ng-value="attrType"
>
{{attrTypeValue | translate}}
</md-option>
</md-select>
<div ng-messages="theForm['modbusTimeserieType_' + serverIndex + deviceIndex + timeseriesIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="20" class="md-block">
<label translate>extension.modbus-poll-period</label>
<input type="number"
name="pollPeriod_{{serverIndex}}{{deviceIndex}}"
ng-model="timeserie.pollPeriod"
min="1"
>
<div ng-messages="theForm['pollPeriod_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="min"
>extension.modbus-poll-period-range</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="50" class="md-block tb-container-for-select">
<label translate>extension.type</label>
<md-select required name="modbusTimeserieFunctionCode_{{serverIndex}}{{deviceIndex}}{{timeseriesIndex}}"
ng-model="timeserie.functionCode"
>
<md-option ng-repeat="(functionCode, functionName) in types.extensionModbusFunctionCodes"
ng-value="functionCode"
>
{{functionName}}
</md-option>
</md-select>
<div ng-messages="theForm['modbusTimeserieFunctionCode_' + serverIndex + deviceIndex + timeseriesIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block">
<label translate>extension.modbus-register-address</label>
<input type="number"
required
name="address_{{serverIndex}}{{deviceIndex}}"
ng-model="timeserie.address"
min="0"
max="65535"
>
<div ng-messages="theForm['address_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
<div translate
ng-message="min"
>extension.modbus-register-address-range</div>
<div translate
ng-message="max"
>extension.modbus-register-address-range</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="50" class="md-block" ng-if="timeserie.type != 'boolean'">
<label translate>extension.modbus-register-count</label>
<input type="number"
name="registerCount_{{serverIndex}}{{deviceIndex}}"
ng-model="timeserie.registerCount"
min="1"
>
<div ng-messages="theForm['registerCount_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="min"
>extension.modbus-register-count-range</div>
</div>
</md-input-container>
<md-input-container flex="50" class="md-block" ng-if="timeserie.type == 'boolean' && timeserie.functionCode >= 3">
<label translate>extension.modbus-register-bit-index</label>
<input type="number"
required
name="bit_{{serverIndex}}{{deviceIndex}}"
ng-model="timeserie.bit"
min="0"
max="15"
>
<div ng-messages="theForm['bit_' + serverIndex + deviceIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
<div translate
ng-message="min"
>extension.modbus-register-bit-index-range</div>
<div translate
ng-message="max"
>extension.modbus-register-bit-index-range</div>
</div>
</md-input-container>
</section>
<section flex layout="row">
<md-input-container flex="100" class="md-block" ng-if="timeserie.functionCode >= 3">
<label translate>extension.modbus-byte-order</label>
<input required
name="modbusByteOrder_{{serverIndex}}{{deviceIndex}}{{timeseriesIndex}}"
ng-model="timeserie.byteOrder"
>
<div ng-messages="theForm['modbusByteOrder_' + serverIndex + deviceIndex + timeseriesIndex].$error">
<div translate
ng-message="required"
>extension.field-required</div>
</div>
</md-input-container>
</section>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex layout="row" layout-align="start center">
<md-button class="md-primary md-raised"
ng-click="addNewTimeseries(device)"
aria-label="{{ 'action.add' | translate }}"
>
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-timeseries</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
</md-card-content>
</md-card>
</li>
</ol>
</div>
<div flex
layout="row"
layout-align="start center"
>
<md-button class="md-primary md-raised"
ng-click="addDevice(server.devices)"
aria-label="{{ 'action.add' | translate }}"
>
<md-icon class="material-icons">add</md-icon>
<span translate>extension.add-device</span>
</md-button>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
</md-card-content>
</md-card>
</li>
</ol>
<div flex
layout="row"
layout-align="start center"
>
<md-button class="md-primary md-raised"
ng-click="addServer(configuration.servers)"
aria-label="{{ 'action.add' | translate }}"
>
<md-icon class="material-icons">add</md-icon>
<span translate>extension.modbus-add-server</span>
</md-button>
</div>
</div>
</v-pane-content>
</v-pane>
</v-accordion>
<!--{{config}}-->
</md-card-content>
</md-card>

2
ui/src/app/extension/extensions-forms/extension-form-opc.tpl.html

@ -194,7 +194,7 @@
</md-input-container> </md-input-container>
</div> </div>
<v-accordion id="opc-keystore-accordion" class="vAccordion--default" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)"> <v-accordion id="opc-keystore-accordion" class="vAccordion--default" ng-if="server.security != 'None'" onexpand="expandValidation(index, id)" oncollapse="collapseValidation(index, id)">
<v-pane id="opc-keystore-pane__{{serverIndex}}" expanded="true"> <v-pane id="opc-keystore-pane__{{serverIndex}}" expanded="true">
<v-pane-header> <v-pane-header>
{{ 'extension.opc-keystore' | translate }} {{ 'extension.opc-keystore' | translate }}

3
ui/src/app/extension/index.js

@ -17,6 +17,8 @@ import ExtensionTableDirective from './extension-table.directive';
import ExtensionFormHttpDirective from './extensions-forms/extension-form-http.directive'; import ExtensionFormHttpDirective from './extensions-forms/extension-form-http.directive';
import ExtensionFormMqttDirective from './extensions-forms/extension-form-mqtt.directive' import ExtensionFormMqttDirective from './extensions-forms/extension-form-mqtt.directive'
import ExtensionFormOpcDirective from './extensions-forms/extension-form-opc.directive'; import ExtensionFormOpcDirective from './extensions-forms/extension-form-opc.directive';
import ExtensionFormModbusDirective from './extensions-forms/extension-form-modbus.directive';
import {ParseToNull} from './extension-dialog.controller'; import {ParseToNull} from './extension-dialog.controller';
export default angular.module('thingsboard.extension', []) export default angular.module('thingsboard.extension', [])
@ -24,5 +26,6 @@ export default angular.module('thingsboard.extension', [])
.directive('tbExtensionFormHttp', ExtensionFormHttpDirective) .directive('tbExtensionFormHttp', ExtensionFormHttpDirective)
.directive('tbExtensionFormMqtt', ExtensionFormMqttDirective) .directive('tbExtensionFormMqtt', ExtensionFormMqttDirective)
.directive('tbExtensionFormOpc', ExtensionFormOpcDirective) .directive('tbExtensionFormOpc', ExtensionFormOpcDirective)
.directive('tbExtensionFormModbus', ExtensionFormModbusDirective)
.directive('parseToNull', ParseToNull) .directive('parseToNull', ParseToNull)
.name; .name;

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

@ -886,8 +886,10 @@ export default angular.module('thingsboard.locale', [])
"response-timeout": "Response timeout in milliseconds", "response-timeout": "Response timeout in milliseconds",
"topic-expression": "Topic expression", "topic-expression": "Topic expression",
"client-scope": "Client scope", "client-scope": "Client scope",
"add-device": "Add device",
"opc-server": "Servers", "opc-server": "Servers",
"opc-add-server": "Add server", "opc-add-server": "Add server",
"opc-add-server-prompt": "Please add server",
"opc-application-name": "Application name", "opc-application-name": "Application name",
"opc-application-uri": "Application uri", "opc-application-uri": "Application uri",
"opc-scan-period-in-seconds": "Scan period in seconds", "opc-scan-period-in-seconds": "Scan period in seconds",
@ -902,6 +904,34 @@ export default angular.module('thingsboard.locale', [])
"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-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": { "sync": {
"status": "Status", "status": "Status",

4
ui/src/app/widget/lib/flot-widget.js

@ -251,7 +251,7 @@ export default class TbFlot {
if (this.tickUnits) { if (this.tickUnits) {
formatted += ' ' + this.tickUnits; formatted += ' ' + this.tickUnits;
} }
return formatted; return formatted;
}; };
@ -1097,7 +1097,7 @@ export default class TbFlot {
"axisTickDecimals": { "axisTickDecimals": {
"title": "Axis tick number of digits after floating point", "title": "Axis tick number of digits after floating point",
"type": "number", "type": "number",
"default": 0 "default": null
}, },
"axisTickSize": { "axisTickSize": {
"title": "Axis step size between ticks", "title": "Axis step size between ticks",

Loading…
Cancel
Save