diff --git a/application/pom.xml b/application/pom.xml
index c1a49b09e3..e3dcd9f0f9 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -85,6 +85,10 @@
org.thingsboard.common.transport
coap
+
+ org.thingsboard.common.transport
+ snmp
+
org.thingsboard
dao
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index 0cc7c08669..437a672c54 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -566,6 +566,8 @@ transport:
bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}"
bind_port: "${COAP_BIND_PORT:5683}"
timeout: "${COAP_TIMEOUT:10000}"
+ snmp:
+ enabled: "${SNMP_ENABLED:true}"
swagger:
api_path_regex: "${SWAGGER_API_PATH_REGEX:/api.*}"
diff --git a/common/data/pom.xml b/common/data/pom.xml
index 4454e628c0..bc977b1c52 100644
--- a/common/data/pom.xml
+++ b/common/data/pom.xml
@@ -79,6 +79,10 @@
org.thingsboard
protobuf-dynamic
+
+ org.apache.commons
+ commons-lang3
+
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceTransportType.java b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceTransportType.java
index 9b9a021fcf..579cdde968 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/DeviceTransportType.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/DeviceTransportType.java
@@ -18,5 +18,6 @@ package org.thingsboard.server.common.data;
public enum DeviceTransportType {
DEFAULT,
MQTT,
- LWM2M
+ LWM2M,
+ SNMP
}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/JsonBasedTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/JsonBasedTransportConfiguration.java
new file mode 100644
index 0000000000..c65e9dd972
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/JsonBasedTransportConfiguration.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016-2021 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.device;
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public abstract class JsonBasedTransportConfiguration {
+
+ @JsonIgnore
+ private final Map properties = new HashMap<>();
+
+ @JsonAnyGetter
+ public Map properties() {
+ return this.properties;
+ }
+
+ @JsonAnySetter
+ public void put(String name, Object value) {
+ this.properties.put(name, value);
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceTransportConfiguration.java
index 5409dcd339..0ae9f26cf3 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceTransportConfiguration.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/DeviceTransportConfiguration.java
@@ -29,7 +29,8 @@ import org.thingsboard.server.common.data.DeviceTransportType;
@JsonSubTypes({
@JsonSubTypes.Type(value = DefaultDeviceTransportConfiguration.class, name = "DEFAULT"),
@JsonSubTypes.Type(value = MqttDeviceTransportConfiguration.class, name = "MQTT"),
- @JsonSubTypes.Type(value = Lwm2mDeviceTransportConfiguration.class, name = "LWM2M")})
+ @JsonSubTypes.Type(value = Lwm2mDeviceTransportConfiguration.class, name = "LWM2M"),
+ @JsonSubTypes.Type(value = SnmpDeviceTransportConfiguration.class, name = "SNMP")})
public interface DeviceTransportConfiguration {
@JsonIgnore
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/data/SnmpDeviceTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/SnmpDeviceTransportConfiguration.java
new file mode 100644
index 0000000000..3db31b4f2a
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/data/SnmpDeviceTransportConfiguration.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright © 2016-2021 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.device.data;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import lombok.Data;
+import org.apache.commons.lang3.StringUtils;
+import org.thingsboard.server.common.data.DeviceTransportType;
+
+@Data
+public class SnmpDeviceTransportConfiguration implements DeviceTransportConfiguration {
+
+ private String address;
+ private int port;
+ private String community;
+ private String protocolVersion;
+
+ @Override
+ public DeviceTransportType getType() {
+ return DeviceTransportType.SNMP;
+ }
+
+ @JsonIgnore
+ public boolean isValid() {
+ return StringUtils.isNotEmpty(this.address)
+ && this.port > 0
+ && StringUtils.isNotEmpty(this.community)
+ && StringUtils.isNotEmpty(this.protocolVersion);
+ }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileTransportConfiguration.java
index 2feba45d34..ffbe5f7048 100644
--- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileTransportConfiguration.java
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DeviceProfileTransportConfiguration.java
@@ -19,7 +19,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
-import org.thingsboard.server.common.data.DeviceProfileType;
import org.thingsboard.server.common.data.DeviceTransportType;
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -28,9 +27,10 @@ import org.thingsboard.server.common.data.DeviceTransportType;
include = JsonTypeInfo.As.PROPERTY,
property = "type")
@JsonSubTypes({
- @JsonSubTypes.Type(value = DefaultDeviceProfileTransportConfiguration.class, name = "DEFAULT"),
- @JsonSubTypes.Type(value = MqttDeviceProfileTransportConfiguration.class, name = "MQTT"),
- @JsonSubTypes.Type(value = Lwm2mDeviceProfileTransportConfiguration.class, name = "LWM2M")})
+ @JsonSubTypes.Type(value = DefaultDeviceProfileTransportConfiguration.class, name = "DEFAULT"),
+ @JsonSubTypes.Type(value = MqttDeviceProfileTransportConfiguration.class, name = "MQTT"),
+ @JsonSubTypes.Type(value = Lwm2mDeviceProfileTransportConfiguration.class, name = "LWM2M"),
+ @JsonSubTypes.Type(value = SnmpProfileTransportConfiguration.class, name = "SNMP")})
public interface DeviceProfileTransportConfiguration {
@JsonIgnore
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpDeviceProfileKvMapping.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpDeviceProfileKvMapping.java
new file mode 100644
index 0000000000..6eff7cbd34
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpDeviceProfileKvMapping.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016-2021 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.device.profile;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.kv.DataType;
+
+//TODO: rename class
+@Data
+public class SnmpDeviceProfileKvMapping {
+ private String key;
+ private DataType type;
+ private String method;
+ private String oid;
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpProfileTransportConfiguration.java
new file mode 100644
index 0000000000..a2529e74e9
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/SnmpProfileTransportConfiguration.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright © 2016-2021 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.device.profile;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import lombok.Data;
+import org.thingsboard.server.common.data.DeviceTransportType;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+@Data
+public class SnmpProfileTransportConfiguration implements DeviceProfileTransportConfiguration {
+
+ private int poolPeriodMs;
+ private int timeoutMs;
+ private int retries;
+ private List attributes;
+ private List telemetry;
+
+ @Override
+ public DeviceTransportType getType() {
+ return DeviceTransportType.SNMP;
+ }
+
+ @JsonIgnore
+ public List getKvMappings() {
+ return Stream.concat(attributes.stream(), telemetry.stream()).collect(Collectors.toList());
+ }
+}
diff --git a/common/transport/pom.xml b/common/transport/pom.xml
index 957b0d518c..35722cdd1d 100644
--- a/common/transport/pom.xml
+++ b/common/transport/pom.xml
@@ -39,6 +39,7 @@
mqtt
http
coap
+ snmp
diff --git a/common/transport/snmp/pom.xml b/common/transport/snmp/pom.xml
new file mode 100644
index 0000000000..1f334b9699
--- /dev/null
+++ b/common/transport/snmp/pom.xml
@@ -0,0 +1,66 @@
+
+
+ 4.0.0
+
+
+ org.thingsboard.common
+ 3.2.1-SNAPSHOT
+ transport
+
+
+ org.thingsboard.common.transport
+ snmp
+ jar
+
+ Thingsboard SNMP Transport Common
+ https://thingsboard.io
+
+
+ UTF-8
+ ${basedir}/../../..
+
+
+
+
+ org.thingsboard.common.transport
+ transport-api
+
+
+ org.springframework
+ spring-context-support
+
+
+ org.springframework
+ spring-context
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.snmp4j
+ snmp4j
+
+
+ org.thingsboard.common
+ dao-api
+
+
+
diff --git a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java
new file mode 100644
index 0000000000..66f852a209
--- /dev/null
+++ b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportContext.java
@@ -0,0 +1,142 @@
+/**
+ * Copyright © 2016-2021 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.snmp;
+
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.snmp4j.PDU;
+import org.snmp4j.Snmp;
+import org.snmp4j.smi.OID;
+import org.snmp4j.smi.VariableBinding;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.DeviceProfile;
+import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration;
+import org.thingsboard.server.common.data.device.profile.SnmpDeviceProfileKvMapping;
+import org.thingsboard.server.common.data.device.profile.SnmpProfileTransportConfiguration;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.common.data.security.DeviceCredentialsType;
+import org.thingsboard.server.common.transport.TransportContext;
+import org.thingsboard.server.dao.device.DeviceCredentialsService;
+import org.thingsboard.server.transport.snmp.session.DeviceSessionCtx;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.stream.Collectors;
+
+@Service("SnmpTransportContext")
+@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.snmp.enabled}'=='true')")
+@Slf4j
+public class SnmpTransportContext extends TransportContext {
+ @Autowired
+ DeviceCredentialsService deviceCredentialsService;
+
+ @Autowired
+ SnmpTransportService snmpTransportService;
+
+ @Getter
+ private final Map profileTransportConfig = new ConcurrentHashMap<>();
+ @Getter
+ private final Map> pdusPerProfile = new ConcurrentHashMap<>();
+ @Getter
+ private final Map deviceSessions = new ConcurrentHashMap<>();
+
+ public Optional findAttributesMapping(DeviceProfileId deviceProfileId, OID responseOid) {
+ if (profileTransportConfig.containsKey(deviceProfileId)) {
+ return findMapping(responseOid, profileTransportConfig.get(deviceProfileId).getAttributes());
+ }
+ return Optional.empty();
+ }
+
+ public Optional findTelemetryMapping(DeviceProfileId deviceProfileId, OID responseOid) {
+ if (profileTransportConfig.containsKey(deviceProfileId)) {
+ return findMapping(responseOid, profileTransportConfig.get(deviceProfileId).getTelemetry());
+ }
+ return Optional.empty();
+ }
+
+ private Optional findMapping(OID responseOid, List mappings) {
+ return mappings.stream()
+ .filter(kvMapping -> new OID(kvMapping.getOid()).equals(responseOid))
+ //TODO: OID shouldn't be duplicated in the config, add backend and UI verification
+ .findFirst();
+ }
+
+ public void initPduListPerProfile() {
+ profileTransportConfig.forEach(this::updatePduListPerProfile);
+ }
+
+ public void updatePduListPerProfile(DeviceProfileId id, SnmpProfileTransportConfiguration config) {
+ pdusPerProfile.put(id, createPduList(config));
+ }
+
+ public void updateDeviceSessionCtx(Device device, DeviceProfile deviceProfile, Snmp snmp) {
+ DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(device.getTenantId(), device.getId());
+ if (DeviceCredentialsType.ACCESS_TOKEN.equals(credentials.getCredentialsType())) {
+ SnmpDeviceTransportConfiguration snmpDeviceTransportConfiguration = (SnmpDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration();
+ if (snmpDeviceTransportConfiguration.isValid()) {
+ DeviceSessionCtx deviceSessionCtx = new DeviceSessionCtx(this, credentials.getCredentialsId(), snmpDeviceTransportConfiguration, snmp, device.getId(), deviceProfile);
+ deviceSessionCtx.createSessionInfo(ctx -> getTransportService().registerAsyncSession(deviceSessionCtx.getSessionInfo(), deviceSessionCtx));
+ this.deviceSessions.put(device.getId(), deviceSessionCtx);
+ }
+ } else {
+ log.warn("[{}] Expected credentials type is {} but found {}", device.getId(), DeviceCredentialsType.ACCESS_TOKEN, credentials.getCredentialsType());
+ }
+ }
+
+ public ExecutorService getSnmpCallbackExecutor() {
+ return snmpTransportService.getSnmpCallbackExecutor();
+ }
+
+ private List createPduList(SnmpProfileTransportConfiguration deviceProfileConfig) {
+ Map> varBindingPerMethod = new HashMap<>();
+
+ deviceProfileConfig.getKvMappings().forEach(mapping -> varBindingPerMethod
+ .computeIfAbsent(mapping.getMethod(), v -> new ArrayList<>())
+ .add(new VariableBinding(new OID(mapping.getOid()))));
+
+ return varBindingPerMethod.keySet().stream()
+ .map(method -> {
+ PDU request = new PDU();
+ request.setType(getSnmpMethod(method));
+ request.addAll(varBindingPerMethod.get(method));
+ return request;
+ })
+ .collect(Collectors.toList());
+ }
+
+ //TODO: Extract SNMP methods to enum
+ private int getSnmpMethod(String configMethod) {
+ switch (configMethod) {
+ case "get":
+ return PDU.GET;
+ case "getNext":
+ case "response":
+ case "set":
+ default:
+ return -1;
+ }
+ }
+}
diff --git a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportService.java b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportService.java
new file mode 100644
index 0000000000..5d8e4d84b4
--- /dev/null
+++ b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/SnmpTransportService.java
@@ -0,0 +1,147 @@
+/**
+ * Copyright © 2016-2021 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.snmp;
+
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.snmp4j.Snmp;
+import org.snmp4j.transport.DefaultUdpTransportMapping;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Service;
+import org.thingsboard.common.util.ThingsBoardThreadFactory;
+import org.thingsboard.server.common.data.DeviceInfo;
+import org.thingsboard.server.common.data.DeviceProfile;
+import org.thingsboard.server.common.data.DeviceTransportType;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.device.profile.SnmpProfileTransportConfiguration;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageDataIterable;
+import org.thingsboard.server.dao.device.DeviceProfileService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.tenant.TenantService;
+import org.thingsboard.server.transport.snmp.session.DeviceSessionCtx;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.io.IOException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+@Service("SnmpTransportService")
+@ConditionalOnExpression("'${service.type:null}'=='tb-transport' || ('${service.type:null}'=='monolith' && '${transport.api_enabled:true}'=='true' && '${transport.snmp.enabled}'=='true')")
+@Slf4j
+public class SnmpTransportService {
+
+ private static final int ENTITY_PACK_LIMIT = 1024;
+
+ @Autowired
+ private SnmpTransportContext snmpTransportContext;
+
+ @Autowired
+ DeviceProfileService deviceProfileService;
+
+ @Autowired
+ TenantService tenantService;
+
+ @Autowired
+ DeviceService deviceService;
+
+ @Getter
+ private ExecutorService snmpCallbackExecutor;
+ private Snmp snmp;
+ private ScheduledExecutorService pollingExecutor;
+
+ @PostConstruct
+ public void init() {
+ log.info("Starting SNMP transport...");
+ pollingExecutor = Executors.newScheduledThreadPool(1, ThingsBoardThreadFactory.forName("snmp-polling"));
+ //TODO: Set parallelism value in the config
+ snmpCallbackExecutor = Executors.newWorkStealingPool(20);
+ initializeSnmp();
+ log.info("SNMP transport started!");
+ }
+
+ @PreDestroy
+ public void shutdown() {
+ log.info("Stopping SNMP transport!");
+ if (pollingExecutor != null) {
+ pollingExecutor.shutdownNow();
+ }
+ if (snmpCallbackExecutor != null) {
+ snmpCallbackExecutor.shutdownNow();
+ }
+ if (snmp != null) {
+ try {
+ snmp.close();
+ } catch (IOException e) {
+ log.error(e.getMessage(), e);
+ }
+ }
+ log.info("SNMP transport stopped!");
+ }
+
+ @EventListener(ApplicationReadyEvent.class)
+ @Order(value = 2)
+ public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
+ log.info("Received application ready event. Starting SNMP polling.");
+ initSessionCtxList();
+ startPolling();
+ }
+
+ private void initializeSnmp() {
+ try {
+ this.snmp = new Snmp(new DefaultUdpTransportMapping());
+ this.snmp.listen();
+ } catch (IOException e) {
+ //TODO: what should be done if transport wasn't initialized?
+ log.error(e.getMessage(), e);
+ }
+ }
+
+ private void initSessionCtxList() {
+ //TODO: This approach works for monolith, in cluster the same data will be fetched by each node.
+ for (Tenant tenant : new PageDataIterable<>(tenantService::findTenants, ENTITY_PACK_LIMIT)) {
+ TenantId tenantId = tenant.getTenantId();
+ for (DeviceProfile deviceProfile : new PageDataIterable<>(pageLink -> deviceProfileService.findDeviceProfiles(tenantId, pageLink), ENTITY_PACK_LIMIT)) {
+ if (DeviceTransportType.SNMP.equals(deviceProfile.getTransportType())) {
+ snmpTransportContext.getProfileTransportConfig().put(deviceProfile.getId(),
+ (SnmpProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration());
+ initDeviceSessions(deviceProfile);
+ }
+ }
+ }
+ snmpTransportContext.initPduListPerProfile();
+ }
+
+ private void initDeviceSessions(DeviceProfile deviceProfile) {
+ for (DeviceInfo deviceInfo : new PageDataIterable<>(pageLink -> deviceService.findDeviceInfosByTenantIdAndDeviceProfileId(deviceProfile.getTenantId(), deviceProfile.getId(), pageLink), ENTITY_PACK_LIMIT)) {
+ snmpTransportContext.updateDeviceSessionCtx(deviceInfo, deviceProfile, snmp);
+ }
+ }
+
+ private void startPolling() {
+ //TODO: Get poll period from configuration;
+ int poolPeriodSeconds = 1;
+ pollingExecutor.scheduleAtFixedRate(() -> snmpTransportContext.getDeviceSessions().values().forEach(DeviceSessionCtx::executeSnmpRequest),
+ 0, poolPeriodSeconds, TimeUnit.SECONDS);
+ }
+}
diff --git a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/DeviceSessionCtx.java b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/DeviceSessionCtx.java
new file mode 100644
index 0000000000..424b964387
--- /dev/null
+++ b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/DeviceSessionCtx.java
@@ -0,0 +1,206 @@
+/**
+ * Copyright © 2016-2021 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.snmp.session;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.snmp4j.CommunityTarget;
+import org.snmp4j.Snmp;
+import org.snmp4j.Target;
+import org.snmp4j.mp.SnmpConstants;
+import org.snmp4j.smi.GenericAddress;
+import org.snmp4j.smi.OctetString;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.DeviceProfile;
+import org.thingsboard.server.common.data.DeviceTransportType;
+import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration;
+import org.thingsboard.server.common.data.device.profile.SnmpProfileTransportConfiguration;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.transport.SessionMsgListener;
+import org.thingsboard.server.common.transport.TransportServiceCallback;
+import org.thingsboard.server.common.transport.auth.SessionInfoCreator;
+import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
+import org.thingsboard.server.common.transport.session.DeviceAwareSessionContext;
+import org.thingsboard.server.gen.transport.TransportProtos;
+import org.thingsboard.server.gen.transport.TransportProtos.AttributeUpdateNotificationMsg;
+import org.thingsboard.server.gen.transport.TransportProtos.GetAttributeResponseMsg;
+import org.thingsboard.server.gen.transport.TransportProtos.SessionCloseNotificationProto;
+import org.thingsboard.server.gen.transport.TransportProtos.ToDeviceRpcRequestMsg;
+import org.thingsboard.server.gen.transport.TransportProtos.ToServerRpcResponseMsg;
+import org.thingsboard.server.transport.snmp.SnmpTransportContext;
+
+import java.io.IOException;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+@Slf4j
+public class DeviceSessionCtx extends DeviceAwareSessionContext implements SessionMsgListener {
+ private final AtomicInteger msgIdSeq = new AtomicInteger(0);
+
+ @Getter
+ @Setter
+ private SnmpDeviceTransportConfiguration deviceTransportConfig;
+ @Getter
+ @Setter
+ private SnmpSessionListener snmpSessionListener;
+ @Getter
+ @Setter
+ private Target target;
+ @Getter
+ @Setter
+ private volatile TransportProtos.SessionInfoProto sessionInfo;
+
+ private Snmp snmp;
+ private SnmpProfileTransportConfiguration snmpProfileTransportConfiguration;
+ private long previousRequestExecutedAt = 0;
+
+ public DeviceSessionCtx(SnmpTransportContext transportContext, String token, SnmpDeviceTransportConfiguration deviceTransportConfig,
+ Snmp snmp, DeviceId deviceId, DeviceProfile deviceProfile) {
+ super(UUID.randomUUID());
+ this.snmpSessionListener = new SnmpSessionListener(transportContext, token);
+ super.setDeviceId(deviceId);
+ super.setDeviceProfile(deviceProfile);
+ //TODO: What should be done if snmp null?
+ if (snmp != null) {
+ this.snmp = snmp;
+ }
+ this.snmpProfileTransportConfiguration = (SnmpProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration();
+ initTarget(this.snmpProfileTransportConfiguration, deviceTransportConfig);
+ }
+
+ @Override
+ public int nextMsgId() {
+ return msgIdSeq.incrementAndGet();
+ }
+
+ @Override
+ public void onGetAttributesResponse(GetAttributeResponseMsg getAttributesResponse) {
+ }
+
+ @Override
+ public void onAttributeUpdate(AttributeUpdateNotificationMsg attributeUpdateNotification) {
+ }
+
+ @Override
+ public void onRemoteSessionCloseCommand(SessionCloseNotificationProto sessionCloseNotification) {
+ }
+
+ @Override
+ public void onToDeviceRpcRequest(ToDeviceRpcRequestMsg toDeviceRequest) {
+ }
+
+ @Override
+ public void onToServerRpcResponse(ToServerRpcResponseMsg toServerResponse) {
+ }
+
+ @Override
+ public void onDeviceProfileUpdate(TransportProtos.SessionInfoProto newSessionInfo, DeviceProfile deviceProfile) {
+ super.onDeviceProfileUpdate(sessionInfo, deviceProfile);
+ if (DeviceTransportType.SNMP.equals(deviceProfile.getTransportType())) {
+ snmpProfileTransportConfiguration = (SnmpProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration();
+ snmpSessionListener.getSnmpTransportContext().getProfileTransportConfig().put(
+ deviceProfile.getId(),
+ snmpProfileTransportConfiguration);
+ snmpSessionListener.getSnmpTransportContext().updatePduListPerProfile(deviceProfile.getId(), snmpProfileTransportConfiguration);
+ } else {
+ //TODO: should the context be removed from the map?
+ }
+ }
+
+ @Override
+ public void onDeviceUpdate(TransportProtos.SessionInfoProto sessionInfo, Device device, Optional deviceProfileOpt) {
+ super.onDeviceUpdate(sessionInfo, device, deviceProfileOpt);
+ if (super.getDeviceProfile() != null && DeviceTransportType.SNMP.equals(super.getDeviceProfile().getTransportType())) {
+ snmpSessionListener.getSnmpTransportContext().updateDeviceSessionCtx(device, deviceProfile, null);
+ SnmpProfileTransportConfiguration profileTransportConfig = (SnmpProfileTransportConfiguration) deviceProfile.getProfileData().getTransportConfiguration();
+ SnmpDeviceTransportConfiguration deviceTransportConfig = (SnmpDeviceTransportConfiguration) device.getDeviceData().getTransportConfiguration();
+ initTarget(profileTransportConfig, deviceTransportConfig);
+ } else {
+ //TODO: should the context be removed from the map?
+ }
+ }
+
+ public void createSessionInfo(Consumer registerSession) {
+ getSnmpSessionListener().getSnmpTransportContext().getTransportService().process(DeviceTransportType.SNMP,
+ TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(getSnmpSessionListener().getToken()).build(),
+ new TransportServiceCallback() {
+ @Override
+ public void onSuccess(ValidateDeviceCredentialsResponse msg) {
+ if (msg.hasDeviceInfo()) {
+ sessionInfo = SessionInfoCreator.create(msg, getSnmpSessionListener().getSnmpTransportContext(), UUID.randomUUID());
+ registerSession.accept(sessionInfo);
+ setDeviceInfo(msg.getDeviceInfo());
+ } else {
+ log.warn("[{}] Failed to process device auth", getDeviceId());
+ }
+ }
+
+ @Override
+ public void onError(Throwable e) {
+ log.warn("[{}] Failed to process device auth", getDeviceId(), e);
+ }
+ });
+ }
+
+ public void executeSnmpRequest() {
+ long timeNow = System.currentTimeMillis();
+ long nextRequestExecutionTime = previousRequestExecutedAt + snmpProfileTransportConfiguration.getPoolPeriodMs();
+ if (nextRequestExecutionTime < timeNow) {
+ previousRequestExecutedAt = timeNow;
+
+ snmpSessionListener.getSnmpTransportContext().getPdusPerProfile().get(deviceProfile.getId()).forEach(pdu -> {
+ try {
+ log.debug("[{}] Sending SNMP message...", pdu.getRequestID());
+ snmp.send(pdu,
+ target,
+ deviceProfile.getId(),
+ snmpSessionListener);
+ } catch (IOException e) {
+ log.error(e.getMessage(), e);
+ }
+ });
+ }
+ }
+
+ private void initTarget(SnmpProfileTransportConfiguration profileTransportConfig, SnmpDeviceTransportConfiguration deviceTransportConfig) {
+ this.deviceTransportConfig = deviceTransportConfig;
+ CommunityTarget communityTarget = new CommunityTarget();
+ communityTarget.setAddress(GenericAddress.parse(GenericAddress.TYPE_UDP + ":" + this.deviceTransportConfig.getAddress() + "/" + this.deviceTransportConfig.getPort()));
+ communityTarget.setVersion(getSnmpVersion(this.deviceTransportConfig.getProtocolVersion()));
+ communityTarget.setCommunity(new OctetString(this.deviceTransportConfig.getCommunity()));
+ communityTarget.setTimeout(profileTransportConfig.getTimeoutMs());
+ communityTarget.setRetries(profileTransportConfig.getRetries());
+ this.target = communityTarget;
+ log.info("SNMP target initialized: {}", this.target);
+ }
+
+ //TODO: replace with enum, wtih preliminary discussion of type version in config (string or integer)
+ private int getSnmpVersion(String configSnmpVersion) {
+ switch (configSnmpVersion) {
+ case ("v1"):
+ return SnmpConstants.version1;
+ case ("v2c"):
+ return SnmpConstants.version2c;
+ case ("v3"):
+ return SnmpConstants.version3;
+ default:
+ return -1;
+ }
+ }
+}
diff --git a/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/SnmpSessionListener.java b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/SnmpSessionListener.java
new file mode 100644
index 0000000000..35aa1a9347
--- /dev/null
+++ b/common/transport/snmp/src/main/java/org/thingsboard/server/transport/snmp/session/SnmpSessionListener.java
@@ -0,0 +1,173 @@
+/**
+ * Copyright © 2016-2021 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.snmp.session;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonSyntaxException;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.snmp4j.PDU;
+import org.snmp4j.Snmp;
+import org.snmp4j.event.ResponseEvent;
+import org.snmp4j.event.ResponseListener;
+import org.snmp4j.smi.VariableBinding;
+import org.thingsboard.server.common.data.DeviceTransportType;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
+import org.thingsboard.server.common.data.kv.DataType;
+import org.thingsboard.server.common.transport.TransportContext;
+import org.thingsboard.server.common.transport.TransportService;
+import org.thingsboard.server.common.transport.TransportServiceCallback;
+import org.thingsboard.server.common.transport.adaptor.AdaptorException;
+import org.thingsboard.server.common.transport.adaptor.JsonConverter;
+import org.thingsboard.server.common.transport.auth.SessionInfoCreator;
+import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse;
+import org.thingsboard.server.gen.transport.TransportProtos;
+import org.thingsboard.server.transport.snmp.SnmpTransportContext;
+
+import java.util.UUID;
+import java.util.function.Consumer;
+
+@Slf4j
+@AllArgsConstructor
+public class SnmpSessionListener implements ResponseListener {
+
+ @Getter
+ private final SnmpTransportContext snmpTransportContext;
+
+ @Getter
+ private final String token;
+
+ @Override
+ public void onResponse(ResponseEvent event) {
+ ((Snmp) event.getSource()).cancel(event.getRequest(), this);
+ snmpTransportContext.getSnmpCallbackExecutor().submit(() -> processSnmpResponse(event));
+ }
+
+ private void processSnmpResponse(ResponseEvent event) {
+ PDU response = event.getResponse();
+ if (event.getError() != null) {
+ log.warn("Response error: {}", event.getError().getMessage(), event.getError());
+ }
+
+ if (response != null) {
+ log.debug("[{}] Processing SNMP response: {}", response.getRequestID(), response);
+
+ DeviceProfileId deviceProfileId = (DeviceProfileId) event.getUserObject();
+ TransportService transportService = snmpTransportContext.getTransportService();
+ for (int i = 0; i < response.size(); i++) {
+ VariableBinding vb = response.get(i);
+ snmpTransportContext.findAttributesMapping(deviceProfileId, vb.getOid()).ifPresent(kvMapping -> transportService.process(DeviceTransportType.DEFAULT,
+ TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(token).build(),
+ new DeviceAuthCallback(snmpTransportContext, sessionInfo -> {
+ try {
+ transportService.process(sessionInfo,
+ convertToPostAttributes(kvMapping.getKey(), kvMapping.getType(), vb.toValueString()),
+ TransportServiceCallback.EMPTY);
+ reportActivity(sessionInfo);
+ } catch (Exception e) {
+ log.warn("Failed to process SNMP response: {}", e.getMessage(), e);
+ }
+ })));
+ snmpTransportContext.findTelemetryMapping(deviceProfileId, vb.getOid()).ifPresent(kvMapping -> transportService.process(DeviceTransportType.DEFAULT,
+ TransportProtos.ValidateDeviceTokenRequestMsg.newBuilder().setToken(token).build(),
+ new DeviceAuthCallback(snmpTransportContext, sessionInfo -> {
+ try {
+ transportService.process(sessionInfo,
+ convertToPostTelemetry(kvMapping.getKey(), kvMapping.getType(), vb.toValueString()),
+ TransportServiceCallback.EMPTY);
+ reportActivity(sessionInfo);
+
+ } catch (Exception e) {
+ log.warn("Failed to process SNMP response: {}", e.getMessage(), e);
+ }
+ })));
+ }
+ } else {
+ log.warn("No SNMP response, requestId: {}", event.getRequest().getRequestID());
+ }
+ }
+
+ private TransportProtos.PostAttributeMsg convertToPostAttributes(String keyName, DataType dataType, String payload) throws AdaptorException {
+ try {
+ return JsonConverter.convertToAttributesProto(getKvJson(keyName, dataType, payload));
+ } catch (IllegalStateException | JsonSyntaxException ex) {
+ //TODO: change the exception type
+ throw new AdaptorException(ex);
+ }
+ }
+
+ private TransportProtos.PostTelemetryMsg convertToPostTelemetry(String keyName, DataType dataType, String payload) throws AdaptorException {
+ try {
+ return JsonConverter.convertToTelemetryProto(getKvJson(keyName, dataType, payload));
+ } catch (IllegalStateException | JsonSyntaxException ex) {
+ //TODO: change the exception type
+ throw new AdaptorException(ex);
+ }
+ }
+
+ private JsonElement getKvJson(String keyName, DataType dataType, String payload) throws AdaptorException {
+ JsonObject result = new JsonObject();
+ switch (dataType) {
+ case LONG:
+ result.addProperty(keyName, Long.parseLong(payload));
+ break;
+ case BOOLEAN:
+ result.addProperty(keyName, Boolean.parseBoolean(payload));
+ break;
+ case DOUBLE:
+ result.addProperty(keyName, Double.parseDouble(payload));
+ break;
+ case STRING:
+ result.addProperty(keyName, payload);
+ break;
+ default:
+ //TODO: change the exception type
+ throw new AdaptorException("Unsupported data type");
+ }
+ return new JsonParser().parse(result.toString());
+ }
+
+ private void reportActivity(TransportProtos.SessionInfoProto sessionInfo) {
+ snmpTransportContext.getTransportService().process(sessionInfo, TransportProtos.SubscriptionInfoProto.newBuilder()
+ .setAttributeSubscription(false)
+ .setRpcSubscription(false)
+ .setLastActivityTime(System.currentTimeMillis())
+ .build(), TransportServiceCallback.EMPTY);
+ }
+
+ @AllArgsConstructor
+ private static class DeviceAuthCallback implements TransportServiceCallback {
+ private final TransportContext transportContext;
+ private final Consumer onSuccess;
+
+ @Override
+ public void onSuccess(ValidateDeviceCredentialsResponse msg) {
+ if (msg.hasDeviceInfo()) {
+ onSuccess.accept(SessionInfoCreator.create(msg, transportContext, UUID.randomUUID()));
+ } else {
+ log.warn("Failed to process device auth");
+ }
+ }
+
+ @Override
+ public void onError(Throwable e) {
+ log.warn("Failed to process device auth", e);
+ }
+ }
+}
diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
index cd161d650e..c088d18d3c 100644
--- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
+++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java
@@ -47,6 +47,7 @@ import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConf
import org.thingsboard.server.common.data.device.data.DeviceData;
import org.thingsboard.server.common.data.device.data.Lwm2mDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.MqttDeviceTransportConfiguration;
+import org.thingsboard.server.common.data.device.data.SnmpDeviceTransportConfiguration;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
@@ -240,6 +241,8 @@ public class DeviceServiceImpl extends AbstractEntityService implements DeviceSe
case LWM2M:
deviceData.setTransportConfiguration(new Lwm2mDeviceTransportConfiguration());
break;
+ case SNMP:
+ deviceData.setTransportConfiguration(new SnmpDeviceTransportConfiguration());
}
}
return deviceData;
diff --git a/msa/transport/pom.xml b/msa/transport/pom.xml
index 6fdedfbfcf..6a9fe17fc2 100644
--- a/msa/transport/pom.xml
+++ b/msa/transport/pom.xml
@@ -38,6 +38,7 @@
mqtt
http
coap
+ snmp
diff --git a/msa/transport/snmp/docker/Dockerfile b/msa/transport/snmp/docker/Dockerfile
new file mode 100644
index 0000000000..e39624d538
--- /dev/null
+++ b/msa/transport/snmp/docker/Dockerfile
@@ -0,0 +1,33 @@
+#
+# Copyright © 2016-2021 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.
+#
+
+FROM thingsboard/openjdk8
+
+COPY start-tb-snmp-transport.sh ${pkg.name}.deb /tmp/
+
+RUN chmod a+x /tmp/*.sh \
+ && mv /tmp/start-tb-snmp-transport.sh /usr/bin
+
+RUN yes | dpkg -i /tmp/${pkg.name}.deb
+RUN rm /tmp/${pkg.name}.deb
+
+RUN systemctl --no-reload disable --now ${pkg.name}.service > /dev/null 2>&1 || :
+
+RUN chmod 555 ${pkg.installFolder}/bin/${pkg.name}.jar
+
+USER ${pkg.user}
+
+CMD ["start-tb-snmp-transport.sh"]
diff --git a/msa/transport/snmp/docker/start-tb-snmp-transport.sh b/msa/transport/snmp/docker/start-tb-snmp-transport.sh
new file mode 100644
index 0000000000..2e69cf26f9
--- /dev/null
+++ b/msa/transport/snmp/docker/start-tb-snmp-transport.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+#
+# Copyright © 2016-2021 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.
+#
+
+CONF_FOLDER="/config"
+jarfile=${pkg.installFolder}/bin/${pkg.name}.jar
+configfile=${pkg.name}.conf
+
+source "${CONF_FOLDER}/${configfile}"
+
+export LOADER_PATH=/config,${LOADER_PATH}
+
+echo "Starting '${project.name}' ..."
+
+cd ${pkg.installFolder}/bin
+
+exec java -cp ${jarfile} $JAVA_OPTS -Dloader.main=org.thingsboard.server.snmp.ThingsboardSnmpTransportApplication \
+ -Dspring.jpa.hibernate.ddl-auto=none \
+ -Dlogging.config=/config/logback.xml \
+ org.springframework.boot.loader.PropertiesLauncher
diff --git a/msa/transport/snmp/pom.xml b/msa/transport/snmp/pom.xml
new file mode 100644
index 0000000000..df5caa658c
--- /dev/null
+++ b/msa/transport/snmp/pom.xml
@@ -0,0 +1,190 @@
+
+
+ 4.0.0
+
+ org.thingsboard.msa
+ transport
+ 3.2.1-SNAPSHOT
+
+
+ org.thingsboard.msa.transport
+ snmp
+ pom
+
+ ThingsBoard SNMP Transport Microservice
+ https://thingsboard.io
+ ThingsBoard SNMP Transport Microservice
+
+
+ UTF-8
+ ${basedir}/../../..
+ tb-snmp-transport
+ tb-snmp-transport
+ /var/log/${pkg.name}
+ /usr/share/${pkg.name}
+
+
+
+
+ org.thingsboard.transport
+ snmp
+ ${project.version}
+ deb
+ deb
+ provided
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+ copy-tb-snmp-transport-deb
+ package
+
+ copy
+
+
+
+
+ org.thingsboard.transport
+ snmp
+ deb
+ deb
+ ${pkg.name}.deb
+ ${project.build.directory}
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+
+
+ copy-docker-config
+ process-resources
+
+ copy-resources
+
+
+ ${project.build.directory}
+
+
+ docker
+ true
+
+
+
+
+
+
+
+ com.spotify
+ dockerfile-maven-plugin
+
+
+ build-docker-image
+ pre-integration-test
+
+ build
+
+
+ ${dockerfile.skip}
+ ${docker.repo}/${docker.name}
+ true
+ false
+ ${project.build.directory}
+
+
+
+ tag-docker-image
+ pre-integration-test
+
+ tag
+
+
+ ${dockerfile.skip}
+ ${docker.repo}/${docker.name}
+ ${project.version}
+
+
+
+
+
+
+
+
+ push-docker-image
+
+
+ push-docker-image
+
+
+
+
+
+ com.spotify
+ dockerfile-maven-plugin
+
+
+ push-latest-docker-image
+ pre-integration-test
+
+ push
+
+
+ latest
+ ${docker.repo}/${docker.name}
+
+
+
+ push-version-docker-image
+ pre-integration-test
+
+ push
+
+
+ ${project.version}
+ ${docker.repo}/${docker.name}
+
+
+
+
+
+
+
+
+
+
+ jenkins
+ Jenkins Repository
+ https://repo.jenkins-ci.org/releases
+
+ false
+
+
+
+
diff --git a/pom.xml b/pom.xml
index 4ec162be8f..063cb027cb 100755
--- a/pom.xml
+++ b/pom.xml
@@ -109,6 +109,7 @@
1.0.2TB
3.4.0
7.54.2
+ 2.8.5
@@ -838,6 +839,11 @@
coap
${project.version}
+
+ org.thingsboard.common.transport
+ snmp
+ ${project.version}
+
org.thingsboard
dao
@@ -1408,6 +1414,11 @@
+
+ org.snmp4j
+ snmp4j
+ ${snmp4j.version}
+
diff --git a/transport/pom.xml b/transport/pom.xml
index bcafb9a184..98429f2329 100644
--- a/transport/pom.xml
+++ b/transport/pom.xml
@@ -37,6 +37,7 @@
http
mqtt
coap
+ snmp
diff --git a/transport/snmp/pom.xml b/transport/snmp/pom.xml
new file mode 100644
index 0000000000..fbcca16149
--- /dev/null
+++ b/transport/snmp/pom.xml
@@ -0,0 +1,112 @@
+
+
+ 4.0.0
+
+
+ org.thingsboard
+ 3.2.1-SNAPSHOT
+ transport
+
+
+ org.thingsboard.transport
+ snmp
+ jar
+
+ Thingsboard SNMP Transport Service
+ https://thingsboard.io
+
+
+ UTF-8
+ ${basedir}/../..
+ java
+ false
+ process-resources
+ package
+ tb-snmp-transport
+ false
+ ${project.build.directory}/windows
+ ThingsBoard SNMP Transport Service
+ org.thingsboard.server.snmp.ThingsboardSnmpTransportApplication
+
+
+
+
+ org.thingsboard.common.transport
+ snmp
+
+
+ org.thingsboard.common
+ queue
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+
+ ${pkg.name}-${project.version}
+
+
+ ${project.basedir}/src/main/resources
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.thingsboard
+ gradle-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+
+ org.apache.maven.plugins
+ maven-install-plugin
+
+
+
+
+
+ jenkins
+ Jenkins Repository
+ https://repo.jenkins-ci.org/releases
+
+ false
+
+
+
+
diff --git a/transport/snmp/src/main/java/org/thingsboard/server/snmp/ThingsboardSnmpTransportApplication.java b/transport/snmp/src/main/java/org/thingsboard/server/snmp/ThingsboardSnmpTransportApplication.java
new file mode 100644
index 0000000000..47d96db8c5
--- /dev/null
+++ b/transport/snmp/src/main/java/org/thingsboard/server/snmp/ThingsboardSnmpTransportApplication.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright © 2016-2021 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.snmp;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+import java.util.Arrays;
+
+@SpringBootConfiguration
+@EnableAsync
+@EnableScheduling
+@ComponentScan({"org.thingsboard.server.snmp", "org.thingsboard.server.common", "org.thingsboard.server.transport.snmp", "org.thingsboard.server.queue"})
+public class ThingsboardSnmpTransportApplication {
+
+ private static final String SPRING_CONFIG_NAME_KEY = "--spring.config.name";
+ private static final String DEFAULT_SPRING_CONFIG_PARAM = SPRING_CONFIG_NAME_KEY + "=" + "tb-snmp-transport";
+
+ public static void main(String[] args) {
+ SpringApplication.run(ThingsboardSnmpTransportApplication.class, updateArguments(args));
+ }
+
+ private static String[] updateArguments(String[] args) {
+ if (Arrays.stream(args).noneMatch(arg -> arg.startsWith(SPRING_CONFIG_NAME_KEY))) {
+ String[] modifiedArgs = new String[args.length + 1];
+ System.arraycopy(args, 0, modifiedArgs, 0, args.length);
+ modifiedArgs[args.length] = DEFAULT_SPRING_CONFIG_PARAM;
+ return modifiedArgs;
+ }
+ return args;
+ }
+}
diff --git a/transport/snmp/src/main/resources/device-data-config.json b/transport/snmp/src/main/resources/device-data-config.json
new file mode 100644
index 0000000000..e425ccc3a6
--- /dev/null
+++ b/transport/snmp/src/main/resources/device-data-config.json
@@ -0,0 +1,9 @@
+{
+ "deviceName": "Thermostat T1",
+ "snmpConfig": {
+ "address": "192.168.1.2",
+ "port": 161,
+ "community": "U5J=$HWj6f@7",
+ "protocolVersion": "v2c"
+ }
+}
diff --git a/transport/snmp/src/main/resources/device-profile-config.json b/transport/snmp/src/main/resources/device-profile-config.json
new file mode 100644
index 0000000000..b272913045
--- /dev/null
+++ b/transport/snmp/src/main/resources/device-profile-config.json
@@ -0,0 +1,21 @@
+{
+ "poolPeriodMs": 10000,
+ "timeoutMs": 5000,
+ "retries": 5,
+ "attributes": [
+ {
+ "key": "snmpNodeManagerEmail",
+ "type": "STRING",
+ "method": "get",
+ "oid": ".1.3.6.1.2.1.1.4.0"
+ }
+ ],
+ "telemetry": [
+ {
+ "key": "snmpNodeSysUpTime",
+ "type": "LONG",
+ "method": "get",
+ "oid": ".1.3.6.1.2.1.1.3.0"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts
index 3552821a8b..abcbb5989f 100644
--- a/ui-ngx/src/app/modules/home/components/home-components.module.ts
+++ b/ui-ngx/src/app/modules/home/components/home-components.module.ts
@@ -97,6 +97,7 @@ import { DeviceProfileDialogComponent } from '@home/components/profile/device-pr
import { DeviceProfileAutocompleteComponent } from '@home/components/profile/device-profile-autocomplete.component';
import { MqttDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/mqtt-device-profile-transport-configuration.component';
import { Lwm2mDeviceProfileTransportConfigurationComponent } from '@home/components/profile/device/lwm2m-device-profile-transport-configuration.component';
+import { SnmpDeviceProfileTransportConfigurationComponent } from './profile/device/snmp-device-profile-transport-configuration.component';
import { DeviceProfileAlarmsComponent } from '@home/components/profile/alarm/device-profile-alarms.component';
import { DeviceProfileAlarmComponent } from '@home/components/profile/alarm/device-profile-alarm.component';
import { CreateAlarmRulesComponent } from '@home/components/profile/alarm/create-alarm-rules.component';
@@ -199,6 +200,7 @@ import { CopyDeviceCredentialsComponent } from '@home/components/device/copy-dev
DefaultDeviceProfileTransportConfigurationComponent,
MqttDeviceProfileTransportConfigurationComponent,
Lwm2mDeviceProfileTransportConfigurationComponent,
+ SnmpDeviceProfileTransportConfigurationComponent,
DeviceProfileTransportConfigurationComponent,
CreateAlarmRulesComponent,
AlarmRuleComponent,
@@ -288,6 +290,7 @@ import { CopyDeviceCredentialsComponent } from '@home/components/device/copy-dev
DefaultDeviceProfileTransportConfigurationComponent,
MqttDeviceProfileTransportConfigurationComponent,
Lwm2mDeviceProfileTransportConfigurationComponent,
+ SnmpDeviceProfileTransportConfigurationComponent,
DeviceProfileTransportConfigurationComponent,
CreateAlarmRulesComponent,
AlarmRuleComponent,
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html
index 78707a9221..b877ef0015 100644
--- a/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html
+++ b/ui-ngx/src/app/modules/home/components/profile/device/device-profile-transport-configuration.component.html
@@ -35,5 +35,11 @@
formControlName="configuration">
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp-device-profile-transport-configuration.component.html b/ui-ngx/src/app/modules/home/components/profile/device/snmp-device-profile-transport-configuration.component.html
new file mode 100644
index 0000000000..f57b1ac716
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp-device-profile-transport-configuration.component.html
@@ -0,0 +1,23 @@
+
+
diff --git a/ui-ngx/src/app/modules/home/components/profile/device/snmp-device-profile-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/components/profile/device/snmp-device-profile-transport-configuration.component.ts
new file mode 100644
index 0000000000..96f7454cba
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/components/profile/device/snmp-device-profile-transport-configuration.component.ts
@@ -0,0 +1,100 @@
+///
+/// Copyright © 2016-2021 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 {Component, forwardRef, Input, OnInit} from '@angular/core';
+import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators} from '@angular/forms';
+import {Store} from '@ngrx/store';
+import {AppState} from '@app/core/core.state';
+import {coerceBooleanProperty} from '@angular/cdk/coercion';
+import {
+ DeviceProfileTransportConfiguration,
+ DeviceTransportType,
+ SnmpDeviceProfileTransportConfiguration
+} from '@shared/models/device.models';
+import {isDefinedAndNotNull} from "@core/utils";
+
+export interface OidMappingConfiguration {
+ isAttribute: boolean;
+ key: string;
+ type: string;
+ method: string;
+ oid: string;
+}
+
+@Component({
+ selector: 'tb-snmp-device-profile-transport-configuration',
+ templateUrl: './snmp-device-profile-transport-configuration.component.html',
+ styleUrls: [],
+ providers: [{
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => SnmpDeviceProfileTransportConfigurationComponent),
+ multi: true
+ }]
+})
+export class SnmpDeviceProfileTransportConfigurationComponent implements ControlValueAccessor, OnInit {
+ snmpDeviceProfileTransportConfigurationFormGroup: FormGroup;
+ private requiredValue: boolean;
+ private configuration = [];
+
+ get required(): boolean {
+ return this.requiredValue;
+ }
+
+ @Input()
+ set required(value: boolean) {
+ this.requiredValue = coerceBooleanProperty(value);
+ }
+
+ @Input()
+ disabled: boolean;
+
+ private propagateChange = (v: any) => {
+ }
+
+ constructor(private store: Store, private fb: FormBuilder) {
+ }
+
+ ngOnInit(): void {
+ this.snmpDeviceProfileTransportConfigurationFormGroup = this.fb.group({
+ configuration: [null, Validators.required]
+ });
+ this.snmpDeviceProfileTransportConfigurationFormGroup.valueChanges.subscribe(() => {
+ this.updateModel();
+ });
+ }
+
+ registerOnChange(fn: any): void {
+ this.propagateChange = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ }
+
+ writeValue(value: SnmpDeviceProfileTransportConfiguration | null): void {
+ if (isDefinedAndNotNull(value)) {
+ this.snmpDeviceProfileTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false});
+ }
+ }
+
+ private updateModel() {
+ let configuration: DeviceProfileTransportConfiguration = null;
+ if (this.snmpDeviceProfileTransportConfigurationFormGroup.valid) {
+ configuration = this.snmpDeviceProfileTransportConfigurationFormGroup.getRawValue().configuration;
+ configuration.type = DeviceTransportType.SNMP;
+ }
+ this.propagateChange(configuration);
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html
index 73cceba5bc..02799a840c 100644
--- a/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html
+++ b/ui-ngx/src/app/modules/home/pages/device/data/device-transport-configuration.component.html
@@ -35,5 +35,11 @@
formControlName="configuration">
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.html b/ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.html
new file mode 100644
index 0000000000..fc9f615db9
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.html
@@ -0,0 +1,24 @@
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.ts b/ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.ts
new file mode 100644
index 0000000000..165a2906f4
--- /dev/null
+++ b/ui-ngx/src/app/modules/home/pages/device/data/snmp-device-transport-configuration.component.ts
@@ -0,0 +1,100 @@
+///
+/// Copyright © 2016-2021 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 {Component, forwardRef, Input, OnInit} from '@angular/core';
+import {ControlValueAccessor, FormBuilder, FormGroup, NG_VALUE_ACCESSOR, Validators} from '@angular/forms';
+import {Store} from '@ngrx/store';
+import {AppState} from '@app/core/core.state';
+import {coerceBooleanProperty} from '@angular/cdk/coercion';
+import {
+ DeviceTransportConfiguration,
+ DeviceTransportType,
+ SnmpDeviceTransportConfiguration
+} from '@shared/models/device.models';
+
+@Component({
+ selector: 'tb-snmp-device-transport-configuration',
+ templateUrl: './snmp-device-transport-configuration.component.html',
+ styleUrls: [],
+ providers: [{
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => SnmpDeviceTransportConfigurationComponent),
+ multi: true
+ }]
+})
+export class SnmpDeviceTransportConfigurationComponent implements ControlValueAccessor, OnInit {
+
+ snmpDeviceTransportConfigurationFormGroup: FormGroup;
+
+ private requiredValue: boolean;
+
+ get required(): boolean {
+ return this.requiredValue;
+ }
+
+ @Input()
+ set required(value: boolean) {
+ this.requiredValue = coerceBooleanProperty(value);
+ }
+
+ @Input()
+ disabled: boolean;
+
+ private propagateChange = (v: any) => {
+ };
+
+ constructor(private store: Store,
+ private fb: FormBuilder) {
+ }
+
+ registerOnChange(fn: any): void {
+ this.propagateChange = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ }
+
+ ngOnInit() {
+ this.snmpDeviceTransportConfigurationFormGroup = this.fb.group({
+ configuration: [null, Validators.required]
+ });
+ this.snmpDeviceTransportConfigurationFormGroup.valueChanges.subscribe(() => {
+ this.updateModel();
+ });
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ if (this.disabled) {
+ this.snmpDeviceTransportConfigurationFormGroup.disable({emitEvent: false});
+ } else {
+ this.snmpDeviceTransportConfigurationFormGroup.enable({emitEvent: false});
+ }
+ }
+
+ writeValue(value: SnmpDeviceTransportConfiguration | null): void {
+ this.snmpDeviceTransportConfigurationFormGroup.patchValue({configuration: value}, {emitEvent: false});
+ }
+
+ private updateModel() {
+ let configuration: DeviceTransportConfiguration = null;
+ if (this.snmpDeviceTransportConfigurationFormGroup.valid) {
+ configuration = this.snmpDeviceTransportConfigurationFormGroup.getRawValue().configuration;
+ configuration.type = DeviceTransportType.SNMP;
+ }
+ this.propagateChange(configuration);
+ }
+}
diff --git a/ui-ngx/src/app/modules/home/pages/device/device.module.ts b/ui-ngx/src/app/modules/home/pages/device/device.module.ts
index 7ba9a5c0fe..9347fab505 100644
--- a/ui-ngx/src/app/modules/home/pages/device/device.module.ts
+++ b/ui-ngx/src/app/modules/home/pages/device/device.module.ts
@@ -31,6 +31,7 @@ import { DefaultDeviceTransportConfigurationComponent } from './data/default-dev
import { DeviceTransportConfigurationComponent } from './data/device-transport-configuration.component';
import { MqttDeviceTransportConfigurationComponent } from './data/mqtt-device-transport-configuration.component';
import { Lwm2mDeviceTransportConfigurationComponent } from './data/lwm2m-device-transport-configuration.component';
+import { SnmpDeviceTransportConfigurationComponent } from './data/snmp-device-transport-configuration.component';
@NgModule({
declarations: [
@@ -39,6 +40,7 @@ import { Lwm2mDeviceTransportConfigurationComponent } from './data/lwm2m-device-
DefaultDeviceTransportConfigurationComponent,
MqttDeviceTransportConfigurationComponent,
Lwm2mDeviceTransportConfigurationComponent,
+ SnmpDeviceTransportConfigurationComponent,
DeviceTransportConfigurationComponent,
DeviceDataComponent,
DeviceComponent,
diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts
index dcf3f34093..fd293aae64 100644
--- a/ui-ngx/src/app/shared/models/device.models.ts
+++ b/ui-ngx/src/app/shared/models/device.models.ts
@@ -29,13 +29,15 @@ import * as _moment from 'moment';
import { AbstractControl, ValidationErrors } from '@angular/forms';
export enum DeviceProfileType {
- DEFAULT = 'DEFAULT'
+ DEFAULT = 'DEFAULT',
+ SNMP = 'SNMP'
}
export enum DeviceTransportType {
DEFAULT = 'DEFAULT',
MQTT = 'MQTT',
// LWM2M = 'LWM2M'
+ SNMP = 'SNMP'
}
export enum MqttTransportPayloadType {
@@ -68,6 +70,13 @@ export const deviceProfileTypeConfigurationInfoMap = new Map(
[DeviceTransportType.DEFAULT, 'device-profile.transport-type-default-hint'],
[DeviceTransportType.MQTT, 'device-profile.transport-type-mqtt-hint'],
// [DeviceTransportType.LWM2M, 'device-profile.transport-type-lwm2m-hint']
+ [DeviceTransportType.SNMP, 'device-profile.transport-type-snmp-hint'],
]
);
@@ -128,6 +139,13 @@ export const deviceTransportTypeConfigurationInfoMap = new Map[+] and multi-level [#] wildcards supported.",
"telemetry-topic-filter": "Telemetry topic filter",