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}"
boss_group_thread_count: "${NETTY_BOSS_GROUP_THREADS:1}"
worker_group_thread_count: "${NETTY_WORKER_GROUP_THREADS:12}"
max_payload_size: "${NETTY_MAX_PAYLOAD_SIZE:65536}"
# MQTT SSL configuration
ssl:
# 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;
@Value("${cassandra.init_retry_interval_ms}")
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
private CassandraSocketOptions socketOpts;
@ -90,8 +94,8 @@ public abstract class AbstractCassandraCluster {
.withClusterName(clusterName)
.withSocketOptions(socketOpts.getOpts())
.withPoolingOptions(new PoolingOptions()
.setMaxRequestsPerConnection(HostDistance.LOCAL, 32768)
.setMaxRequestsPerConnection(HostDistance.REMOTE, 32768));
.setMaxRequestsPerConnection(HostDistance.LOCAL, max_requests_local)
.setMaxRequestsPerConnection(HostDistance.REMOTE, max_requests_remote));
this.clusterBuilder.withQueryOptions(queryOpts.getOpts());
this.clusterBuilder.withCompression(StringUtils.isEmpty(compression) ? Compression.NONE : Compression.valueOf(compression.toUpperCase()));
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);
log.trace("Executing deleteEntityRelations [{}]", entity);
validate(entity);
List<ListenableFuture<List<EntityRelation>>> inboundRelationsListTo = new ArrayList<>();
List<ListenableFuture<List<EntityRelation>>> inboundRelationsList = new ArrayList<>();
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<Boolean>> inboundDeletions = Futures.transform(inboundRelationsTo, (List<List<EntityRelation>> relations) ->
ListenableFuture<List<List<EntityRelation>>> inboundRelations = Futures.allAsList(inboundRelationsList);
ListenableFuture<List<Boolean>> inboundDeletions = Futures.transform(inboundRelations, (List<List<EntityRelation>> relations) ->
getBooleans(relations, cache, true));
ListenableFuture<Boolean> inboundFuture = Futures.transform(inboundDeletions, getListToBooleanFunction());
@ -186,12 +186,12 @@ public class BaseRelationService implements RelationService {
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()) {
inboundRelationsListFrom.add(relationDao.findAllByFrom(entity, typeGroup));
outboundRelationsList.add(relationDao.findAllByFrom(entity, typeGroup));
}
ListenableFuture<List<List<EntityRelation>>> inboundRelationsFrom = Futures.allAsList(inboundRelationsListFrom);
Futures.transform(inboundRelationsFrom, (Function<List<List<EntityRelation>>, List<Boolean>>) relations ->
ListenableFuture<List<List<EntityRelation>>> outboundRelations = Futures.allAsList(outboundRelationsList);
Futures.transform(outboundRelations, (Function<List<List<EntityRelation>>, List<Boolean>>) relations ->
getBooleans(relations, cache, false));
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) {
List<Boolean> results = new ArrayList<>();
for (List<EntityRelation> relationList : relations) {
relationList.stream().forEach(relation -> {
checkFromDeleteSync(cache, results, relation, isRemove);
});
relationList.stream().forEach(relation -> checkFromDeleteSync(cache, results, relation, isRemove));
}
return results;
}
@ -211,10 +209,8 @@ public class BaseRelationService implements RelationService {
private void checkFromDeleteSync(Cache cache, List<Boolean> results, EntityRelation relation, boolean isRemove) {
if (isRemove) {
results.add(relationDao.deleteRelation(relation));
cacheEviction(relation, false, cache);
} else {
cacheEviction(relation, true, cache);
}
cacheEviction(relation, cache);
}
@Override
@ -222,12 +218,12 @@ public class BaseRelationService implements RelationService {
Cache cache = cacheManager.getCache(RELATIONS_CACHE);
log.trace("Executing deleteEntityRelationsAsync [{}]", entity);
validate(entity);
List<ListenableFuture<List<EntityRelation>>> inboundRelationsListTo = new ArrayList<>();
List<ListenableFuture<List<EntityRelation>>> inboundRelationsList = new ArrayList<>();
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<Boolean>> inboundDeletions = Futures.transform(inboundRelationsTo,
ListenableFuture<List<List<EntityRelation>>> inboundRelations = Futures.allAsList(inboundRelationsList);
ListenableFuture<List<Boolean>> inboundDeletions = Futures.transform(inboundRelations,
(AsyncFunction<List<List<EntityRelation>>, List<Boolean>>) relations -> {
List<ListenableFuture<Boolean>> results = getListenableFutures(relations, cache, true);
return Futures.allAsList(results);
@ -235,12 +231,12 @@ public class BaseRelationService implements RelationService {
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()) {
inboundRelationsListFrom.add(relationDao.findAllByTo(entity, typeGroup));
outboundRelationsList.add(relationDao.findAllByFrom(entity, typeGroup));
}
ListenableFuture<List<List<EntityRelation>>> inboundRelationsFrom = Futures.allAsList(inboundRelationsListFrom);
Futures.transform(inboundRelationsFrom, (AsyncFunction<List<List<EntityRelation>>, List<Boolean>>) relations -> {
ListenableFuture<List<List<EntityRelation>>> outboundRelations = Futures.allAsList(outboundRelationsList);
Futures.transform(outboundRelations, (AsyncFunction<List<List<EntityRelation>>, List<Boolean>>) relations -> {
List<ListenableFuture<Boolean>> results = getListenableFutures(relations, cache, false);
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) {
List<ListenableFuture<Boolean>> results = new ArrayList<>();
for (List<EntityRelation> relationList : relations) {
relationList.stream().forEach(relation -> {
checkFromDeleteAsync(cache, results, relation, isRemove);
});
relationList.stream().forEach(relation -> checkFromDeleteAsync(cache, results, relation, isRemove));
}
return results;
}
@ -262,13 +256,11 @@ public class BaseRelationService implements RelationService {
private void checkFromDeleteAsync(Cache cache, List<ListenableFuture<Boolean>> results, EntityRelation relation, boolean isRemove) {
if (isRemove) {
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<>();
fromToTypeAndTypeGroup.add(relation.getFrom());
fromToTypeAndTypeGroup.add(relation.getTo());
@ -287,18 +279,16 @@ public class BaseRelationService implements RelationService {
fromAndTypeGroup.add(relation.getTypeGroup());
cache.evict(fromAndTypeGroup);
if (!outboundOnly) {
List<Object> toAndTypeGroup = new ArrayList<>();
toAndTypeGroup.add(relation.getTo());
toAndTypeGroup.add(relation.getTypeGroup());
cache.evict(toAndTypeGroup);
List<Object> toTypeAndTypeGroup = new ArrayList<>();
fromTypeAndTypeGroup.add(relation.getTo());
fromTypeAndTypeGroup.add(relation.getType());
fromTypeAndTypeGroup.add(relation.getTypeGroup());
cache.evict(toTypeAndTypeGroup);
}
List<Object> toAndTypeGroup = new ArrayList<>();
toAndTypeGroup.add(relation.getTo());
toAndTypeGroup.add(relation.getTypeGroup());
cache.evict(toAndTypeGroup);
List<Object> toTypeAndTypeGroup = new ArrayList<>();
fromTypeAndTypeGroup.add(relation.getTo());
fromTypeAndTypeGroup.add(relation.getType());
fromTypeAndTypeGroup.add(relation.getTypeGroup());
cache.evict(toTypeAndTypeGroup);
}
@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.port: "9042"
database.type: "cassandra"
cache.type: "redis"
redis.host: "redis-service"
---
apiVersion: apps/v1beta1
kind: StatefulSet
@ -127,6 +129,16 @@ spec:
valueFrom:
fieldRef:
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:
- sh
- -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> {
private static final int MAX_PAYLOAD_SIZE = 64 * 1024 * 1024;
private final SessionMsgProcessor processor;
private final DeviceService deviceService;
private final DeviceAuthService authService;
@ -42,10 +40,11 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
private final MqttTransportAdaptor adaptor;
private final MqttSslHandlerProvider sslHandlerProvider;
private final QuotaService quotaService;
private final int maxPayloadSize;
public MqttTransportServerInitializer(SessionMsgProcessor processor, DeviceService deviceService, DeviceAuthService authService, RelationService relationService,
MqttTransportAdaptor adaptor, MqttSslHandlerProvider sslHandlerProvider,
QuotaService quotaService) {
QuotaService quotaService, int maxPayloadSize) {
this.processor = processor;
this.deviceService = deviceService;
this.authService = authService;
@ -53,6 +52,7 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
this.adaptor = adaptor;
this.sslHandlerProvider = sslHandlerProvider;
this.quotaService = quotaService;
this.maxPayloadSize = maxPayloadSize;
}
@Override
@ -63,7 +63,7 @@ public class MqttTransportServerInitializer extends ChannelInitializer<SocketCha
sslHandler = sslHandlerProvider.getSslHandler();
pipeline.addLast(sslHandler);
}
pipeline.addLast("decoder", new MqttDecoder(MAX_PAYLOAD_SIZE));
pipeline.addLast("decoder", new MqttDecoder(maxPayloadSize));
pipeline.addLast("encoder", MqttEncoder.INSTANCE);
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;
@Value("${mqtt.netty.worker_group_thread_count}")
private Integer workerGroupThreadCount;
@Value("${mqtt.netty.max_payload_size}")
private Integer maxPayloadSize;
private MqttTransportAdaptor adaptor;
@ -106,7 +107,7 @@ public class MqttTransportService {
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new MqttTransportServerInitializer(processor, deviceService, authService, relationService,
adaptor, sslHandlerProvider, quotaService));
adaptor, sslHandlerProvider, quotaService, maxPayloadSize));
serverChannel = b.bind(host, port).sync().channel();
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-icons/angular-material-icons.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 'mdPickers/dist/mdPickers.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: {
http: "HTTP",
mqtt: "MQTT",
opc: "OPC UA"
opc: "OPC UA",
modbus: "MODBUS"
},
extensionValueType: {
string: 'value.string',
@ -450,6 +451,26 @@ export default angular.module('thingsboard.types', [])
PKCS12: "PKCS12",
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: {
value: "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": []
};
}
if (vm.extension.type === "OPC UA") {
if (vm.extension.type === "OPC UA" || vm.extension.type === "MODBUS") {
vm.extension.configuration = {
"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-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-modbus configuration="vm.extension.configuration" ng-if="vm.extension.type && vm.extension.type == vm.types.extensionType.modbus"></div>
</fieldset>
</md-content>
</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>
</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-header>
{{ '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 ExtensionFormMqttDirective from './extensions-forms/extension-form-mqtt.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';
export default angular.module('thingsboard.extension', [])
@ -24,5 +26,6 @@ export default angular.module('thingsboard.extension', [])
.directive('tbExtensionFormHttp', ExtensionFormHttpDirective)
.directive('tbExtensionFormMqtt', ExtensionFormMqttDirective)
.directive('tbExtensionFormOpc', ExtensionFormOpcDirective)
.directive('tbExtensionFormModbus', ExtensionFormModbusDirective)
.directive('parseToNull', ParseToNull)
.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",
"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",
@ -902,6 +904,34 @@ export default angular.module('thingsboard.locale', [])
"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",

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

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

Loading…
Cancel
Save