<dependency>
<groupId>org.zeromq</groupId>
<artifactId>jeromq</artifactId>
<version>0.5.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.zeromq</groupId>
<artifactId>jnacl</artifactId>
<version>0.1.0</version>
<scope>provided</scope>
</dependency>
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Copyright (c) 2020 SAP SE or an affiliate company. All rights reserved.
# The sample is not intended for production use. Provided "as is".
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
'''
This program implements a KNN algorithm to recognize the color of in input sample,
in terms of RGB components. The model is trained within a dataset of red-scale,
so it able to recognize what colours are red, and provide as output the euclidean distances
of the top 3 nearest neighbors.
'''
import json
import os.path
# Third-party libraries
import sklearn.neighbors
import zmq
# This folder must contain the model data
DATA_DIR = '.'
TRAINING_DATASET_FILENAME = os.path.join(DATA_DIR, 'red-colors.json')
def predict(array_data, labels, knn_classifier):
'''Predict the color name by its RGB components
Args:
array_data (list): a list of 3 integer elements: R, G, B. Each element must be in the 0..255 range.
labels (list): a list of labels that come from the dataset.
knn_classifier (object): The classifier used to make the prediction.
Returns:
A dictionary with neighbor names as keys and distances from the RGB point to each neighbor as values
'''
predicted = knn_classifier.predict(array_data)
print(predicted)
distances,indexes = knn_classifier.kneighbors(array_data)
print(distances)
print([labels[i] for i in indexes[0]])
print(indexes)
data = {}
data['label'] = predicted[0]
data['neighbor(1)'] = distances[0][0]
data['neighbor(2)'] = distances[0][1]
data['neighbor(3)'] = distances[0][2]
print(data)
return data
def zmq_start_server(port):
'''
Strart ZMQ messagebus at the specified port (binded to any available ip)
Args:
port (string): A string with the port used to bind the socket at the server side.
'''
context = zmq.Context()
socket = context.socket(zmq.REP)
socket.bind("tcp://*:" + port)
return socket
def main():
'''
The entry point of the predictive algorithm
'''
f = open(TRAINING_DATASET_FILENAME)
dataset = json.load(f)
f.close()
points = [el['data'] for el in dataset]
labels = [el['label'] for el in dataset]
knn_classifier = sklearn.neighbors.KNeighborsClassifier(3)
knn_classifier.fit(points, labels)
# Create the server
socket = zmq_start_server("5555")
# Process messages
while True:
# Wait for next request from client
message = socket.recv()
print("Received request: %s" % message)
if(message == b'hello'):
socket.send(b'hello')
continue
# Parse Json
try:
objSample = json.loads(message.decode('utf-8'))
rgbSamples = []
print(objSample)
rgbSamples.append([objSample['measures']['R'],objSample['measures']['G'],objSample['measures']['B']])
# Do prediction
prediction = predict(rgbSamples, labels, knn_classifier)
# Create json
jsonprediction = json.dumps(prediction)
print(jsonprediction)
# Send reply back to client
socket.send((jsonprediction.encode('utf-8')))
except Exception as e:
print(e)
if __name__ == '__main__':
# Run the main process
main()
package com.sap.iotservices.gateway.interceptor;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicReference;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zeromq.SocketType;
import org.zeromq.ZContext;
import org.zeromq.ZMQ;
import org.zeromq.ZMQ.Socket;
import com.sap.iotservices.commandmanagement.interfaces.CommandManager;
import com.sap.iotservices.gateway.datamanager.IDataManager;
import com.sap.iotservices.gateway.devicemanager.IDeviceManagerAsync;
import com.sap.iotservices.gateway.eventsender.EventSender;
import com.sap.iotservices.gateway.interceptor.custom.CustomParameters;
import com.sap.iotservices.gateway.properties.IGatewayProperties;
import com.sap.iotservices.gateway.topology.IGatewayTopology;
import com.sap.iotservices.hooks.gateway.IGatewayInterceptor;
import com.sap.iotservices.hooks.gateway.IGatewayInterceptorService;
import com.sap.iotservices.utils.DSUtils;
import com.sap.iotservices.xmlparse.IJMSFrameParser;
/**
* This class starts the actual implementation for the Interceptor
*
*/
@Component(immediate = true, service = {})
public class InterceptorActivator {
private static final Logger log = LoggerFactory.getLogger(InterceptorActivator.class);
private static Object lock = new Object();
private static Object zmqlock = new Object();
private boolean registered = false;
private static boolean initialized = false;
private IGatewayInterceptor interceptor;
private static Socket socket;
private static String addressAndport = CustomParameters.ZMQ_CONNECTION;
private static Process pair;
private static String pythonPath = CustomParameters.PYTHON_PATH;
private static String scriptName = CustomParameters.PYTHON_SCRIPT_PATH;
private static volatile boolean zmq = false;
public static boolean isZmq() {
synchronized (zmqlock) {
return zmq;
}
}
public static void setZmq(boolean zmq) {
InterceptorActivator.zmq = zmq;
}
@Activate
public void start() {
log.info("Starting Gateway Interceptor...");
this.interceptor = new InterceptorImpl();
new Thread(() -> {
IGatewayInterceptorService interceptorMng = getInterceptorManager();
synchronized (lock) {
if ((interceptorMng != null) && (!registered)) {
log.info("Registering implementation of the test interceptor");
registered = interceptorMng.addInterceptor(interceptor);
}
}
}).start();
}
@Deactivate
public void stop() {
log.info("Stopping Interceptor...");
IGatewayInterceptorService interceptorMng = getInterceptorManager();
synchronized (lock) {
if ((interceptorMng != null) && (registered)) {
log.info("Unregistering implementation of the test interceptor");
interceptorMng.removeInterceptor(this.interceptor);
registered = false;
}
}
closeSocket();
}
static void closeSocket(){
socket.disconnect("tcp://" + addressAndport);
socket.close();
if (pair != null){
pair.destroyForcibly();
}
}
static String receive(){
byte[] reply = socket.recv(0);
System.out.println("Received: [" + new String(reply, ZMQ.CHARSET) + "]");
return new String(reply, ZMQ.CHARSET);
}
static void send(String measurement) {
socket.send(measurement.getBytes(ZMQ.CHARSET), 0);
}
static void initSocket() {
synchronized (zmqlock) {
socket = new ZContext().createSocket(SocketType.REQ);
socket.setReceiveTimeOut(10000);
//update from configreader
socket.connect("tcp://" + addressAndport);
runPair(pythonPath, scriptName);
zmq = true;
}
}
static boolean runPair(String process, String args){
try{
send("hello");
String resp = receive();
if (resp.isEmpty() || !resp.contentEquals("hello")){
return false;
}
log.info("Server response: {}", resp);
}
catch (Exception e){
try {
pair = new ProcessBuilder(process, args).start();
send("hello");
String msg = receive();
if (msg.isEmpty() || !msg.contentEquals("hello")){
return false;
}
log.info("Server response: {}", msg);
} catch (IOException ex) {
log.error(ex.getMessage(), ex);
return false;
}
}
return true;
}
public static boolean isInitialized() {
return initialized;
}
public static void setInitialized(boolean initialized) {
InterceptorActivator.initialized = initialized;
}
//////////////////////////////////////////////////////////////////////////////////////////
// Available Declarative Services
// To define and consume services via XML metadata, see OSGI-INF/InterceptorActivator.xml
/**
* Interceptor Manager
*/
private static AtomicReference<IGatewayInterceptorService> interceptorMngr = new AtomicReference<>();
@Reference(cardinality = ReferenceCardinality.AT_LEAST_ONE, policy = ReferencePolicy.DYNAMIC)
void setInterceptorManager(IGatewayInterceptorService arg) {
DSUtils.setRef(log, interceptorMngr, arg, IGatewayInterceptorService.class, this.getClass());
}
void unsetInterceptorManager(IGatewayInterceptorService arg) {
DSUtils.removeRef(log, interceptorMngr, arg, IGatewayInterceptorService.class, this.getClass());
}
public static IGatewayInterceptorService getInterceptorManager() {
return DSUtils.get(log, interceptorMngr, DSUtils.WAIT_FOR_VALID_REFERENCE);
}
/**
* Frame Parser
*/
private static AtomicReference<IJMSFrameParser> frameParser = new AtomicReference<>();
@Reference(cardinality = ReferenceCardinality.AT_LEAST_ONE, policy = ReferencePolicy.DYNAMIC)
void setFrameParser(IJMSFrameParser arg) {
DSUtils.setRef(log, frameParser, arg, IJMSFrameParser.class, this.getClass());
}
void unsetFrameParser(IJMSFrameParser arg) {
DSUtils.removeRef(log, frameParser, arg, IJMSFrameParser.class, this.getClass());
}
public static IJMSFrameParser getFrameParser() {
return DSUtils.get(log, frameParser, DSUtils.WAIT_FOR_VALID_REFERENCE);
}
/**
* Device manager service
*/
private static AtomicReference<IDeviceManagerAsync> deviceManager = new AtomicReference<>(); // NOSONAR
@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
void setDeviceManagerService(IDeviceManagerAsync arg) {
DSUtils.setRef(log, deviceManager, arg, IDeviceManagerAsync.class, this.getClass());
}
void unsetDeviceManagerService(IDeviceManagerAsync arg) {
DSUtils.removeRef(log, deviceManager, arg, IDeviceManagerAsync.class, this.getClass());
}
public static IDeviceManagerAsync getDeviceManagerService() {
return DSUtils.get(log, deviceManager, !DSUtils.WAIT_FOR_VALID_REFERENCE);
}
/**
* Gateway manager service
*/
private static AtomicReference<IGatewayProperties> gatewayManagerService = new AtomicReference<>(); // NOSONAR
@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
void setGatewayManagerService(IGatewayProperties arg) {
DSUtils.setRef(log, gatewayManagerService, arg, IGatewayProperties.class, this.getClass());
}
void unsetGatewayManagerService(IGatewayProperties arg) {
DSUtils.removeRef(log, gatewayManagerService, arg, IGatewayProperties.class, this.getClass());
}
public static IGatewayProperties getGatewayManagerService() {
return DSUtils.get(log, gatewayManagerService, !DSUtils.WAIT_FOR_VALID_REFERENCE);
}
/**
* Event Sender
*/
private static AtomicReference<EventSender> eventSender = new AtomicReference<>(); // NOSONAR
@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
void setEventSender(EventSender arg) {
DSUtils.setRef(log, eventSender, arg, EventSender.class, this.getClass());
}
void unsetEventSender(EventSender arg) {
DSUtils.removeRef(log, eventSender, arg, EventSender.class, this.getClass());
}
public static EventSender getEventSender() {
return DSUtils.get(log, eventSender, !DSUtils.WAIT_FOR_VALID_REFERENCE);
}
/**
* Data Manager
*/
private static AtomicReference<IDataManager> dataManager = new AtomicReference<>(); // NOSONAR
@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
void setDataManagerService(IDataManager arg) {
DSUtils.setRef(log, dataManager, arg, IDataManager.class, this.getClass());
}
void unsetDataManagerService(IDataManager arg) {
DSUtils.removeRef(log, dataManager, arg, IDataManager.class, this.getClass());
}
public static IDataManager getDataManagerService() {
return DSUtils.get(log, dataManager, !DSUtils.WAIT_FOR_VALID_REFERENCE);
}
/**
* Topology Service
*/
private static AtomicReference<IGatewayTopology> topologyService = new AtomicReference<>(); // NOSONAR
@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
void setTopologyManagerService(IGatewayTopology arg) {
DSUtils.setRef(log, topologyService, arg, IGatewayTopology.class, this.getClass());
}
void unsetTopologyManagerService(IGatewayTopology arg) {
DSUtils.removeRef(log, topologyService, arg, IGatewayTopology.class, this.getClass());
}
public static IGatewayTopology getTopologyService() {
return DSUtils.get(log, topologyService, !DSUtils.WAIT_FOR_VALID_REFERENCE);
}
/**
* Command Manager
*/
private static AtomicReference<CommandManager> commandManager = new AtomicReference<>(); // NOSONAR
@Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
void setCommandManager(CommandManager arg) {
DSUtils.setRef(log, commandManager, arg, CommandManager.class, this.getClass());
}
void unsetCommandManager(CommandManager arg) {
DSUtils.removeRef(log, commandManager, arg, CommandManager.class, this.getClass());
}
public static CommandManager getCommandManager() {
return DSUtils.get(log, commandManager, !DSUtils.WAIT_FOR_VALID_REFERENCE);
}
}
package com.sap.iotservices.gateway.interceptor;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sap.iotservices.api.models.gateway.Device;
import com.sap.iotservices.api.models.gateway.Sensor;
import com.sap.iotservices.data.WSNdataType;
import com.sap.iotservices.gateway.datamanager.IDataManager;
import com.sap.iotservices.gateway.interceptor.custom.CustomParameters;
import com.sap.iotservices.gateway.topology.IGatewayTopology;
import com.sap.iotservices.hooks.gateway.IGatewayInterceptor;
import com.sap.iotservices.hooks.gateway.IoTServicesPointcut;
import com.sap.iotservices.network.node.data.Value;
import com.sap.iotservices.network.node.data.WSNParsedMeasure;
public class InterceptorImpl
implements IGatewayInterceptor {
private static final Logger log = LoggerFactory.getLogger(InterceptorImpl.class);
List<Double> intervalValues = new ArrayList<>();
private static ObjectMapper mapper = new ObjectMapper();
@Override
public void processObject(String pointcutName, Object... args)
throws Exception {
// check the messagebus is running
if (!InterceptorActivator.isZmq()) {
InterceptorActivator.initSocket();
}
try {
IoTServicesPointcut pointcut = IoTServicesPointcut.valueOf(pointcutName);
switch (pointcut) {
case GATEWAY_PARSED_DATA_DISPATCH:
// triggered upon dispatch of parsed sensor data
manageIncomingMeasures(args);
break;
default:
break;
}
} catch (Exception e) {
log.error(e.getMessage());
}
}
@Override
public List<String> getBranchPoints() {
List<String> list = new ArrayList<>();
list.add(IoTServicesPointcut.GATEWAY_PARSED_DATA_DISPATCH.name());
return list;
}
@Override
public void onError(Object event, String pointcut, Exception e) {
log.info("OnError triggered; pointcut is {}", pointcut);
}
// Go through measure list
private void manageIncomingMeasures(Object... args) {
@SuppressWarnings("unchecked")
List<WSNParsedMeasure> measures = (List<WSNParsedMeasure>) args[0];
// list of measures that are going to be dropped
List<WSNParsedMeasure> toBeRemoved = new ArrayList<>();
if (measures != null) {
for (WSNParsedMeasure wsnParsedMeasure : measures) {
List<Value<?>> valueList = wsnParsedMeasure.getValues();
log.info("Measurements received for CapabilityID: {}", wsnParsedMeasure.getCapabilityAlternateId());
manageIncomingValues(wsnParsedMeasure, valueList, toBeRemoved);
}
for (WSNParsedMeasure wsnParsedMeasure : toBeRemoved) {
measures.remove(wsnParsedMeasure);
log.info("Measure for Capability {}, Device {} and Sensor {} will be dropped",
wsnParsedMeasure.getCapabilityAlternateId(), wsnParsedMeasure.getDeviceAlternateId(),
wsnParsedMeasure.getSensorAlternateId());
}
}
}
// Go through values for measure
@SuppressWarnings("unchecked")
private void manageIncomingValues(WSNParsedMeasure wsnParsedMeasure, List<Value<?>> valueList,
List<WSNParsedMeasure> toBeRemoved) {
if(wsnParsedMeasure.getCapabilityAlternateId()
.equals(CustomParameters.TARGET_CAPABILITY_ALTERNATEID_COLOR) &&
valueList.size() == CustomParameters.Colors.values().length) {
// create a map to be serialized for the predictive module
Map<String,Object> map = new HashMap<String, Object>();
map.put(valueList.get(0).getMeasureName(), valueList.get(0).getInnerMeasure());
map.put(valueList.get(1).getMeasureName(), valueList.get(1).getInnerMeasure());
map.put(valueList.get(2).getMeasureName(), valueList.get(2).getInnerMeasure());
String measurement;
try {
// serialize the map
measurement = mapper.writeValueAsString(map);
log.info("json = {}", measurement);
} catch (JsonProcessingException e) {
log.error(e.getMessage(), e);
return;
}
// Send measurement
InterceptorActivator.send(measurement);
String reply = InterceptorActivator.receive();
Map<String, ?> prediction;
try {
//read the predicted value into a map
prediction = mapper.readValue(reply, Map.class);
} catch (IOException e) {
log.error(e.getMessage(), e);
return;
}
// send the prediction in the data ingestion pipeline
sendPrediction(wsnParsedMeasure, prediction);
}
}
public void sendPrediction(WSNParsedMeasure wsnParsedMeasure, Map<String, ?> rgbPrediction) {
// send Measure
IGatewayTopology topologyService = InterceptorActivator.getTopologyService();
String deviceAlternateId = wsnParsedMeasure.getDeviceAlternateId();
String sensorAlternateId = wsnParsedMeasure.getSensorAlternateId();
int sensorTypeAlternateId = wsnParsedMeasure.getSensorTypeAlternateId();
String capabilityAlternateId = CustomParameters.TARGET_CAPABILITY_ALTERNATEID_PREDICTION;
if (topologyService != null) {
// Device should be present
Device device = topologyService.getDevice(deviceAlternateId);
if (device == null) {
log.warn("No device: {}", deviceAlternateId);
return;
}
// Sensor should be present
Sensor sensor = topologyService.getSensor(deviceAlternateId, sensorAlternateId);
if (sensor == null) {
log.warn("No sensor: {}", sensorAlternateId);
return;
}
List<WSNParsedMeasure> measureList = new ArrayList<>();
// Add predicted label
Value<String> label = new Value<>();
label.setDataType(WSNdataType.ASCIIStr);
label.setInnerMeasure((String) rgbPrediction.get(CustomParameters.Prediction.label.toString()));
label.setMeasureName(CustomParameters.Prediction.label.name());
label.setMeasureUnit("");
// Add first neighbor
Value<Float> neighbor1 = new Value<>();
neighbor1.setDataType(WSNdataType.Single_prec);
neighbor1.setInnerMeasure(((Double)rgbPrediction.get(CustomParameters.Prediction.neighbor1.toString())).floatValue());
neighbor1.setMeasureName(CustomParameters.Prediction.neighbor1.name());
neighbor1.setMeasureUnit("");
// Add second neighbor
Value<Float> neighbor2 = new Value<>();
neighbor2.setDataType(WSNdataType.Single_prec);
neighbor2.setInnerMeasure(((Double)rgbPrediction.get(CustomParameters.Prediction.neighbor2.toString())).floatValue());
neighbor2.setMeasureName(CustomParameters.Prediction.neighbor2.name());
neighbor2.setMeasureUnit("");
// Add three neighbor
Value<Float> neighbor3 = new Value<>();
neighbor3.setDataType(WSNdataType.Single_prec);
neighbor3.setInnerMeasure(((Double)rgbPrediction.get(CustomParameters.Prediction.neighbor3.toString())).floatValue());
neighbor3.setMeasureName(CustomParameters.Prediction.neighbor3.name());
neighbor3.setMeasureUnit("");
List<Value<?>> values = new ArrayList<>();
values.add(label);
values.add(neighbor1);
values.add(neighbor2);
values.add(neighbor3);
// create the measure for the predicted value
WSNParsedMeasure prediction = new WSNParsedMeasure(deviceAlternateId, sensorAlternateId,
sensorTypeAlternateId, capabilityAlternateId, values, new Date());
measureList.add(prediction);
IDataManager dataManager = InterceptorActivator.getDataManagerService();
dataManager.sendMeasures(device, measureList);
}
}
}
package com.sap.iotservices.gateway.interceptor.custom;
/**
* It is possible to customize all the values present in this class to customize this interceptor example
*
*/
public class CustomParameters {
public static enum Colors{
R,
G,
B
}
public static enum Prediction{
label,
neighbor1,
neighbor2,
neighbor3;
@Override
public String toString() {
//map propety name with the output of the prediction. The name() is equals to the propertyname and the python has the same output of the toString()
switch(this) {
case neighbor1: return "neighbor(1)";
case neighbor2: return "neighbor(2)";
case neighbor3: return "neighbor(3)";
default:
break;
}
return super.toString();
}
}
// ****** Constants related to INTERCEPTORIMPL
// - Target Device
public static final String ZMQ_CONNECTION = "127.0.0.1:5555";
public static final String PYTHON_PATH = "python";
public static final String PYTHON_SCRIPT_PATH = "./knn.py";
// - Target Sensor
// public static final String TARGET_SENSOR_ALTERNATEID = "1";
// - Target Capabilities
public static final String TARGET_CAPABILITY_ALTERNATEID_COLOR = "color";
public static final String TARGET_CAPABILITY_ALTERNATEID_PREDICTION = "color prediction";
}
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: predictive model
Bundle-SymbolicName: predictive model
Bundle-Version: 4.0.0.SNAPSHOT
Bundle-Vendor: XYZ Company
Export-Package: com.sap.iotservices.gateway.interceptor;uses:="com.sap.iotservices.hooks.gateway,com.sap.iotservices.xmlparse"
Require-Bundle: org.eclipse.osgi.services,
com.sap.iotservices.common.basic,
com.sap.iotservices.common.interface,
com.sap.iotservices.gateway.topology-interface,
com.sap.iotservices.gateway.topology-service,
com.sap.iotservices.gateway.device-manager-interface,
com.sap.iotservices.gateway.properties-interface,
com.sap.iotservices.gateway.eventsender-interface,
com.sap.iotservices.gateway.data-manager-interface,
org.zeromq.jeromq,
com.fasterxml.jackson.core.jackson-databind,
com.fasterxml.jackson.core.jackson-core
Bnd-LastModified: 1481173207479
Require-Capability: osgi.service;filter:="(objectClass=com.sap.iotservices.hooks.gateway.IGatewayInterceptorService)";effective:=active;cardinality:=multiple,osgi.service;filter:="(objectClass=com.sap.iotservices.xmlparse.IJMSFrameParser)";effective:=active;cardinality:=multiple,osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=1.8))"
Build-Jdk: 1.8.0_92
Service-Component: OSGI-INF/com.sap.iotservices.gateway.interceptor.InterceptorActivator.xml
Bundle-ActivationPolicy: lazy
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Import-Package: com.sap.iotservices.commandmanagement.interfaces,
com.sap.iotservices.commandmanagement.interfaces.bean,
com.sap.iotservices.gateway.response.manager.beans,
org.slf4j
Bundle-ClassPath: third-parties-libs/,
.
https://<HOSTNAME>:443/<INSTANCE ID>/iot/core/api/v1/tenant/<TENANT ID>
[31/10/19 14.30] Deployment successful; Filename=predictive model.jar
[31/10/19 15.19] Build was successful.
[31/10/19 15.19]
Sending 'POST' request to URL : https://HOSTNAME:443/INSTANCE ID/iot/core/api/v1/tenant/TENANT ID/gateways/GATEWAY ID/bundles
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
User | Count |
---|---|
26 | |
16 | |
15 | |
13 | |
12 | |
9 | |
7 | |
6 | |
5 | |
5 |