Adds 3rd party application scenario examples
- Common base (Dynamic AAS & AAS Wrapper)
- Streamsheets
- Grafana
- NodeRed
Change-Id: Ie31d459bb30df0afecf65ae0b0ffe052130c1d8f
Signed-off-by: Daniel Espen <Daniel.Espen@iese.fraunhofer.de>
diff --git a/examples/.mvn/wrapper/MavenWrapperDownloader.java b/examples/.mvn/wrapper/MavenWrapperDownloader.java
new file mode 100644
index 0000000..e76d1f3
--- /dev/null
+++ b/examples/.mvn/wrapper/MavenWrapperDownloader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2007-present the original author or 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
+ *
+ * https://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 java.net.*;
+import java.io.*;
+import java.nio.channels.*;
+import java.util.Properties;
+
+public class MavenWrapperDownloader {
+
+ private static final String WRAPPER_VERSION = "0.5.6";
+ /**
+ * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
+ */
+ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
+
+ /**
+ * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
+ * use instead of the default one.
+ */
+ private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
+ ".mvn/wrapper/maven-wrapper.properties";
+
+ /**
+ * Path where the maven-wrapper.jar will be saved to.
+ */
+ private static final String MAVEN_WRAPPER_JAR_PATH =
+ ".mvn/wrapper/maven-wrapper.jar";
+
+ /**
+ * Name of the property which should be used to override the default download url for the wrapper.
+ */
+ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+
+ public static void main(String args[]) {
+ System.out.println("- Downloader started");
+ File baseDirectory = new File(args[0]);
+ System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
+
+ // If the maven-wrapper.properties exists, read it and check if it contains a custom
+ // wrapperUrl parameter.
+ File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
+ String url = DEFAULT_DOWNLOAD_URL;
+ if(mavenWrapperPropertyFile.exists()) {
+ FileInputStream mavenWrapperPropertyFileInputStream = null;
+ try {
+ mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
+ Properties mavenWrapperProperties = new Properties();
+ mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
+ url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
+ } catch (IOException e) {
+ System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
+ } finally {
+ try {
+ if(mavenWrapperPropertyFileInputStream != null) {
+ mavenWrapperPropertyFileInputStream.close();
+ }
+ } catch (IOException e) {
+ // Ignore ...
+ }
+ }
+ }
+ System.out.println("- Downloading from: " + url);
+
+ File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
+ if(!outputFile.getParentFile().exists()) {
+ if(!outputFile.getParentFile().mkdirs()) {
+ System.out.println(
+ "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
+ }
+ }
+ System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
+ try {
+ downloadFileFromURL(url, outputFile);
+ System.out.println("Done");
+ System.exit(0);
+ } catch (Throwable e) {
+ System.out.println("- Error downloading");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+ if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
+ String username = System.getenv("MVNW_USERNAME");
+ char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
+ Authenticator.setDefault(new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(username, password);
+ }
+ });
+ }
+ URL website = new URL(urlString);
+ ReadableByteChannel rbc;
+ rbc = Channels.newChannel(website.openStream());
+ FileOutputStream fos = new FileOutputStream(destination);
+ fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+ fos.close();
+ rbc.close();
+ }
+
+}
diff --git a/examples/.mvn/wrapper/maven-wrapper.jar b/examples/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 0000000..2cc7d4a
--- /dev/null
+++ b/examples/.mvn/wrapper/maven-wrapper.jar
Binary files differ
diff --git a/examples/.mvn/wrapper/maven-wrapper.properties b/examples/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..642d572
--- /dev/null
+++ b/examples/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,2 @@
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
diff --git a/examples/basyx.aasWrapper/Dockerfile b/examples/basyx.aasWrapper/Dockerfile
new file mode 100644
index 0000000..5c98c98
--- /dev/null
+++ b/examples/basyx.aasWrapper/Dockerfile
@@ -0,0 +1,23 @@
+# Add java runtime environment for execution
+FROM java:8-jdk-alpine
+
+# Copy built jar to image using the jar name specified in the pom.xml (JAR_FILE)
+ARG JAR_FILE
+COPY target/${JAR_FILE} /usr/share/basyxExecutable.jar
+COPY target/lib /usr/share/lib
+COPY src/main/resources/context.properties /usr/share/config/context.properties
+COPY src/main/resources/wrapper.properties /usr/share/config/wrapper.properties
+
+# Expose the appropriate port. In case of Tomcat, this is 8080.
+ARG PORT
+EXPOSE ${PORT}
+
+# Set the path for the context configuration file
+ARG CONTEXT_CONFIG_KEY
+ENV ${CONTEXT_CONFIG_KEY} "/usr/share/config/context.properties"
+
+# Set the path for the context configuration file
+ENV BASYX_WRAPPER "/usr/share/config/wrapper.properties"
+
+# Start the jar
+CMD java -jar "/usr/share/basyxExecutable.jar"
\ No newline at end of file
diff --git a/examples/basyx.aasWrapper/build.bat b/examples/basyx.aasWrapper/build.bat
new file mode 100644
index 0000000..0977f37
--- /dev/null
+++ b/examples/basyx.aasWrapper/build.bat
@@ -0,0 +1 @@
+.././mvnw clean install -U -Pdocker
\ No newline at end of file
diff --git a/examples/basyx.aasWrapper/build.sh b/examples/basyx.aasWrapper/build.sh
new file mode 100644
index 0000000..d223f82
--- /dev/null
+++ b/examples/basyx.aasWrapper/build.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+.././mvnw clean install -U -Pdocker -DskipTests
\ No newline at end of file
diff --git a/examples/basyx.aasWrapper/docker-compose.yml b/examples/basyx.aasWrapper/docker-compose.yml
new file mode 100644
index 0000000..4240df8
--- /dev/null
+++ b/examples/basyx.aasWrapper/docker-compose.yml
@@ -0,0 +1,23 @@
+version: '3'
+services:
+
+ registry:
+ image: basyx/registry:0.1.0-SNAPSHOT
+ container_name: dashboard-registry
+ ports:
+ - 4000:4000
+
+ dashboard-aas:
+ image: basyx/dashboard-aas:0.1.0-SNAPSHOT
+ container_name: dashboard-aas
+ environment:
+ - BaSyxDashboardSubmodel_Min=15
+# - BaSyxDashboardSubmodel_Max=30
+ ports:
+ - 6400:6400
+
+ aas-wrapper:
+ image: basyx/aas-wrapper:0.1.0-SNAPSHOT
+ container_name: aas-wrapper
+ ports:
+ - 6500:6500
\ No newline at end of file
diff --git a/examples/basyx.aasWrapper/pom.xml b/examples/basyx.aasWrapper/pom.xml
new file mode 100644
index 0000000..fdec27f
--- /dev/null
+++ b/examples/basyx.aasWrapper/pom.xml
@@ -0,0 +1,66 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.eclipse.basyx</groupId>
+ <artifactId>basyx.components.docker</artifactId>
+ <version>0.1.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>basyx.components.AASWrapper</artifactId>
+ <name>BaSyx AAS Wrapper</name>
+
+ <properties>
+ <!--
+ basyx.components.executable is the executable class with the definition of the public void main(String[]).
+ It is needed when building the jar in the maven-jar-plugin (see basyx.components.docker/pom.xml)
+ -->
+ <basyx.components.executable>org.eclipse.basyx.wrapper.AASWrapperExecutable</basyx.components.executable>
+ </properties>
+
+ <packaging>jar</packaging>
+
+ <!-- Define additional plugins that are not included by default -->
+ <!-- Plugin configuration is done in parent project(s) -->
+ <build>
+ <plugins>
+ <!-- Attach sources to jar file -->
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-source-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+
+ <profiles>
+ <profile>
+ <!--
+ "Docker" profile - do not build & install docker images by default
+ Run "mvn install -Pdocker" in order to include docker
+ -->
+ <id>docker</id>
+ <build>
+ <plugins>
+ <!-- Read maven properties from file -->
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>properties-maven-plugin</artifactId>
+ </plugin>
+
+ <!-- Copy the dependencies necessary to run the jar -->
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-dependency-plugin</artifactId>
+ </plugin>
+
+ <!-- Build the docker image -->
+ <plugin>
+ <groupId>com.spotify</groupId>
+ <artifactId>dockerfile-maven-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+</project>
\ No newline at end of file
diff --git a/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/AASWrapperExecutable.java b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/AASWrapperExecutable.java
new file mode 100644
index 0000000..0d4a049
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/AASWrapperExecutable.java
@@ -0,0 +1,200 @@
+package org.eclipse.basyx.wrapper;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.eclipse.basyx.aas.registration.api.IAASRegistryService;
+import org.eclipse.basyx.aas.registration.proxy.AASRegistryProxy;
+import org.eclipse.basyx.components.configuration.BaSyxConfiguration;
+import org.eclipse.basyx.components.configuration.BaSyxContextConfiguration;
+import org.eclipse.basyx.vab.protocol.http.server.AASHTTPServer;
+import org.eclipse.basyx.vab.protocol.http.server.BaSyxContext;
+import org.eclipse.basyx.wrapper.provider.GenericHTTPInterface;
+import org.eclipse.basyx.wrapper.provider.GenericWrapperProvider;
+import org.eclipse.basyx.wrapper.provider.IWrapperProvider;
+import org.eclipse.basyx.wrapper.provider.aas.AASWrapperProvider;
+import org.eclipse.basyx.wrapper.provider.grafana.GrafanaProvider;
+import org.eclipse.basyx.wrapper.provider.streamsheets.StreamsheetsProvider;
+import org.eclipse.basyx.wrapper.receiver.IPropertyWrapperService;
+import org.eclipse.basyx.wrapper.receiver.SeparateAASService;
+import org.eclipse.basyx.wrapper.receiver.configuration.AASPropertyConfiguration;
+import org.eclipse.basyx.wrapper.receiver.configuration.PropertyConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Runs the AAS wrapper for the given configuration. The configuration is loaded
+ * from the wrapper.properties by default, but can also be given by an external file.
+ * For this, the path has to be specified in the BASYX_WRAPPER java property or system
+ * environment variable.
+ *
+ * @author espen
+ *
+ */
+public class AASWrapperExecutable {
+ private static final Logger logger = LoggerFactory.getLogger(AASWrapperExecutable.class);
+ public static final int PORT = 6500;
+ public static final String HOST = "aas-wrapper";
+
+ private static String registryAddress;
+ private static Map<String, PropertyConfiguration> propertyConfigs;
+ private static Map<String, String> activeProviders = new HashMap<>();
+ private static Map<String, String> providerPaths = new HashMap<>();
+
+ public static void main(String[] args) throws Exception {
+ logger.info("Wrapper started, waiting for other components");
+ // Use docker-compose health check for registry and aas instead
+ Thread.sleep(6000);
+
+ logger.info("Loading configuration...");
+ loadConfigs("wrapper.properties");
+
+ logger.info("Creating proxy for " + propertyConfigs.size() + " properties...");
+ IAASRegistryService registry = new AASRegistryProxy(registryAddress);
+ IPropertyWrapperService connector = new SeparateAASService(registry, propertyConfigs.values());
+
+ logger.info("Creating " + activeProviders.size() + " providers...");
+ List<IWrapperProvider> providers = createProxyProviders();
+ providers.forEach(p -> p.initialize(connector, registry, propertyConfigs.values()));
+
+ if (!providers.isEmpty()) {
+ logger.info("Starting property proxy service...");
+ connector.start();
+ logger.info("Starting proxy providers...");
+ createServlet(providers);
+ } else {
+ logger.info("No providers have been created!");
+ }
+
+ logger.info("Finished");
+ }
+
+ private static List<IWrapperProvider> createProxyProviders() {
+ List<IWrapperProvider> providers = new ArrayList<>();
+ for (Entry<String, String> entry : activeProviders.entrySet()) {
+ String providerId = entry.getKey();
+ String providerType = entry.getValue();
+ IWrapperProvider newProvider;
+ switch (providerType) {
+ case (AASWrapperProvider.TYPE):
+ newProvider = new AASWrapperProvider(HOST, PORT);
+ break;
+ case (GrafanaProvider.TYPE):
+ newProvider = new GrafanaProvider();
+ break;
+ case (StreamsheetsProvider.TYPE):
+ newProvider = new StreamsheetsProvider();
+ break;
+ default:
+ newProvider = new GenericWrapperProvider();
+ }
+ String newPath = providerPaths.get(providerId);
+ if (newPath != null) {
+ newProvider.setProviderPath(newPath);
+ }
+ providers.add(newProvider);
+ }
+ return providers;
+ }
+
+ private static void loadConfigs(String resourceFileName) {
+ // Load resource file
+ logger.info("Loading proxy property file " + resourceFileName);
+ Map<String, String> configMap = new HashMap<>();
+ BaSyxConfiguration config = new BaSyxConfiguration(configMap);
+ config.loadFileOrDefaultResource("BASYX_WRAPPER", "wrapper.properties");
+
+ loadRegistryConfig(config);
+ propertyConfigs = loadPropertyConfigs(configMap);
+ loadProviderConfigs(configMap);
+ }
+
+ private static void loadRegistryConfig(BaSyxConfiguration config) {
+ registryAddress = config.getProperty("registry.endpoint");
+ logger.info("Registry address: " + registryAddress);
+ }
+
+ private static Map<String, PropertyConfiguration> loadPropertyConfigs(Map<String, String> configMap) {
+ Map<String, PropertyConfiguration> configs = new HashMap<>();
+ for (Entry<String, String> entry : configMap.entrySet()) {
+ String key = entry.getKey();
+ String[] propKeys = key.split("\\.");
+ if (propKeys.length != 3 || !propKeys[0].equals(PropertyConfiguration.INDEX)) {
+ continue;
+ }
+
+ String propId = propKeys[1];
+ String parameter = propKeys[2];
+ String value = entry.getValue();
+ if (!configs.containsKey(propId)) {
+ logger.info("Creating property " + propId);
+ PropertyConfiguration propConfig = new PropertyConfiguration();
+ propConfig.setId(propId);
+ configs.put(propId, propConfig);
+ }
+ setPropertyParameter(configs, propId, parameter, value);
+ }
+ return configs;
+ }
+
+ private static void loadProviderConfigs(Map<String, String> configMap) {
+ for (Entry<String, String> entry : configMap.entrySet()) {
+ String key = entry.getKey();
+ String[] propKeys = key.split("\\.");
+ if (propKeys.length != 3 || !propKeys[0].equals(GenericWrapperProvider.INDEX)) {
+ continue;
+ }
+
+ String providerId = propKeys[1];
+ String parameter = propKeys[2];
+ String value = entry.getValue();
+
+ switch (parameter) {
+ case ("type"):
+ activeProviders.put(providerId, value);
+ logger.info("Setting provider '" + providerId + "': type=" + value);
+ break;
+ case ("path"):
+ providerPaths.put(providerId, value);
+ logger.info("Setting provider '" + providerId + "': path=" + value);
+ break;
+ default:
+ logger.info("Invalid config for provider '" + providerId + "': " + parameter + "=" + value);
+ }
+
+ }
+ }
+
+ private static void setPropertyParameter(Map<String, PropertyConfiguration> configs, String propId,
+ String parameter, String value) {
+ logger.info("Setting property '" + propId + "': " + parameter + "=" + value);
+ PropertyConfiguration prop = configs.get(propId);
+ prop.put(parameter, value);
+ if (parameter.equals(PropertyConfiguration.TYPE)) {
+ // Possibility to add more property types
+ switch (value) {
+ case (AASPropertyConfiguration.TYPE):
+ configs.put(propId, new AASPropertyConfiguration(prop));
+ break;
+ default:
+ logger.error("Unknown property type: " + value);
+ }
+ }
+ }
+
+ private static void createServlet(List<IWrapperProvider> providers) {
+ BaSyxContextConfiguration contextConfig = new BaSyxContextConfiguration();
+ contextConfig.loadFromDefaultSource();
+ BaSyxContext context = contextConfig.createBaSyxContext();
+ for (IWrapperProvider provider : providers) {
+ context.addServletMapping(provider.getProviderPath() + "/*",
+ new GenericHTTPInterface(provider));
+ }
+ AASHTTPServer server = new AASHTTPServer(context);
+ server.start();
+ }
+}
+
diff --git a/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/exception/WrapperRequestException.java b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/exception/WrapperRequestException.java
new file mode 100644
index 0000000..136e305
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/exception/WrapperRequestException.java
@@ -0,0 +1,20 @@
+package org.eclipse.basyx.wrapper.exception;
+
+/**
+ * Exception that is thrown when the wrapper cannot request a value from its datasource
+ *
+ * @author espen
+ *
+ */
+public class WrapperRequestException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ public WrapperRequestException(String message) {
+ super(message);
+ }
+
+ public WrapperRequestException(String message, Throwable error) {
+ super(message, error);
+ }
+
+}
\ No newline at end of file
diff --git a/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/GenericHTTPInterface.java b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/GenericHTTPInterface.java
new file mode 100644
index 0000000..702c848
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/GenericHTTPInterface.java
@@ -0,0 +1,243 @@
+package org.eclipse.basyx.wrapper.provider;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.StringJoiner;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.basyx.vab.exception.provider.MalformedRequestException;
+import org.eclipse.basyx.vab.exception.provider.ProviderException;
+import org.eclipse.basyx.vab.modelprovider.VABPathTools;
+import org.eclipse.basyx.vab.protocol.http.server.BasysHTTPServlet;
+import org.eclipse.basyx.vab.protocol.http.server.ExceptionToHTTPCodeMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.io.ByteSource;
+
+/**
+ * Implements a generic HTTP interface based on a JSONProvider that implements
+ * GET, PUT, POST, PATCH and DELETE and supports returning objects for each of the
+ * primitive. For PUT, POST and PATCH, it also supports passing objects.
+ *
+ * @author espen
+ *
+ */
+public class GenericHTTPInterface extends BasysHTTPServlet {
+ private static Logger logger = LoggerFactory.getLogger(GenericHTTPInterface.class);
+
+ private static final String CHARSET = "UTF-8";
+ private static final String MIMETYPE = "application/json";
+
+ /**
+ * Version information to identify the version of serialized instances
+ */
+ private static final long serialVersionUID = 1L;
+
+ private final JSONProvider provider;
+
+ /**
+ * Constructor
+ */
+ public GenericHTTPInterface(IWrapperProvider backend) {
+ this.provider = new JSONProvider(backend);
+ }
+
+ /**
+ * Implement "Get" operation
+ *
+ * Process HTTP get request - get sub model property value
+ */
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ try {
+ String path = extractPath(req);
+ prepareResponse(resp);
+ provider.processGet(path, resp.getOutputStream());
+ } catch (IOException e) {
+ logger.warn("Exception in HTTP-GET", e);
+ } catch (ProviderException e) {
+ int httpCode = ExceptionToHTTPCodeMapper.mapFromException(e);
+ resp.setStatus(httpCode);
+ logger.debug("Exception in GET request - response-code: " + httpCode, e);
+ }
+ }
+
+ /**
+ * Implement "Set" operation
+ */
+ @Override
+ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ try {
+ String path = extractPath(req);
+ String serValue = extractSerializedValue(req);
+ prepareResponse(resp);
+ provider.processPut(path, serValue, resp.getOutputStream());
+ } catch (IOException e) {
+ logger.warn("Exception in HTTP-PUT", e);
+ } catch (ProviderException e) {
+ int httpCode = ExceptionToHTTPCodeMapper.mapFromException(e);
+ resp.setStatus(httpCode);
+ logger.debug("Exception in PUT request - response-code: " + httpCode, e);
+ }
+ }
+
+
+ /**
+ * <pre>
+ * Handle HTTP POST operation. Creates a new Property, Operation, Event,
+ * Submodel or AAS or invokes an operation.
+ */
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ try {
+ String path = extractPath(req);
+ String serValue = extractSerializedValue(req);
+ prepareResponse(resp);
+ provider.processPost(path, serValue, resp.getOutputStream());
+ } catch (IOException e) {
+ logger.warn("Exception in HTTP-POST", e);
+ } catch (ProviderException e) {
+ int httpCode = ExceptionToHTTPCodeMapper.mapFromException(e);
+ resp.setStatus(httpCode);
+ logger.debug("Exception in POST request - response-code: " + httpCode, e);
+ }
+ }
+
+
+ /**
+ * Handle a HTTP PATCH operation. Updates a map or collection
+ *
+ */
+ @Override
+ protected void doPatch(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ try {
+ String path = extractPath(req);
+ String serValue = extractSerializedValue(req);
+ prepareResponse(resp);
+ provider.processPatch(path, serValue, resp.getOutputStream());
+ } catch (IOException e) {
+ logger.warn("Exception in HTTP-PATCH", e);
+ } catch (ProviderException e) {
+ int httpCode = ExceptionToHTTPCodeMapper.mapFromException(e);
+ resp.setStatus(httpCode);
+ logger.debug("Exception in PATCH request - response-code: " + httpCode, e);
+ }
+ }
+
+
+ /**
+ * Implement "Delete" operation. Deletes any resource under the given path.
+ */
+ @Override
+ protected void doDelete(HttpServletRequest req, HttpServletResponse resp) {
+ try {
+ String path = extractPath(req);
+ prepareResponse(resp);
+ provider.processDelete(path, resp.getOutputStream());
+ } catch (IOException e) {
+ logger.warn("Exception in HTTP-DELETE", e);
+ } catch (ProviderException e) {
+ int httpCode = ExceptionToHTTPCodeMapper.mapFromException(e);
+ resp.setStatus(httpCode);
+ logger.debug("Exception in DELETE request - response-code: " + httpCode, e);
+ }
+ }
+
+
+ private String extractPath(HttpServletRequest req) {
+ // Extract path
+ String uri = req.getRequestURI();
+
+ // Normalizes URI
+ String nUri = "/" + VABPathTools.stripSlashes(uri);
+ String contextPath = req.getContextPath();
+ if (nUri.startsWith(contextPath) && nUri.length() > getEnvironmentPathSize(req) - 1) {
+ String path = nUri.substring(getEnvironmentPathSize(req));
+ String extractedParameters = extractParameters(req);
+
+ path = VABPathTools.stripSlashes(path);
+
+ if (extractedParameters.isEmpty()) {
+ path += "/";
+ } else {
+ path += extractedParameters;
+ }
+ return path;
+ }
+ throw new MalformedRequestException("The passed path " + uri + " is not a possbile path for this server.");
+ }
+
+ /**
+ * Extracts request parameters from the request
+ *
+ * @param req
+ * @return
+ */
+ private String extractParameters(HttpServletRequest req) {
+ Enumeration<String> parameterNames = req.getParameterNames();
+
+ // Collect list of parameters
+ List<String> parameters = new ArrayList<>();
+ while (parameterNames.hasMoreElements()) {
+
+ StringBuilder ret = new StringBuilder();
+ String paramName = parameterNames.nextElement();
+ ret.append(paramName);
+ ret.append("=");
+
+ String[] paramValues = req.getParameterValues(paramName);
+ for (int i = 0; i < paramValues.length; i++) {
+ ret.append(paramValues[i]);
+ }
+ parameters.add(ret.toString());
+
+ }
+
+ // If no parameter is existing, return an empty string. Else join the parameters
+ // and return them prefixed with a ?
+ if (parameters.isEmpty()) {
+ return "";
+ } else {
+ StringJoiner joiner = new StringJoiner("&");
+ parameters.stream().forEach(joiner::add);
+ return "?" + joiner.toString();
+ }
+ }
+
+ private int getEnvironmentPathSize(HttpServletRequest req) {
+ return req.getContextPath().length() + req.getServletPath().length();
+ }
+
+
+ /**
+ * Read serialized value
+ * @param req
+ * @return
+ * @throws IOException
+ */
+ private String extractSerializedValue(HttpServletRequest req) throws IOException {
+ // https://www.baeldung.com/convert-input-stream-to-string#guava
+ ByteSource byteSource = new ByteSource() {
+ @Override
+ public InputStream openStream() throws IOException {
+ return req.getInputStream();
+ }
+ };
+
+ return byteSource.asCharSource(StandardCharsets.UTF_8).read();
+ }
+
+ private void prepareResponse(HttpServletResponse resp) {
+ resp.setContentType(MIMETYPE);
+ resp.setCharacterEncoding(CHARSET);
+ resp.setStatus(200);
+ }
+}
diff --git a/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/GenericWrapperProvider.java b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/GenericWrapperProvider.java
new file mode 100644
index 0000000..27180b7
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/GenericWrapperProvider.java
@@ -0,0 +1,88 @@
+package org.eclipse.basyx.wrapper.provider;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.eclipse.basyx.aas.registration.api.IAASRegistryService;
+import org.eclipse.basyx.vab.exception.provider.MalformedRequestException;
+import org.eclipse.basyx.vab.modelprovider.VABPathTools;
+import org.eclipse.basyx.wrapper.receiver.IPropertyWrapperService;
+import org.eclipse.basyx.wrapper.receiver.configuration.PropertyConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Wraps any connector API with a ModelProvider
+ *
+ * @author espen
+ *
+ */
+public class GenericWrapperProvider implements IWrapperProvider {
+ public static final String INDEX = "providers";
+ private static final Logger logger = LoggerFactory.getLogger(GenericWrapperProvider.class);
+
+ protected IPropertyWrapperService proxyService;
+ protected String providerPath = "provider";
+ protected Set<String> passiveProperties = new HashSet<>();
+
+ @Override
+ public void initialize(IPropertyWrapperService wrapperService, IAASRegistryService registry,
+ Collection<PropertyConfiguration> configs) {
+ logger.info(
+ "Initializing provider '" + this.getClass().getSimpleName() + "' on path " + this.getProviderPath());
+ this.proxyService = wrapperService;
+ for (PropertyConfiguration config : configs) {
+ if (!config.getActive()) {
+ passiveProperties.add(config.getId());
+ }
+ }
+ }
+
+ @Override
+ public Object get(String path) {
+ path = preparePath(path);
+ if ( passiveProperties.contains(path) ) {
+ proxyService.generatePassiveValue(path);
+ }
+ return proxyService.getPropertyValue(path);
+ }
+
+ @Override
+ public Object post(String path, Object newValue) {
+ path = preparePath(path);
+ proxyService.setPropertyValue(path, newValue);
+ return newValue;
+ }
+
+ protected String preparePath(String path) {
+ VABPathTools.checkPathForNull(path);
+ path = VABPathTools.stripSlashes(path);
+ return path;
+ }
+
+ @Override
+ public String getProviderPath() {
+ return providerPath;
+ }
+
+ @Override
+ public void setProviderPath(String path) {
+ this.providerPath = path;
+ }
+
+ @Override
+ public Object delete(String path) {
+ throw new MalformedRequestException("Deleting elements is not supported");
+ }
+
+ @Override
+ public Object put(String path, Object data) {
+ throw new MalformedRequestException("Setting elements is not supported");
+ }
+
+ @Override
+ public Object patch(String path, Object data) {
+ throw new MalformedRequestException("Patching elements is not supported");
+ }
+}
diff --git a/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/HTTPModelProvider.java b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/HTTPModelProvider.java
new file mode 100644
index 0000000..e50edfe
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/HTTPModelProvider.java
@@ -0,0 +1,19 @@
+package org.eclipse.basyx.wrapper.provider;
+
+/**
+ * A simple, generic HTTP-REST model provider
+ *
+ * @author espen
+ *
+ */
+public interface HTTPModelProvider {
+ public Object get(String path);
+
+ public Object delete(String path);
+
+ public Object put(String path, Object data);
+
+ public Object post(String path, Object data);
+
+ public Object patch(String path, Object data);
+}
diff --git a/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/IWrapperProvider.java b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/IWrapperProvider.java
new file mode 100644
index 0000000..d8b62b9
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/IWrapperProvider.java
@@ -0,0 +1,39 @@
+package org.eclipse.basyx.wrapper.provider;
+
+import java.util.Collection;
+
+import org.eclipse.basyx.aas.registration.api.IAASRegistryService;
+import org.eclipse.basyx.wrapper.receiver.IPropertyWrapperService;
+import org.eclipse.basyx.wrapper.receiver.configuration.PropertyConfiguration;
+
+/**
+ * Interface for a generic connector that makes use of the HTTPModelProvider interface
+ *
+ * @author espen
+ *
+ */
+public interface IWrapperProvider extends HTTPModelProvider {
+
+ /**
+ * Returns the path for this provider
+ *
+ * @return
+ */
+ public String getProviderPath();
+
+ /**
+ * Sets the path for this provider
+ *
+ * @param path
+ */
+ public void setProviderPath(String path);
+
+ /**
+ * Initialize the provider
+ *
+ * @param proxyService
+ * @param registry
+ */
+ public void initialize(IPropertyWrapperService proxyService, IAASRegistryService registry,
+ Collection<PropertyConfiguration> configs);
+}
diff --git a/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/JSONProvider.java b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/JSONProvider.java
new file mode 100644
index 0000000..e63e888
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/JSONProvider.java
@@ -0,0 +1,166 @@
+package org.eclipse.basyx.wrapper.provider;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.eclipse.basyx.vab.coder.json.metaprotocol.Result;
+import org.eclipse.basyx.vab.coder.json.serialization.DefaultTypeFactory;
+import org.eclipse.basyx.vab.coder.json.serialization.GSONTools;
+import org.eclipse.basyx.vab.exception.provider.MalformedRequestException;
+import org.eclipse.basyx.vab.exception.provider.ProviderException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Implementation variant of the JSONProvider for a generic HTTPModelProvider backend
+ *
+ * @author espen
+ *
+ */
+public class JSONProvider {
+
+ private static Logger logger = LoggerFactory.getLogger(JSONProvider.class);
+
+ /**
+ * Reference to serializer / deserializer
+ */
+ protected GSONTools serializer = null;
+
+ private HTTPModelProvider backend;
+
+ /**
+ * Constructor
+ */
+ public JSONProvider(HTTPModelProvider backend) {
+ this.backend = backend;
+ serializer = new GSONTools(new DefaultTypeFactory());
+ }
+
+ /**
+ * Marks success as false and delivers exception cause messages
+ * @param e
+ * @return
+ */
+ private String serialize(Exception e) {
+ // Create Ack
+ Result result = new Result(e);
+
+ // Serialize the whole thing
+ return serialize(result);
+ }
+
+
+ /**
+ * Serialize IResult (HashMap)
+ * @param string
+ * @return
+ */
+ private String serialize(Result string) {
+ // Serialize the whole thing
+ return serializer.serialize(string);
+ }
+
+
+ /**
+ * Send Error
+ * @param e
+ * @param path
+ * @param resp
+ */
+ private void sendException(OutputStream resp, Exception e) {
+
+ // Serialize Exception
+ String jsonString = serialize(e);
+ try {
+ resp.write(jsonString.getBytes(StandardCharsets.UTF_8));
+ } catch(IOException innerE) {
+ throw new ProviderException("Failed to send Exception '" + e.getMessage() + "' to client", innerE);
+ }
+
+ //If the Exception is a ProviderException, just rethrow it
+ if(e instanceof ProviderException) {
+ throw (ProviderException) e;
+ }
+
+ //If the Exception is not a ProviderException encapsulate it in one and log it
+ logger.error("Unknown Exception in JSONProvider", e);
+ throw new ProviderException(e);
+ }
+
+
+ /**
+ * Extracts parameter from JSON and handles de-serialization errors
+ */
+ private Object extractParameter(String serializedJSONValue) {
+ // Return value
+ Object result = null;
+ try {
+ // Deserialize json body
+ result = serializer.deserialize(serializedJSONValue);
+ } catch (Exception e) {
+ //JSON could not be deserialized
+ throw new MalformedRequestException(e);
+ }
+
+ return result;
+ }
+
+
+ public void processGet(String path, OutputStream outputStream) {
+ try {
+ logger.info("GET '" + path + "'");
+ Object value = backend.get(path);
+ String jsonString = serializer.serialize(value);
+ outputStream.write(jsonString.getBytes(StandardCharsets.UTF_8));
+ } catch (Exception e) {
+ sendException(outputStream, e);
+ }
+ }
+
+ public void processDelete(String path, OutputStream outputStream) {
+ try {
+ logger.info("DELETE '" + path + "'");
+ Object value = backend.delete(path);
+ String jsonString = serializer.serialize(value);
+ outputStream.write(jsonString.getBytes(StandardCharsets.UTF_8));
+ } catch (Exception e) {
+ sendException(outputStream, e);
+ }
+ }
+
+ public void processPost(String path, String serializedJSONValue, OutputStream outputStream) {
+ try {
+ Object parameter = extractParameter(serializedJSONValue);
+ Object value = backend.post(path, parameter);
+ String jsonString = serializer.serialize(value);
+ outputStream.write(jsonString.getBytes(StandardCharsets.UTF_8));
+ } catch (Exception e) {
+ sendException(outputStream, e);
+ }
+ }
+
+ public void processPatch(String path, String serializedJSONValue, OutputStream outputStream) {
+ try {
+ logger.info("PATCH " + path + "'");
+ Object parameter = extractParameter(serializedJSONValue);
+ Object value = backend.patch(path, parameter);
+ String jsonString = serializer.serialize(value);
+ outputStream.write(jsonString.getBytes(StandardCharsets.UTF_8));
+ } catch (Exception e) {
+ sendException(outputStream, e);
+ }
+ }
+
+ public void processPut(String path, String serializedJSONValue, OutputStream outputStream) {
+ try {
+ logger.info("PUT " + path + "'");
+ Object parameter = extractParameter(serializedJSONValue);
+ Object value = backend.put(path, parameter);
+ String jsonString = serializer.serialize(value);
+ outputStream.write(jsonString.getBytes(StandardCharsets.UTF_8));
+ } catch (Exception e) {
+ sendException(outputStream, e);
+ }
+ }
+}
diff --git a/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/aas/AASWrapperProvider.java b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/aas/AASWrapperProvider.java
new file mode 100644
index 0000000..7d0c473
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/aas/AASWrapperProvider.java
@@ -0,0 +1,138 @@
+package org.eclipse.basyx.wrapper.provider.aas;
+
+import java.util.Collection;
+
+import org.eclipse.basyx.aas.metamodel.map.AssetAdministrationShell;
+import org.eclipse.basyx.aas.metamodel.map.descriptor.AASDescriptor;
+import org.eclipse.basyx.aas.metamodel.map.descriptor.SubmodelDescriptor;
+import org.eclipse.basyx.aas.metamodel.map.parts.Asset;
+import org.eclipse.basyx.aas.registration.api.IAASRegistryService;
+import org.eclipse.basyx.aas.restapi.AASModelProvider;
+import org.eclipse.basyx.aas.restapi.VABMultiSubmodelProvider;
+import org.eclipse.basyx.submodel.metamodel.api.identifier.IdentifierType;
+import org.eclipse.basyx.submodel.metamodel.map.SubModel;
+import org.eclipse.basyx.submodel.metamodel.map.identifier.Identifier;
+import org.eclipse.basyx.submodel.restapi.SubModelProvider;
+import org.eclipse.basyx.vab.exception.provider.MalformedRequestException;
+import org.eclipse.basyx.wrapper.provider.IWrapperProvider;
+import org.eclipse.basyx.wrapper.receiver.IPropertyWrapperService;
+import org.eclipse.basyx.wrapper.receiver.configuration.PropertyConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * AAS Provider for the wrapper that exposes the GET-Part of the AAS interface
+ *
+ * @author espen
+ *
+ */
+public class AASWrapperProvider implements IWrapperProvider {
+ private static final Logger logger = LoggerFactory.getLogger(AASWrapperProvider.class);
+ public static final String TYPE = "AAS";
+
+ private final String host;
+ private final int port;
+
+ private VABMultiSubmodelProvider provider;
+
+ private String providerPath = "/aas";
+
+ /**
+ * Standard constructor for the AASWrapperProvider. Needs to know the wrapper endpoint to be able to register the
+ * created AAS interface
+ *
+ * @param host Host of the wrapper
+ * @param port Port for the wrapper
+ */
+ public AASWrapperProvider(String host, int port) {
+ this.host = host;
+ this.port = port;
+ }
+
+ @Override
+ public void initialize(IPropertyWrapperService wrapperService, IAASRegistryService registry,
+ Collection<PropertyConfiguration> configs) {
+ logger.info("Initializing provider '" + this.getClass().getSimpleName() + "' on path " + this.getProviderPath());
+
+ // Create the VABMultiSubmodelProvider
+ AssetAdministrationShell aas = new WrapperAssetAdministrationShell();
+ Asset asset = new WrapperAsset();
+ aas.setAsset(asset);
+ SubModel sm = new WrapperSubmodel("Wrapper", new Identifier(IdentifierType.CUSTOM, "WrapperSubmodel"),
+ wrapperService,
+ configs);
+ aas.addSubModel(sm);
+
+ provider = new VABMultiSubmodelProvider(new AASModelProvider(aas));
+ provider.addSubmodel(new SubModelProvider(sm));
+
+ // Register the aas
+ AASDescriptor desc = createDescriptor(aas, sm);
+ registerAAS(registry, desc);
+ }
+
+ private AASDescriptor createDescriptor(AssetAdministrationShell aas, SubModel... submodels) {
+ AASDescriptor descriptor = new AASDescriptor(aas, "http://" + host + ":" + port + "/aas");
+ for (SubModel sm : submodels) {
+ descriptor.addSubmodelDescriptor(
+ new SubmodelDescriptor(sm,
+ "http://" + host + ":" + port + "/aas/submodels/" + sm.getIdShort() + "/"));
+ }
+ return descriptor;
+ }
+
+ private static void registerAAS(IAASRegistryService registry, AASDescriptor descriptor) {
+ // Quick & dirty, try to register until registry is up
+ for (int i = 0; i < 10; i++) {
+ try {
+ registry.register(descriptor);
+ break;
+ } catch (Exception e) {
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e1) {
+ logger.warn("Interrupted", e1);
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ }
+
+ @Override
+ public String getProviderPath() {
+ return providerPath;
+ }
+
+ @Override
+ public void setProviderPath(String path) {
+ this.providerPath = path;
+ }
+
+ @Override
+ public Object post(String path, Object data) {
+ throw new MalformedRequestException("Creating elements is not supported");
+ }
+
+ /**
+ * Only expose the get-Part of the HTTP-REST Interface of the AAS
+ */
+ @Override
+ public Object get(String path) {
+ return provider.getModelPropertyValue(path);
+ }
+
+ @Override
+ public Object delete(String path) {
+ throw new MalformedRequestException("Deleting elements is not supported");
+ }
+
+ @Override
+ public Object put(String path, Object data) {
+ throw new MalformedRequestException("Setting elements is not supported");
+ }
+
+ @Override
+ public Object patch(String path, Object data) {
+ throw new MalformedRequestException("Patching elements is not supported");
+ }
+}
diff --git a/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/aas/WrapperAsset.java b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/aas/WrapperAsset.java
new file mode 100644
index 0000000..bd4b86d
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/aas/WrapperAsset.java
@@ -0,0 +1,19 @@
+package org.eclipse.basyx.wrapper.provider.aas;
+
+import org.eclipse.basyx.aas.metamodel.api.parts.asset.AssetKind;
+import org.eclipse.basyx.aas.metamodel.map.parts.Asset;
+import org.eclipse.basyx.submodel.metamodel.api.identifier.IdentifierType;
+
+/**
+ * Dummy asset for the wrapper AAS interface
+ *
+ * @author espen
+ *
+ */
+public class WrapperAsset extends Asset {
+ public WrapperAsset() {
+ setIdShort("WrapperAsset");
+ setIdentification(IdentifierType.CUSTOM, "WrapperAsset");
+ setAssetKind(AssetKind.INSTANCE);
+ }
+}
diff --git a/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/aas/WrapperAssetAdministrationShell.java b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/aas/WrapperAssetAdministrationShell.java
new file mode 100644
index 0000000..c9a473a
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/aas/WrapperAssetAdministrationShell.java
@@ -0,0 +1,17 @@
+package org.eclipse.basyx.wrapper.provider.aas;
+
+import org.eclipse.basyx.aas.metamodel.map.AssetAdministrationShell;
+import org.eclipse.basyx.submodel.metamodel.api.identifier.IdentifierType;
+
+/**
+ * Dummy AAS for the wrapper AAS interface
+ *
+ * @author espen
+ *
+ */
+public class WrapperAssetAdministrationShell extends AssetAdministrationShell {
+ public WrapperAssetAdministrationShell() {
+ setIdShort("WrapperAAS");
+ setIdentification(IdentifierType.CUSTOM, "WrapperAAS");
+ }
+}
diff --git a/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/aas/WrapperSubmodel.java b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/aas/WrapperSubmodel.java
new file mode 100644
index 0000000..cce4e44
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/aas/WrapperSubmodel.java
@@ -0,0 +1,113 @@
+package org.eclipse.basyx.wrapper.provider.aas;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import org.eclipse.basyx.submodel.metamodel.api.identifier.IIdentifier;
+import org.eclipse.basyx.submodel.metamodel.api.reference.enums.KeyElements;
+import org.eclipse.basyx.submodel.metamodel.api.reference.enums.KeyType;
+import org.eclipse.basyx.submodel.metamodel.api.submodelelement.ISubmodelElement;
+import org.eclipse.basyx.submodel.metamodel.map.SubModel;
+import org.eclipse.basyx.submodel.metamodel.map.reference.Key;
+import org.eclipse.basyx.submodel.metamodel.map.reference.Reference;
+import org.eclipse.basyx.submodel.metamodel.map.submodelelement.SubmodelElementCollection;
+import org.eclipse.basyx.submodel.metamodel.map.submodelelement.dataelement.property.Property;
+import org.eclipse.basyx.submodel.metamodel.map.submodelelement.operation.Operation;
+import org.eclipse.basyx.vab.modelprovider.lambda.VABLambdaProviderHelper;
+import org.eclipse.basyx.wrapper.receiver.IPropertyWrapperService;
+import org.eclipse.basyx.wrapper.receiver.PropertyResult;
+import org.eclipse.basyx.wrapper.receiver.configuration.PropertyConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A submodel for wrapping the data
+ *
+ * @author espen
+ *
+ */
+public class WrapperSubmodel extends SubModel {
+ private static final Logger logger = LoggerFactory.getLogger(WrapperSubmodel.class);
+
+ protected Map<String, SubmodelElementCollection> collections = new HashMap<>();
+ private IPropertyWrapperService proxyService;
+
+ public WrapperSubmodel(String idShort, IIdentifier id, IPropertyWrapperService connector,
+ Collection<PropertyConfiguration> config) {
+ super();
+ setIdShort(idShort);
+ setIdentification(id.getIdType(), id.getId());
+ setSemanticId(new Reference(new Key(KeyElements.CONCEPTDESCRIPTION, true, "0112/2///61360_4#AAF891#001", KeyType.IRDI)));
+
+ proxyService = connector;
+ proxyService.addProxyListener((String propId, PropertyResult result) -> {
+ SubmodelElementCollection coll = collections.get(propId);
+ if (coll != null) {
+ Collection<ISubmodelElement> smElements = generateSubmodelElements(propId, result);
+ coll.setValue(smElements);
+ } else {
+ logger.error("Invalid property '" + propId + "' has been updated");
+ }
+ });
+ initialize(config, connector);
+ }
+
+ private void initialize(Collection<PropertyConfiguration> configs, IPropertyWrapperService connector) {
+ for ( PropertyConfiguration config : configs ) {
+ String propId = config.getId();
+ if ( config.getActive() ) {
+ setActiveProperty(propId);
+ } else {
+ setPassiveProperty(propId);
+ }
+ }
+ }
+
+ private Collection<ISubmodelElement> generateSubmodelElements(String idShort, PropertyResult result) {
+ Collection<ISubmodelElement> elements = new ArrayList<>();
+ List<Object> data = result.getData();
+ List<String> dates = result.getTimestamps();
+ for (int i = data.size() - 1; i >= 0; i--) {
+ Property dateProp = new Property();
+ dateProp.setIdShort("time" + i);
+ dateProp.set(dates.get(i).toString());
+ elements.add(dateProp);
+ Property valueProp = new Property();
+ valueProp.setIdShort("value" + i);
+ valueProp.set(data.get(i));
+ elements.add(valueProp);
+ }
+ Operation setOperation = new Operation();
+ setOperation.setIdShort("set" + idShort);
+ Function<Object[], Object> fillInvokable = (params) -> {
+ // not supported, yet
+ return params;
+ };
+ setOperation.setInvocable(fillInvokable);
+ elements.add(setOperation);
+ return elements;
+ }
+
+ private void setActiveProperty(String propId) {
+ SubmodelElementCollection coll = new SubmodelElementCollection();
+ coll.setIdShort(propId);
+ collections.put(propId, coll);
+ addSubModelElement(coll);
+ }
+
+ private void setPassiveProperty(final String propId) {
+ SubmodelElementCollection coll = new SubmodelElementCollection();
+ coll.setIdShort(propId);
+ collections.put(propId, new SubmodelElementCollection());
+ addSubModelElement(coll);
+
+ coll.put(Property.VALUE, VABLambdaProviderHelper.createSimple(() -> {
+ proxyService.generatePassiveValue(propId);
+ return collections.get(propId).getValue();
+ }, null));
+ }
+}
diff --git a/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/grafana/GrafanaProvider.java b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/grafana/GrafanaProvider.java
new file mode 100644
index 0000000..d2c0766
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/grafana/GrafanaProvider.java
@@ -0,0 +1,62 @@
+package org.eclipse.basyx.wrapper.provider.grafana;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.eclipse.basyx.wrapper.provider.GenericWrapperProvider;
+import org.eclipse.basyx.wrapper.receiver.PropertyResult;
+
+/**
+ * Grafana-specific proxy provider. Is based on a SimpleJson interface for grafana
+ *
+ * @author espen
+ */
+public class GrafanaProvider extends GenericWrapperProvider {
+ public static final String TYPE = "GRAFANA";
+
+ public GrafanaProvider() {
+ this.providerPath = "/grafana";
+ }
+
+ @Override
+ public Object get(String path) {
+ return true;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Object post(String path, Object value) {
+ path = preparePath(path);
+
+ String idShort = null;
+ if (path.equals("search")) {
+ Map<String, Object> request = (Map<String, Object>) value;
+ idShort = (String) request.get("target");
+ Set<String> results = getSearchResults(idShort);
+ return results;
+ } else if (path.equals("query")) {
+ Map<String, Object> request = (Map<String, Object>) value;
+ List<Object> targets = (List<Object>) request.get("targets");
+ Map<String, Object> targetMap = (Map<String, Object>) targets.get(0);
+ idShort = (String) targetMap.get("target");
+ }
+
+ if (idShort == null || idShort.isEmpty()) {
+ return false;
+ }
+
+ PropertyResult result = (PropertyResult) super.get(idShort);
+ GrafanaTimeseries response = new GrafanaTimeseries();
+ response.addTarget(idShort, result);
+ return response;
+ }
+
+ private Set<String> getSearchResults(String term) {
+ Set<String> validProperties = proxyService.getValidProperties();
+ return validProperties.stream()
+ .filter(s -> s.toLowerCase().contains(term.toLowerCase()))
+ .collect(Collectors.toSet());
+ }
+}
diff --git a/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/grafana/GrafanaTimeseries.java b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/grafana/GrafanaTimeseries.java
new file mode 100644
index 0000000..c2c27f4
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/grafana/GrafanaTimeseries.java
@@ -0,0 +1,48 @@
+package org.eclipse.basyx.wrapper.provider.grafana;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.basyx.wrapper.receiver.PropertyResult;
+
+/**
+ * Conversion class for Grafana timeseries response
+ *
+ * @author espen
+ *
+ */
+public class GrafanaTimeseries extends ArrayList<Object> {
+ private static final long serialVersionUID = 1L;
+ public GrafanaTimeseries() {
+ super();
+ }
+
+ public void addTarget(String idShort, PropertyResult result) {
+ Map<String, Object> targetData = new HashMap<>();
+
+ List<Object> values = result.getData();
+ List<String> timestamps = result.getTimestamps();
+ List<List<Object>> dataPoints = new ArrayList<>(values.size());
+ for ( int i = 0; i < values.size(); i++ ) {
+ List<Object> entry = new ArrayList<>(2);
+ entry.add(values.get(i));
+ try {
+ Date date = PropertyResult.DATE_FORMAT.parse(timestamps.get(i));
+ long ms = date.getTime();
+ entry.add(ms);
+ } catch (ParseException e) {
+ e.printStackTrace();
+ break;
+ }
+ // entry.add(Integer.parseInt(timestamps.get(i))); // add string formatted timestamp
+ dataPoints.add(entry);
+ }
+ targetData.put("target", idShort);
+ targetData.put("datapoints", dataPoints);
+ this.add(targetData);
+ }
+}
diff --git a/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/streamsheets/StreamsheetsProvider.java b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/streamsheets/StreamsheetsProvider.java
new file mode 100644
index 0000000..481f110
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/provider/streamsheets/StreamsheetsProvider.java
@@ -0,0 +1,16 @@
+package org.eclipse.basyx.wrapper.provider.streamsheets;
+
+import org.eclipse.basyx.wrapper.provider.GenericWrapperProvider;
+
+/**
+ * Streamsheets-specific proxy provider
+ *
+ * @author espen
+ */
+public class StreamsheetsProvider extends GenericWrapperProvider {
+ public static final String TYPE = "STREAMSHEETS";
+
+ public StreamsheetsProvider() {
+ this.providerPath = "/streamsheets";
+ }
+}
\ No newline at end of file
diff --git a/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/DataPoint.java b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/DataPoint.java
new file mode 100644
index 0000000..d330116
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/DataPoint.java
@@ -0,0 +1,40 @@
+package org.eclipse.basyx.wrapper.receiver;
+
+import java.util.Date;
+
+/**
+ * Represents a single datapoint for a property
+ *
+ * @author espen
+ *
+ */
+public class DataPoint {
+ private Date timestamp;
+ private Object value;
+
+ public DataPoint(Object value) {
+ this.timestamp = new Date();
+ this.value = value;
+ }
+
+ public DataPoint(Date timestamp, Object value) {
+ this.timestamp = timestamp;
+ this.value = value;
+ }
+
+ public Date getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(Date timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public Object getValue() {
+ return value;
+ }
+
+ public void setValue(Object value) {
+ this.value = value;
+ }
+}
\ No newline at end of file
diff --git a/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/IPropertyEndpoint.java b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/IPropertyEndpoint.java
new file mode 100644
index 0000000..2dcf78d
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/IPropertyEndpoint.java
@@ -0,0 +1,13 @@
+package org.eclipse.basyx.wrapper.receiver;
+
+/**
+ * Represents a single property endpoint
+ *
+ * @author espen
+ *
+ */
+public interface IPropertyEndpoint {
+ public DataPoint retrieveValue();
+
+ public void setValue(Object newValue);
+}
diff --git a/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/IPropertyWrapperService.java b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/IPropertyWrapperService.java
new file mode 100644
index 0000000..1ffdc33
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/IPropertyWrapperService.java
@@ -0,0 +1,61 @@
+package org.eclipse.basyx.wrapper.receiver;
+
+import java.util.Set;
+
+import org.eclipse.basyx.wrapper.receiver.configuration.PropertyConfiguration;
+
+/**
+ * Interface for a property service to get and set properties
+ *
+ * @author espen
+ *
+ */
+public interface IPropertyWrapperService {
+ /**
+ * Adds a property configuration to the service
+ *
+ * @param config
+ */
+ public void addPropertyConfig(PropertyConfiguration config);
+
+ /**
+ * Returns a list of valid properties that have been configured
+ */
+ public Set<String> getValidProperties();
+
+ /**
+ * Gets the current result for a property
+ *
+ * @param propId
+ * @return
+ */
+ public PropertyResult getPropertyValue(String propId);
+
+ /**
+ * Explicitely generates a new value for a passive property.
+ *
+ * @param propId
+ * @return
+ */
+ public void generatePassiveValue(String propId);
+
+ /**
+ * Sets a remote data point to a new value
+ *
+ * @param propId
+ * @param newValue
+ */
+ public void setPropertyValue(String propId, Object newValue);
+
+ public void addProxyListener(IWrapperListener listener);
+
+ /**
+ * Start collecting data for all active properties
+ */
+ public void start();
+
+ /**
+ * Stop collecting data for all active properties
+ */
+ public void stop();
+}
diff --git a/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/IWrapperListener.java b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/IWrapperListener.java
new file mode 100644
index 0000000..e79886f
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/IWrapperListener.java
@@ -0,0 +1,16 @@
+package org.eclipse.basyx.wrapper.receiver;
+
+/**
+ * A listener for values that have been generated by the wrapper
+ *
+ * @author espen
+ *
+ */
+public interface IWrapperListener {
+ /**
+ * Informs the listener about a new value that has been generated
+ *
+ * @param result
+ */
+ public void newValue(String propId, PropertyResult result);
+}
diff --git a/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/PropertyResult.java b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/PropertyResult.java
new file mode 100644
index 0000000..118a07d
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/PropertyResult.java
@@ -0,0 +1,79 @@
+package org.eclipse.basyx.wrapper.receiver;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Represents a property result with multiple datapoints
+ *
+ * @author espen
+ *
+ */
+public class PropertyResult extends HashMap<String, Object> {
+ private static final long serialVersionUID = 1L;
+ public static final String CONTENT = "content";
+ public static final String DATES = "timestamp";
+ public static final String TITLE = "title";
+ public static final String SUCCESS = "success";
+
+ protected int maxValues = 10;
+ protected List<Object> content = new ArrayList<>();
+ // VAB can't serialize dates, yet
+ protected List<String> timestamp = new ArrayList<>();
+
+ public static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
+
+ public PropertyResult(String title, int maxValues) {
+ this.maxValues = maxValues;
+ put(CONTENT, content);
+ put(DATES, timestamp);
+ put(SUCCESS, true);
+ put(TITLE, "Value of property '" + title + "'");
+ }
+
+ public PropertyResult(PropertyResult clone) {
+ this.maxValues = clone.maxValues;
+ content = new ArrayList<>(clone.getData());
+ timestamp = new ArrayList<>(clone.getTimestamps());
+ put(CONTENT, content);
+ put(DATES, timestamp);
+ put(SUCCESS, clone.getSuccess());
+ put(TITLE, "Value of property '" + clone.getTitle() + "'");
+ }
+
+ public void setSuccess(boolean success) {
+ put(SUCCESS, success);
+ }
+
+ public boolean getSuccess() {
+ return (boolean) get(SUCCESS);
+ }
+
+ public void setTitle(String title) {
+ put(TITLE, title);
+ }
+
+ public String getTitle() {
+ return (String) get(TITLE);
+ }
+
+ public List<Object> getData() {
+ return content;
+ }
+
+ public List<String> getTimestamps() {
+ return timestamp;
+ }
+
+ public synchronized void addDataPoint(DataPoint value) {
+ content.add(value.getValue());
+ timestamp.add(DATE_FORMAT.format(value.getTimestamp()));
+ while (content.size() > maxValues) {
+ content.remove(0);
+ timestamp.remove(0);
+ }
+ }
+}
diff --git a/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/SeparateAASService.java b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/SeparateAASService.java
new file mode 100644
index 0000000..8c9de7b
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/SeparateAASService.java
@@ -0,0 +1,214 @@
+package org.eclipse.basyx.wrapper.receiver;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.basyx.aas.metamodel.map.descriptor.AASDescriptor;
+import org.eclipse.basyx.aas.metamodel.map.descriptor.SubmodelDescriptor;
+import org.eclipse.basyx.aas.registration.api.IAASRegistryService;
+import org.eclipse.basyx.submodel.metamodel.api.identifier.IIdentifier;
+import org.eclipse.basyx.submodel.metamodel.api.identifier.IdentifierType;
+import org.eclipse.basyx.submodel.metamodel.api.submodelelement.dataelement.IProperty;
+import org.eclipse.basyx.submodel.metamodel.connected.submodelelement.dataelement.ConnectedProperty;
+import org.eclipse.basyx.submodel.metamodel.map.identifier.Identifier;
+import org.eclipse.basyx.submodel.restapi.MultiSubmodelElementProvider;
+import org.eclipse.basyx.vab.factory.java.ModelProxyFactory;
+import org.eclipse.basyx.vab.modelprovider.VABPathTools;
+import org.eclipse.basyx.vab.protocol.http.connector.HTTPConnectorProvider;
+import org.eclipse.basyx.wrapper.exception.WrapperRequestException;
+import org.eclipse.basyx.wrapper.receiver.configuration.AASPropertyConfiguration;
+import org.eclipse.basyx.wrapper.receiver.configuration.PropertyConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Wrapper service for seperately querying the configured properties.
+ *
+ * @author espen
+ *
+ */
+public class SeparateAASService implements IPropertyWrapperService {
+ private static final Logger logger = LoggerFactory.getLogger(SeparateAASService.class);
+
+ // Registry
+ protected IAASRegistryService registry;
+
+ // Property configurations
+ protected Map<String, String> aasIds = new HashMap<>();
+ protected Map<String, String> submodelIds = new HashMap<>();
+ protected Map<String, String> shortIds = new HashMap<>();
+ protected Map<String, Integer> maxValues = new HashMap<>();
+ protected Set<String> activeProperties = new HashSet<>();
+
+ // Current data
+ protected Map<String, PropertyResult> propertyResults = new HashMap<>();
+
+ // Additional
+ protected Map<String, String> submodelCachedEndpoints = new HashMap<>();
+ protected Map<String, Thread> threads = new HashMap<>();
+ protected List<IWrapperListener> listeners = new ArrayList<>();
+
+ public SeparateAASService(IAASRegistryService registry, Collection<PropertyConfiguration> configs) {
+ this.registry = registry;
+ configs.forEach(this::addPropertyConfig);
+ }
+
+ @Override
+ public void addPropertyConfig(PropertyConfiguration config) {
+ if (config instanceof AASPropertyConfiguration) {
+ AASPropertyConfiguration aasConfig = (AASPropertyConfiguration) config;
+
+ String propId = config.getId();
+ logger.info("Initialize " + propId);
+ aasIds.put(propId, aasConfig.getAAS());
+ submodelIds.put(propId, aasConfig.getSubmodel());
+ shortIds.put(propId, aasConfig.getShortId());
+ propertyResults.put(propId, new PropertyResult(propId, config.getMaxValues()));
+ if (config.getActive()) {
+ activeProperties.add(propId);
+ int interval = config.getInterval(); // => in ms
+ threads.put(propId, createPropertyThread(propId, interval));
+ }
+ }
+ }
+
+ private AASDescriptor getAASDescriptorFromRegistry(String aasId) {
+ // Assume custom AAS identifier => aas identifier type doesn't really matter for lookup
+ IIdentifier aasIdentifier = new Identifier(IdentifierType.CUSTOM, aasId);
+ return registry.lookupAAS(aasIdentifier);
+ }
+
+ private String getSubmodelEndpoint(String aasId, String submodelId) {
+ String aasSmId = aasId + "::" + submodelId;
+ String smEndpoint = submodelCachedEndpoints.get(aasSmId);
+
+ if (smEndpoint == null) {
+ // Retrieve AAS descriptor from the registry to parse the submodel endpoint
+ try {
+ AASDescriptor aasDescriptor = getAASDescriptorFromRegistry(aasId);
+ SubmodelDescriptor smDescriptor = aasDescriptor.getSubmodelDescriptorFromIdShort(submodelId);
+
+ if (smDescriptor == null) {
+ throw new WrapperRequestException("Could not look up descriptor for SubModel '" + submodelId
+ + "' since it does not exist in AAS '" + aasId + "'");
+ }
+
+ smEndpoint = smDescriptor.getFirstEndpoint();
+
+ // Cache retrieved submodel endpoints
+ submodelCachedEndpoints.put(aasSmId, smEndpoint);
+ } catch (Exception e) {
+ throw new WrapperRequestException("Could not look up descriptor for AAS '" + aasId
+ + "' since it does not exist or is not available");
+ }
+ }
+
+ return smEndpoint;
+ }
+
+ private IProperty getConnectedProperty(String smEndpoint, String shortId) {
+ // Build the property address from that (and assume it's there)
+ String propEndpoint = VABPathTools.concatenatePaths(smEndpoint, MultiSubmodelElementProvider.ELEMENTS, shortId);
+
+ // Create a "ConnectedProperty" that gives access to the property
+ ModelProxyFactory mpf = new ModelProxyFactory(new HTTPConnectorProvider());
+ return new ConnectedProperty(mpf.createProxy(propEndpoint));
+ }
+
+ private Object getSinglePropertyValue(String propId) {
+ try {
+ String aasId = aasIds.get(propId);
+ String submodelId = submodelIds.get(propId);
+ String shortId = shortIds.get(propId);
+ String smEndpoint = getSubmodelEndpoint(aasId, submodelId);
+ IProperty property = getConnectedProperty(smEndpoint, shortId);
+ return property.getValue();
+ } catch (Exception e) {
+ throw new WrapperRequestException(e.getLocalizedMessage(), e);
+ }
+ }
+
+ @Override
+ public PropertyResult getPropertyValue(String propId) {
+ // Clone a result to prevent changes in the returned value
+ return new PropertyResult(propertyResults.get(propId));
+ }
+
+ @Override
+ public void setPropertyValue(String propId, Object newValue) {
+ try {
+ String aasId = aasIds.get(propId);
+ String submodelId = submodelIds.get(propId);
+ String shortId = shortIds.get(propId);
+ String smEndpoint = getSubmodelEndpoint(aasId, submodelId);
+ IProperty property = getConnectedProperty(smEndpoint, shortId);
+ property.setValue(newValue);
+ } catch (Exception e) {
+ throw new WrapperRequestException(e.getLocalizedMessage(), e);
+ }
+ }
+
+ @Override
+ public void generatePassiveValue(String propId) {
+ if (activeProperties.contains(propId)) {
+ throw new WrapperRequestException("Property with id '" + propId + "' is active");
+ }
+ retrieveValue(propId);
+ }
+
+ private Thread createPropertyThread(final String propId, final int intervalTime) {
+ return new Thread(() -> {
+ Thread thisThread = Thread.currentThread();
+ while (thisThread == threads.get(propId)) {
+ retrieveValue(propId);
+ try {
+ Thread.sleep(intervalTime);
+ } catch (InterruptedException e) {
+ }
+ }
+ });
+ }
+
+ private void retrieveValue(String propId) {
+ PropertyResult values = propertyResults.get(propId);
+ if (values == null) {
+ throw new WrapperRequestException("Property with id '" + propId + "' does not exist");
+ }
+ Object newValue = getSinglePropertyValue(propId);
+ DataPoint dataPoint = new DataPoint(newValue);
+ values.addDataPoint(dataPoint);
+
+ // inform listeners
+ for (IWrapperListener listener : listeners) {
+ listener.newValue(propId, new PropertyResult(values));
+ }
+ }
+
+ @Override
+ public void start() {
+ logger.info("Starting " + threads.size() + " threads");
+ for (Thread t : threads.values()) {
+ t.start();
+ }
+ }
+
+ @Override
+ public void stop() {
+ threads.clear();
+ }
+
+ @Override
+ public void addProxyListener(IWrapperListener listener) {
+ this.listeners.add(listener);
+ }
+
+ @Override
+ public Set<String> getValidProperties() {
+ return propertyResults.keySet();
+ }
+}
diff --git a/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/configuration/AASPropertyConfiguration.java b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/configuration/AASPropertyConfiguration.java
new file mode 100644
index 0000000..1871944
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/configuration/AASPropertyConfiguration.java
@@ -0,0 +1,51 @@
+package org.eclipse.basyx.wrapper.receiver.configuration;
+
+/**
+ * Represents a single configuration for a property. The datasource for this
+ * property is an AAS property.
+ *
+ * @author espen
+ *
+ */
+public class AASPropertyConfiguration extends PropertyConfiguration {
+ private static final long serialVersionUID = 1L;
+
+ public static final String TYPE = "AAS";
+
+ // indices in the configuration file
+ public static final String AAS = "aas";
+ public static final String SUBMODEL = "submodel";
+ public static final String SHORTID = "shortId";
+
+ public AASPropertyConfiguration() {
+ super();
+ }
+
+ public AASPropertyConfiguration(PropertyConfiguration config) {
+ putAll(config);
+ }
+
+ public String getAAS() {
+ return get(AAS);
+ }
+
+ public void setAAS(String aas) {
+ put(AAS, aas);
+ }
+
+ public String getSubmodel() {
+ return get(SUBMODEL);
+ }
+
+ public void setSubmodel(String submodel) {
+ put(SUBMODEL, submodel);
+ }
+
+ public String getShortId() {
+ return get(SHORTID);
+ }
+
+ public void setShortId(String shortId) {
+ put(SHORTID, shortId);
+ }
+}
\ No newline at end of file
diff --git a/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/configuration/PropertyConfiguration.java b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/configuration/PropertyConfiguration.java
new file mode 100644
index 0000000..1157229
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/java/org/eclipse/basyx/wrapper/receiver/configuration/PropertyConfiguration.java
@@ -0,0 +1,84 @@
+package org.eclipse.basyx.wrapper.receiver.configuration;
+
+import java.util.HashMap;
+import java.util.StringJoiner;
+
+/**
+ * Represents a single configuration for a proxy property.
+ *
+ * @author espen
+ *
+ */
+public class PropertyConfiguration extends HashMap<String, String> {
+ private static final long serialVersionUID = 1L;
+
+ // configuration indices
+ public static final String INDEX = "properties";
+ public static final String ID = "id";
+ public static final String TYPE = "type";
+ public static final String ACTIVE = "active";
+ public static final String INTERVAL = "interval";
+ public static final String MAX_VALUES = "maxValues";
+
+ public boolean getActive() {
+ return "true".equals(get(ACTIVE));
+ }
+
+ public void setActive(Boolean active) {
+ put(ACTIVE, active.toString());
+ }
+
+ /**
+ * Get active update interval in ms
+ *
+ * @return
+ */
+ public int getInterval() {
+ if (get(INTERVAL) != null) {
+ return Integer.parseInt(get(INTERVAL));
+ } else {
+ return 0;
+ }
+ }
+
+ public void setInterval(int interval) {
+ put(INTERVAL, String.valueOf(interval));
+ }
+
+ public int getMaxValues() {
+ if (get(MAX_VALUES) != null) {
+ return Integer.parseInt(get(MAX_VALUES));
+ } else {
+ return 1;
+ }
+ }
+
+ public void setMaxValues(int maxValues) {
+ put(INTERVAL, String.valueOf(maxValues));
+ }
+
+ public String getId() {
+ return get(ID);
+ }
+
+ public void setId(String id) {
+ put(ID, id);
+ }
+
+ public String getType() {
+ return get(TYPE);
+ }
+
+ public void setType(String type) {
+ put(TYPE, type);
+ }
+
+ @Override
+ public String toString() {
+ StringJoiner joiner = new StringJoiner(", ");
+ entrySet().forEach((Entry<String, String> e) -> {
+ joiner.add(e.getKey() + "=" + e.getValue());
+ });
+ return "{ " + joiner.toString() + " }";
+ }
+}
\ No newline at end of file
diff --git a/examples/basyx.aasWrapper/src/main/resources/context.properties b/examples/basyx.aasWrapper/src/main/resources/context.properties
new file mode 100644
index 0000000..6429487
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/resources/context.properties
@@ -0,0 +1,17 @@
+# ###############################
+# HTTP Context configuration file
+# ###############################
+
+# ###############################
+# Context Path
+# ###############################
+# Specifies the subpath in the url for this server context
+
+contextPath=/
+
+# ###############################
+# Port
+# ###############################
+# Specifies the port for this server context
+
+contextPort=6500
\ No newline at end of file
diff --git a/examples/basyx.aasWrapper/src/main/resources/wrapper.properties b/examples/basyx.aasWrapper/src/main/resources/wrapper.properties
new file mode 100644
index 0000000..957dc39
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/main/resources/wrapper.properties
@@ -0,0 +1,25 @@
+registry.endpoint=http://registry:4000/registry/
+
+properties.temp.type=AAS
+properties.temp.active=true
+properties.temp.aas=DashboardAAS
+properties.temp.submodel=DashboardSubmodel
+properties.temp.shortId=temperature
+properties.temp.interval=1000
+properties.temp.maxValues=10
+
+properties.dummy.type=AAS
+properties.dummy.active=false
+properties.dummy.aas=DashboardAAS
+properties.dummy.submodel=DashboardSubmodel
+properties.dummy.shortId=dummy
+properties.dummy.maxValues=3
+
+providers.simplified.type=STREAMSHEETS
+providers.simplified.path=/streamsheets
+
+providers.aas.type=AAS
+providers.aas.path=/proxyAAS
+
+providers.asdf.type=GRAFANA
+providers.asdf.path=/grafana
diff --git a/examples/basyx.aasWrapper/src/test/resources/.env b/examples/basyx.aasWrapper/src/test/resources/.env
new file mode 100644
index 0000000..790ac61
--- /dev/null
+++ b/examples/basyx.aasWrapper/src/test/resources/.env
@@ -0,0 +1,39 @@
+# ##################
+# Docker Environment
+# ##################
+
+# ##################
+# Host Port
+# ##################
+# Specifies the port for the Docker HOST the container port is mapped to
+
+BASYX_HOST_PORT=6500
+
+# ##################
+# Container Port
+# ##################
+# Specifies the port for the Docker CONTAINER that is be mapped for the host
+
+BASYX_CONTAINER_PORT=6500
+
+# ##################
+# Image Name
+# ##################
+# The image of the image that is build for this component
+
+BASYX_IMAGE_NAME=basyx/aas-wrapper
+
+# ##################
+# Image Tag
+# ##################
+# The image tag of the image that is build for this component
+
+BASYX_IMAGE_TAG=0.1.0-SNAPSHOT
+
+# ##################
+# Container Name
+# ##################
+# The name of the container used for the default environment
+
+BASYX_CONTAINER_NAME=aas-wrapper
+
diff --git a/examples/basyx.aasWrapper/start.bat b/examples/basyx.aasWrapper/start.bat
new file mode 100644
index 0000000..7d0dc6c
--- /dev/null
+++ b/examples/basyx.aasWrapper/start.bat
@@ -0,0 +1 @@
+docker-compose up
\ No newline at end of file
diff --git a/examples/basyx.aasWrapper/start.sh b/examples/basyx.aasWrapper/start.sh
new file mode 100644
index 0000000..d935e43
--- /dev/null
+++ b/examples/basyx.aasWrapper/start.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+docker-compose up
\ No newline at end of file
diff --git a/examples/basyx.aasWrapper/stop.bat b/examples/basyx.aasWrapper/stop.bat
new file mode 100644
index 0000000..58694d0
--- /dev/null
+++ b/examples/basyx.aasWrapper/stop.bat
@@ -0,0 +1 @@
+docker-compose down
\ No newline at end of file
diff --git a/examples/basyx.aasWrapper/stop.sh b/examples/basyx.aasWrapper/stop.sh
new file mode 100644
index 0000000..f5139e2
--- /dev/null
+++ b/examples/basyx.aasWrapper/stop.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+docker-compose down
\ No newline at end of file
diff --git a/examples/basyx.dashboardAAS/Dockerfile b/examples/basyx.dashboardAAS/Dockerfile
new file mode 100644
index 0000000..1a26b27
--- /dev/null
+++ b/examples/basyx.dashboardAAS/Dockerfile
@@ -0,0 +1,23 @@
+# Add java runtime environment for execution
+FROM java:8-jdk-alpine
+
+# Copy built jar to image using the jar name specified in the pom.xml (JAR_FILE)
+ARG JAR_FILE
+COPY target/${JAR_FILE} /usr/share/basyxExecutable.jar
+COPY target/lib /usr/share/lib
+COPY src/main/resources/context.properties /usr/share/config/context.properties
+COPY src/main/resources/dashboardsubmodel.properties /usr/share/config/dashboardsubmodel.properties
+
+# Expose the appropriate port. In case of Tomcat, this is 8080.
+ARG PORT
+EXPOSE ${PORT}
+
+# Set the path for the context configuration file
+ARG CONTEXT_CONFIG_KEY
+ENV ${CONTEXT_CONFIG_KEY} "/usr/share/config/context.properties"
+
+# Set the path for the submodel config
+ENV BASYX_DASHBOARDSUBMODEL "/usr/share/config/dashboardsubmodel.properties"
+
+# Start the jar
+CMD java -jar "/usr/share/basyxExecutable.jar"
\ No newline at end of file
diff --git a/examples/basyx.dashboardAAS/build.bat b/examples/basyx.dashboardAAS/build.bat
new file mode 100644
index 0000000..0977f37
--- /dev/null
+++ b/examples/basyx.dashboardAAS/build.bat
@@ -0,0 +1 @@
+.././mvnw clean install -U -Pdocker
\ No newline at end of file
diff --git a/examples/basyx.dashboardAAS/build.sh b/examples/basyx.dashboardAAS/build.sh
new file mode 100644
index 0000000..d223f82
--- /dev/null
+++ b/examples/basyx.dashboardAAS/build.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+.././mvnw clean install -U -Pdocker -DskipTests
\ No newline at end of file
diff --git a/examples/basyx.dashboardAAS/docker-compose.yml b/examples/basyx.dashboardAAS/docker-compose.yml
new file mode 100644
index 0000000..316eb9c
--- /dev/null
+++ b/examples/basyx.dashboardAAS/docker-compose.yml
@@ -0,0 +1,17 @@
+version: '3'
+services:
+
+ registry:
+ image: basyx/registry:0.1.0-SNAPSHOT
+ container_name: dashboard-registry
+ ports:
+ - 4000:4000
+
+ dashboard-aas:
+ image: basyx/dashboard-aas:0.1.0-SNAPSHOT
+ container_name: dashboard-aas
+ environment:
+ - BaSyxDashboardSubmodel_Min=15
+# - BaSyxDashboardSubmodel_Max=30
+ ports:
+ - 6400:6400
\ No newline at end of file
diff --git a/examples/basyx.dashboardAAS/pom.xml b/examples/basyx.dashboardAAS/pom.xml
new file mode 100644
index 0000000..ab16c85
--- /dev/null
+++ b/examples/basyx.dashboardAAS/pom.xml
@@ -0,0 +1,66 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.eclipse.basyx</groupId>
+ <artifactId>basyx.components.docker</artifactId>
+ <version>0.1.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>basyx.components.dashboardAAS</artifactId>
+ <name>BaSyx Dashboard AAS</name>
+
+ <properties>
+ <!--
+ basyx.components.executable is the executable class with the definition of the public void main(String[]).
+ It is needed when building the jar in the maven-jar-plugin (see basyx.components.docker/pom.xml)
+ -->
+ <basyx.components.executable>org.eclipse.basyx.dashboard.AASExecutable</basyx.components.executable>
+ </properties>
+
+ <packaging>jar</packaging>
+
+ <!-- Define additional plugins that are not included by default -->
+ <!-- Plugin configuration is done in parent project(s) -->
+ <build>
+ <plugins>
+ <!-- Attach sources to jar file -->
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-source-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+
+ <profiles>
+ <profile>
+ <!--
+ "Docker" profile - do not build & install docker images by default
+ Run "mvn install -Pdocker" in order to include docker
+ -->
+ <id>docker</id>
+ <build>
+ <plugins>
+ <!-- Read maven properties from file -->
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>properties-maven-plugin</artifactId>
+ </plugin>
+
+ <!-- Copy the dependencies necessary to run the jar -->
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-dependency-plugin</artifactId>
+ </plugin>
+
+ <!-- Build the docker image -->
+ <plugin>
+ <groupId>com.spotify</groupId>
+ <artifactId>dockerfile-maven-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+</project>
\ No newline at end of file
diff --git a/examples/basyx.dashboardAAS/src/main/java/org/eclipse/basyx/dashboard/AASExecutable.java b/examples/basyx.dashboardAAS/src/main/java/org/eclipse/basyx/dashboard/AASExecutable.java
new file mode 100644
index 0000000..20d61d7
--- /dev/null
+++ b/examples/basyx.dashboardAAS/src/main/java/org/eclipse/basyx/dashboard/AASExecutable.java
@@ -0,0 +1,86 @@
+package org.eclipse.basyx.dashboard;
+
+import java.util.ArrayList;
+
+import org.eclipse.basyx.aas.metamodel.map.AssetAdministrationShell;
+import org.eclipse.basyx.aas.metamodel.map.descriptor.AASDescriptor;
+import org.eclipse.basyx.aas.metamodel.map.descriptor.SubmodelDescriptor;
+import org.eclipse.basyx.aas.metamodel.map.parts.Asset;
+import org.eclipse.basyx.aas.registration.api.IAASRegistryService;
+import org.eclipse.basyx.aas.registration.proxy.AASRegistryProxy;
+import org.eclipse.basyx.aas.restapi.AASModelProvider;
+import org.eclipse.basyx.aas.restapi.VABMultiSubmodelProvider;
+import org.eclipse.basyx.components.configuration.BaSyxContextConfiguration;
+import org.eclipse.basyx.submodel.metamodel.map.SubModel;
+import org.eclipse.basyx.submodel.restapi.SubModelProvider;
+import org.eclipse.basyx.vab.modelprovider.api.IModelProvider;
+import org.eclipse.basyx.vab.protocol.http.server.AASHTTPServer;
+import org.eclipse.basyx.vab.protocol.http.server.BaSyxContext;
+import org.eclipse.basyx.vab.protocol.http.server.VABHTTPInterface;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Executable for a very basic, dynamic AAS, that is meant to be used in an example about how
+ * to integrate properties of "real" AAS in a dashboard.
+ *
+ * @author espen
+ *
+ */
+public class AASExecutable {
+ private static final Logger logger = LoggerFactory.getLogger(AASExecutable.class);
+
+ public static final String HOST = "dashboard-aas";
+ public static final int PORT = 6400;
+ public static final String REGISTRY_ENDPOINT = "http://registry:4000/registry/";
+
+ public static void main(String[] args) throws Exception {
+ // Use docker-compose health check for registry instead
+ Thread.sleep(3000);
+
+ logger.info("Creating AAS...");
+ AssetAdministrationShell aas = new DashboardAssetAdministrationShell();
+ Asset asset = new DashboardAsset();
+ aas.setAsset(asset);
+ SubModel sm = new DashboardSubmodel();
+ aas.setConceptDictionary(new ArrayList<>());
+ aas.addSubModel(sm);
+
+ logger.info("Starting aas servlet...");
+ createServlet(aas, sm);
+ logger.info("Registering aas...");
+ AASDescriptor descriptor = new AASDescriptor(aas, "http://" + HOST + ":" + PORT + "/aas");
+ descriptor.addSubmodelDescriptor(
+ new SubmodelDescriptor(sm, "http://" + HOST + ":" + PORT + "/aas/submodels/DashboardSubmodel/"));
+ registerAAS(descriptor, REGISTRY_ENDPOINT);
+
+ logger.info("Finished");
+ }
+
+ private static void registerAAS(AASDescriptor descriptor, String registryEndpoint) throws InterruptedException {
+ IAASRegistryService registry = new AASRegistryProxy(registryEndpoint);
+ // Quick & dirty, try to register until registry is up
+ for (int i = 0; i < 10; i++) {
+ try {
+ registry.register(descriptor);
+ break;
+ } catch (Exception e) {
+ Thread.sleep(2000);
+ }
+ }
+ }
+
+ private static void createServlet(AssetAdministrationShell aas, SubModel... submodels) {
+ VABMultiSubmodelProvider provider = new VABMultiSubmodelProvider();
+ provider.setAssetAdministrationShell(new AASModelProvider(aas));
+ for (SubModel sm : submodels) {
+ provider.addSubmodel(new SubModelProvider(sm));
+ }
+ BaSyxContextConfiguration config = new BaSyxContextConfiguration();
+ config.loadFromDefaultSource();
+ BaSyxContext context = config.createBaSyxContext();
+ context.addServletMapping("/*", new VABHTTPInterface<IModelProvider>(provider));
+ AASHTTPServer server = new AASHTTPServer(context);
+ server.start();
+ }
+}
diff --git a/examples/basyx.dashboardAAS/src/main/java/org/eclipse/basyx/dashboard/DashboardAsset.java b/examples/basyx.dashboardAAS/src/main/java/org/eclipse/basyx/dashboard/DashboardAsset.java
new file mode 100644
index 0000000..4f9b771
--- /dev/null
+++ b/examples/basyx.dashboardAAS/src/main/java/org/eclipse/basyx/dashboard/DashboardAsset.java
@@ -0,0 +1,19 @@
+package org.eclipse.basyx.dashboard;
+
+import org.eclipse.basyx.aas.metamodel.api.parts.asset.AssetKind;
+import org.eclipse.basyx.aas.metamodel.map.parts.Asset;
+import org.eclipse.basyx.submodel.metamodel.api.identifier.IdentifierType;
+
+/**
+ * A dummy Asset for the dashboar AAS
+ *
+ * @author espen
+ *
+ */
+public class DashboardAsset extends Asset {
+ public DashboardAsset() {
+ setIdShort("DashboardAsset");
+ setIdentification(IdentifierType.CUSTOM, "DashboardAsset");
+ setAssetKind(AssetKind.INSTANCE);
+ }
+}
diff --git a/examples/basyx.dashboardAAS/src/main/java/org/eclipse/basyx/dashboard/DashboardAssetAdministrationShell.java b/examples/basyx.dashboardAAS/src/main/java/org/eclipse/basyx/dashboard/DashboardAssetAdministrationShell.java
new file mode 100644
index 0000000..331b27d
--- /dev/null
+++ b/examples/basyx.dashboardAAS/src/main/java/org/eclipse/basyx/dashboard/DashboardAssetAdministrationShell.java
@@ -0,0 +1,17 @@
+package org.eclipse.basyx.dashboard;
+
+import org.eclipse.basyx.aas.metamodel.map.AssetAdministrationShell;
+import org.eclipse.basyx.submodel.metamodel.api.identifier.IdentifierType;
+
+/**
+ * A minimal AAS header for the dashboard AAS
+ *
+ * @author espen
+ *
+ */
+public class DashboardAssetAdministrationShell extends AssetAdministrationShell {
+ public DashboardAssetAdministrationShell() {
+ setIdShort("DashboardAAS");
+ setIdentification(IdentifierType.CUSTOM, "DashboardAAS");
+ }
+}
diff --git a/examples/basyx.dashboardAAS/src/main/java/org/eclipse/basyx/dashboard/DashboardSubmodel.java b/examples/basyx.dashboardAAS/src/main/java/org/eclipse/basyx/dashboard/DashboardSubmodel.java
new file mode 100644
index 0000000..8d67ace
--- /dev/null
+++ b/examples/basyx.dashboardAAS/src/main/java/org/eclipse/basyx/dashboard/DashboardSubmodel.java
@@ -0,0 +1,61 @@
+package org.eclipse.basyx.dashboard;
+
+import org.eclipse.basyx.submodel.metamodel.api.identifier.IdentifierType;
+import org.eclipse.basyx.submodel.metamodel.api.reference.IReference;
+import org.eclipse.basyx.submodel.metamodel.api.reference.enums.KeyElements;
+import org.eclipse.basyx.submodel.metamodel.api.reference.enums.KeyType;
+import org.eclipse.basyx.submodel.metamodel.map.SubModel;
+import org.eclipse.basyx.submodel.metamodel.map.reference.Key;
+import org.eclipse.basyx.submodel.metamodel.map.reference.Reference;
+import org.eclipse.basyx.submodel.metamodel.map.submodelelement.dataelement.property.Property;
+import org.eclipse.basyx.submodel.metamodel.map.submodelelement.dataelement.property.valuetypedef.PropertyValueTypeDef;
+import org.eclipse.basyx.vab.modelprovider.lambda.VABLambdaProviderHelper;
+
+/**
+ * Dummy Submodel with two dynamic values. A configurable temperature value that randomly
+ * returns values in a specific range. And a random boolean value.
+ *
+ * @author espen
+ *
+ */
+public class DashboardSubmodel extends SubModel {
+ private int minValue;
+ private int maxValue;
+
+ public DashboardSubmodel() {
+ setIdShort("DashboardSubmodel");
+ setIdentification(IdentifierType.CUSTOM, "DashboardTemperatureSubmodel");
+ setSemanticId(new Reference(
+ new Key(KeyElements.CONCEPTDESCRIPTION, true, "0112/2///61360_4#AAF891#001", KeyType.IRDI)));
+ setTemperatureProperty();
+ setDummyProperty();
+
+ DashboardSubmodelConfiguration config = new DashboardSubmodelConfiguration();
+ config.loadFromDefaultSource();
+ minValue = config.getMin();
+ maxValue = config.getMax();
+ }
+
+ private void setTemperatureProperty() {
+ Property temperatureProperty = new Property();
+ temperatureProperty.setIdShort("temperature");
+ temperatureProperty.set(VABLambdaProviderHelper.createSimple(() -> {
+ return Math.random() * (maxValue - minValue) + minValue;
+ }, null), PropertyValueTypeDef.Double);
+ // Adds a reference to a semantic ID to specify the property semantics (see CDD)
+ // Ref by identifier:
+ Key key = new Key(KeyElements.CONCEPTDESCRIPTION, true, "0112/2///61360_4#AAF891#001", KeyType.IRDI);
+ IReference refByIdentifier = new Reference(key);
+ temperatureProperty.setSemanticID(refByIdentifier);
+ addSubModelElement(temperatureProperty);
+ }
+
+ private void setDummyProperty() {
+ Property dummyProperty = new Property();
+ dummyProperty.setIdShort("dummy");
+ dummyProperty.set(VABLambdaProviderHelper.createSimple(() -> {
+ return (Math.random() > 0.5);
+ }, null), PropertyValueTypeDef.Boolean);
+ addSubModelElement(dummyProperty);
+ }
+}
diff --git a/examples/basyx.dashboardAAS/src/main/java/org/eclipse/basyx/dashboard/DashboardSubmodelConfiguration.java b/examples/basyx.dashboardAAS/src/main/java/org/eclipse/basyx/dashboard/DashboardSubmodelConfiguration.java
new file mode 100644
index 0000000..280c242
--- /dev/null
+++ b/examples/basyx.dashboardAAS/src/main/java/org/eclipse/basyx/dashboard/DashboardSubmodelConfiguration.java
@@ -0,0 +1,89 @@
+package org.eclipse.basyx.dashboard;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.basyx.components.configuration.BaSyxConfiguration;
+
+/**
+ * Simple configuration for setting minimal and maxmimal values for the dashboard submodel
+ *
+ * @author espen
+ *
+ */
+public class DashboardSubmodelConfiguration extends BaSyxConfiguration {
+ // Prefix for environment variables
+ public static final String ENV_PREFIX = "BaSyxDashboardSubmodel_";
+
+ // Default BaSyx Context configuration
+ public static final int DEFAULT_MIN = 10;
+ public static final int DEFAULT_MAX = 30;
+
+ public static final String MIN = "Min";
+ public static final String MAX = "Max";
+
+ // The default path for the context properties file
+ public static final String DEFAULT_CONFIG_PATH = "dashboardsubmodel.properties";
+
+ // The default key for variables pointing to the configuration file
+ public static final String DEFAULT_FILE_KEY = "BASYX_DASHBOARDSUBMODEL";
+
+ public static Map<String, String> getDefaultProperties() {
+ Map<String, String> defaultProps = new HashMap<>();
+ defaultProps.put(MIN, Integer.toString(DEFAULT_MIN));
+ defaultProps.put(MAX, Integer.toString(DEFAULT_MAX));
+ return defaultProps;
+ }
+
+ /**
+ * Empty Constructor - use default values
+ */
+ public DashboardSubmodelConfiguration() {
+ super(getDefaultProperties());
+ }
+
+ /**
+ * Constructor with predefined value map
+ */
+ public DashboardSubmodelConfiguration(Map<String, String> values) {
+ super(values);
+ }
+
+ /**
+ * Constructor with initial configuration - docBasePath and hostname are default values
+ *
+ * @param min The minimal temperature value
+ * @param max The maximal temperature value
+ */
+ public DashboardSubmodelConfiguration(int min, int max) {
+ this();
+ setMin(min);
+ setMax(max);
+ }
+
+ public void loadFromEnvironmentVariables() {
+ String[] properties = { MIN, MAX };
+ loadFromEnvironmentVariables(ENV_PREFIX, properties);
+ }
+
+ public void loadFromDefaultSource() {
+ loadFileOrDefaultResource(DEFAULT_FILE_KEY, DEFAULT_CONFIG_PATH);
+ loadFromEnvironmentVariables();
+ }
+
+ public void setMin(int min) {
+ setProperty(MIN, Integer.toString(min));
+ }
+
+ public void setMax(int max) {
+ setProperty(MIN, Integer.toString(max));
+ }
+
+ public int getMin() {
+ return Integer.parseInt(getProperty(MIN));
+ }
+
+ public int getMax() {
+ return Integer.parseInt(getProperty(MAX));
+ }
+}
diff --git a/examples/basyx.dashboardAAS/src/main/resources/context.properties b/examples/basyx.dashboardAAS/src/main/resources/context.properties
new file mode 100644
index 0000000..9e4461a
--- /dev/null
+++ b/examples/basyx.dashboardAAS/src/main/resources/context.properties
@@ -0,0 +1,17 @@
+# ###############################
+# HTTP Context configuration file
+# ###############################
+
+# ###############################
+# Context Path
+# ###############################
+# Specifies the subpath in the url for this server context
+
+contextPath=/
+
+# ###############################
+# Port
+# ###############################
+# Specifies the port for this server context
+
+contextPort=6400
\ No newline at end of file
diff --git a/examples/basyx.dashboardAAS/src/main/resources/dashboardsubmodel.properties b/examples/basyx.dashboardAAS/src/main/resources/dashboardsubmodel.properties
new file mode 100644
index 0000000..63bbaa6
--- /dev/null
+++ b/examples/basyx.dashboardAAS/src/main/resources/dashboardsubmodel.properties
@@ -0,0 +1,17 @@
+# ###############################
+# Dashboard submodel configuration
+# ###############################
+
+# ###############################
+# Min temperature value
+# ###############################
+# Specifies the min value for the dynamic random temperature
+
+Min=10
+
+# ###############################
+# Max temperature value
+# ###############################
+# Specifies the max value for the dynamic random temperature
+
+Max=30
\ No newline at end of file
diff --git a/examples/basyx.dashboardAAS/src/test/resources/.env b/examples/basyx.dashboardAAS/src/test/resources/.env
new file mode 100644
index 0000000..0f1579a
--- /dev/null
+++ b/examples/basyx.dashboardAAS/src/test/resources/.env
@@ -0,0 +1,39 @@
+# ##################
+# Docker Environment
+# ##################
+
+# ##################
+# Host Port
+# ##################
+# Specifies the port for the Docker HOST the container port is mapped to
+
+BASYX_HOST_PORT=6400
+
+# ##################
+# Container Port
+# ##################
+# Specifies the port for the Docker CONTAINER that is be mapped for the host
+
+BASYX_CONTAINER_PORT=6400
+
+# ##################
+# Image Name
+# ##################
+# The image of the image that is build for this component
+
+BASYX_IMAGE_NAME=basyx/dashboard-aas
+
+# ##################
+# Image Tag
+# ##################
+# The image tag of the image that is build for this component
+
+BASYX_IMAGE_TAG=0.1.0-SNAPSHOT
+
+# ##################
+# Container Name
+# ##################
+# The name of the container used for the default environment
+
+BASYX_CONTAINER_NAME=dashboard-aas
+
diff --git a/examples/basyx.dashboardAAS/start.bat b/examples/basyx.dashboardAAS/start.bat
new file mode 100644
index 0000000..7d0dc6c
--- /dev/null
+++ b/examples/basyx.dashboardAAS/start.bat
@@ -0,0 +1 @@
+docker-compose up
\ No newline at end of file
diff --git a/examples/basyx.dashboardAAS/start.sh b/examples/basyx.dashboardAAS/start.sh
new file mode 100644
index 0000000..d935e43
--- /dev/null
+++ b/examples/basyx.dashboardAAS/start.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+docker-compose up
\ No newline at end of file
diff --git a/examples/basyx.dashboardAAS/stop.bat b/examples/basyx.dashboardAAS/stop.bat
new file mode 100644
index 0000000..58694d0
--- /dev/null
+++ b/examples/basyx.dashboardAAS/stop.bat
@@ -0,0 +1 @@
+docker-compose down
\ No newline at end of file
diff --git a/examples/basyx.dashboardAAS/stop.sh b/examples/basyx.dashboardAAS/stop.sh
new file mode 100644
index 0000000..f5139e2
--- /dev/null
+++ b/examples/basyx.dashboardAAS/stop.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+docker-compose down
\ No newline at end of file
diff --git a/examples/basyx.grafana/.gitignore b/examples/basyx.grafana/.gitignore
new file mode 100644
index 0000000..a5271c0
--- /dev/null
+++ b/examples/basyx.grafana/.gitignore
@@ -0,0 +1 @@
+/lib/plugins/*
\ No newline at end of file
diff --git a/examples/basyx.grafana/dashboard.json b/examples/basyx.grafana/dashboard.json
new file mode 100644
index 0000000..624f1a5
--- /dev/null
+++ b/examples/basyx.grafana/dashboard.json
@@ -0,0 +1,110 @@
+{
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": "-- Grafana --",
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "editable": true,
+ "gnetId": null,
+ "graphTooltip": 0,
+ "id": 1,
+ "links": [],
+ "panels": [
+ {
+ "datasource": null,
+ "description": "",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "thresholds"
+ },
+ "custom": {},
+ "mappings": [],
+ "min": 0,
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "#EAB839",
+ "value": 20
+ },
+ {
+ "color": "red",
+ "value": 27
+ }
+ ]
+ },
+ "unit": "celsius"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 6,
+ "x": 0,
+ "y": 0
+ },
+ "id": 2,
+ "options": {
+ "colorMode": "value",
+ "graphMode": "area",
+ "justifyMode": "auto",
+ "orientation": "auto",
+ "reduceOptions": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "fields": "",
+ "values": false
+ },
+ "text": {},
+ "textMode": "auto"
+ },
+ "pluginVersion": "7.4.0",
+ "targets": [
+ {
+ "refId": "A",
+ "target": "temp",
+ "type": "timeserie"
+ }
+ ],
+ "timeFrom": null,
+ "timeShift": null,
+ "title": "Temperature",
+ "transparent": true,
+ "type": "stat"
+ }
+ ],
+ "refresh": "1s",
+ "schemaVersion": 27,
+ "style": "dark",
+ "tags": [],
+ "templating": {
+ "list": []
+ },
+ "time": {
+ "from": "now-10s",
+ "to": "now"
+ },
+ "timepicker": {
+ "refresh_intervals": [
+ "1s"
+ ]
+ },
+ "timezone": "",
+ "title": "BaSyx Temperature",
+ "uid": "forkaVEGz",
+ "version": 5
+}
\ No newline at end of file
diff --git a/examples/basyx.grafana/docker-compose.yml b/examples/basyx.grafana/docker-compose.yml
new file mode 100644
index 0000000..a6a0f87
--- /dev/null
+++ b/examples/basyx.grafana/docker-compose.yml
@@ -0,0 +1,32 @@
+version: '2.1'
+services:
+
+ registry:
+ image: basyx/registry:0.1.0-SNAPSHOT
+ container_name: dashboard-registry
+ ports:
+ - 4000:4000
+
+ dashboard-aas:
+ image: basyx/dashboard-aas:0.1.0-SNAPSHOT
+ container_name: dashboard-aas
+ environment:
+ - BaSyxDashboardSubmodel_Min=15
+# - BaSyxDashboardSubmodel_Max=30
+ ports:
+ - 6400:6400
+
+ aas-wrapper:
+ image: basyx/aas-wrapper:0.1.0-SNAPSHOT
+ container_name: aas-wrapper
+ ports:
+ - 6500:6500
+
+ grafana:
+ image: grafana/grafana:7.4.0
+ container_name: grafana
+ ports:
+ - 3000:3000
+ volumes:
+ - ./lib:/var/lib/grafana
+ - ./grafana.ini:/etc/grafana/grafana.ini
\ No newline at end of file
diff --git a/examples/basyx.grafana/grafana.ini b/examples/basyx.grafana/grafana.ini
new file mode 100644
index 0000000..098e757
--- /dev/null
+++ b/examples/basyx.grafana/grafana.ini
@@ -0,0 +1,902 @@
+##################### Grafana Configuration Defaults #####################
+#
+# Do not modify this file in grafana installs
+#
+
+# possible values : production, development
+app_mode = production
+
+# instance name, defaults to HOSTNAME environment variable value or hostname if HOSTNAME var is empty
+instance_name = ${HOSTNAME}
+
+#################################### Paths ###############################
+[paths]
+# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used)
+data = data
+
+# Temporary files in `data` directory older than given duration will be removed
+temp_data_lifetime = 24h
+
+# Directory where grafana can store logs
+logs = data/log
+
+# Directory where grafana will automatically scan and look for plugins
+plugins = data/plugins
+
+# folder that contains provisioning config files that grafana will apply on startup and while running.
+provisioning = conf/provisioning
+
+#################################### Server ##############################
+[server]
+# Protocol (http, https, h2, socket)
+protocol = http
+
+# The ip address to bind to, empty will bind to all interfaces
+http_addr =
+
+# The http port to use
+http_port = 3000
+
+# The public facing domain name used to access grafana from a browser
+domain = localhost
+
+# Redirect to correct domain if host header does not match domain
+# Prevents DNS rebinding attacks
+enforce_domain = false
+
+# The full public facing url
+root_url = %(protocol)s://%(domain)s:%(http_port)s/
+
+# Serve Grafana from subpath specified in `root_url` setting. By default it is set to `false` for compatibility reasons.
+serve_from_sub_path = false
+
+# Log web requests
+router_logging = false
+
+# the path relative working path
+static_root_path = public
+
+# enable gzip
+enable_gzip = false
+
+# https certs & key file
+cert_file =
+cert_key =
+
+# Unix socket path
+socket = /tmp/grafana.sock
+
+# CDN Url
+cdn_url =
+
+#################################### Database ############################
+[database]
+# You can configure the database connection by specifying type, host, name, user and password
+# as separate properties or as on string using the url property.
+
+# Either "mysql", "postgres" or "sqlite3", it's your choice
+type = sqlite3
+host = 127.0.0.1:3306
+name = grafana
+user = root
+# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
+password =
+# Use either URL or the previous fields to configure the database
+# Example: mysql://user:secret@host:port/database
+url =
+
+# Max idle conn setting default is 2
+max_idle_conn = 2
+
+# Max conn setting default is 0 (mean not set)
+max_open_conn =
+
+# Connection Max Lifetime default is 14400 (means 14400 seconds or 4 hours)
+conn_max_lifetime = 14400
+
+# Set to true to log the sql calls and execution times.
+log_queries =
+
+# For "postgres", use either "disable", "require" or "verify-full"
+# For "mysql", use either "true", "false", or "skip-verify".
+ssl_mode = disable
+
+ca_cert_path =
+client_key_path =
+client_cert_path =
+server_cert_name =
+
+# For "sqlite3" only, path relative to data_path setting
+path = grafana.db
+
+# For "sqlite3" only. cache mode setting used for connecting to the database
+cache_mode = private
+
+#################################### Cache server #############################
+[remote_cache]
+# Either "redis", "memcached" or "database" default is "database"
+type = database
+
+# cache connectionstring options
+# database: will use Grafana primary database.
+# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=0,ssl=false`. Only addr is required. ssl may be 'true', 'false', or 'insecure'.
+# memcache: 127.0.0.1:11211
+connstr =
+
+#################################### Data proxy ###########################
+[dataproxy]
+
+# This enables data proxy logging, default is false
+logging = false
+
+# How long the data proxy waits before timing out, default is 30 seconds.
+# This setting also applies to core backend HTTP data sources where query requests use an HTTP client with timeout set.
+timeout = 30
+
+# How many seconds the data proxy waits before sending a keepalive request.
+keep_alive_seconds = 30
+
+# How many seconds the data proxy waits for a successful TLS Handshake before timing out.
+tls_handshake_timeout_seconds = 10
+
+# How many seconds the data proxy will wait for a server's first response headers after
+# fully writing the request headers if the request has an "Expect: 100-continue"
+# header. A value of 0 will result in the body being sent immediately, without
+# waiting for the server to approve.
+expect_continue_timeout_seconds = 1
+
+# The maximum number of idle connections that Grafana will keep alive.
+max_idle_connections = 100
+
+# How many seconds the data proxy keeps an idle connection open before timing out.
+idle_conn_timeout_seconds = 90
+
+# If enabled and user is not anonymous, data proxy will add X-Grafana-User header with username into the request.
+send_user_header = false
+
+#################################### Analytics ###########################
+[analytics]
+# Server reporting, sends usage counters to stats.grafana.org every 24 hours.
+# No ip addresses are being tracked, only simple counters to track
+# running instances, dashboard and error counts. It is very helpful to us.
+# Change this option to false to disable reporting.
+reporting_enabled = true
+
+# Set to false to disable all checks to https://grafana.com
+# for new versions (grafana itself and plugins), check is used
+# in some UI views to notify that grafana or plugin update exists
+# This option does not cause any auto updates, nor send any information
+# only a GET request to https://grafana.com to get latest versions
+check_for_updates = true
+
+# Google Analytics universal tracking code, only enabled if you specify an id here
+google_analytics_ua_id =
+
+# Google Tag Manager ID, only enabled if you specify an id here
+google_tag_manager_id =
+
+#################################### Security ############################
+[security]
+# disable creation of admin user on first start of grafana
+disable_initial_admin_creation = false
+
+# default admin user, created on startup
+admin_user = admin
+
+# default admin password, can be changed before first start of grafana, or in profile settings
+admin_password = admin
+
+# used for signing
+secret_key = SW2YcwTIb9zpOOhoPsMm
+
+# disable gravatar profile images
+disable_gravatar = false
+
+# data source proxy whitelist (ip_or_domain:port separated by spaces)
+data_source_proxy_whitelist =
+
+# disable protection against brute force login attempts
+disable_brute_force_login_protection = false
+
+# set to true if you host Grafana behind HTTPS. default is false.
+cookie_secure = false
+
+# set cookie SameSite attribute. defaults to `lax`. can be set to "lax", "strict", "none" and "disabled"
+cookie_samesite = lax
+
+# set to true if you want to allow browsers to render Grafana in a <frame>, <iframe>, <embed> or <object>. default is false.
+allow_embedding = false
+
+# Set to true if you want to enable http strict transport security (HSTS) response header.
+# This is only sent when HTTPS is enabled in this configuration.
+# HSTS tells browsers that the site should only be accessed using HTTPS.
+strict_transport_security = false
+
+# Sets how long a browser should cache HSTS. Only applied if strict_transport_security is enabled.
+strict_transport_security_max_age_seconds = 86400
+
+# Set to true if to enable HSTS preloading option. Only applied if strict_transport_security is enabled.
+strict_transport_security_preload = false
+
+# Set to true if to enable the HSTS includeSubDomains option. Only applied if strict_transport_security is enabled.
+strict_transport_security_subdomains = false
+
+# Set to true to enable the X-Content-Type-Options response header.
+# The X-Content-Type-Options response HTTP header is a marker used by the server to indicate that the MIME types advertised
+# in the Content-Type headers should not be changed and be followed.
+x_content_type_options = true
+
+# Set to true to enable the X-XSS-Protection header, which tells browsers to stop pages from loading
+# when they detect reflected cross-site scripting (XSS) attacks.
+x_xss_protection = true
+
+# Enable adding the Content-Security-Policy header to your requests.
+# CSP allows to control resources the user agent is allowed to load and helps prevent XSS attacks.
+content_security_policy = false
+
+# Set Content Security Policy template used when adding the Content-Security-Policy header to your requests.
+# $NONCE in the template includes a random nonce.
+content_security_policy_template = """script-src 'unsafe-eval' 'strict-dynamic' $NONCE;object-src 'none';font-src 'self';style-src 'self' 'unsafe-inline';img-src 'self' data:;base-uri 'self';connect-src 'self' grafana.com;manifest-src 'self';media-src 'none';form-action 'self';"""
+
+#################################### Snapshots ###########################
+[snapshots]
+# snapshot sharing options
+external_enabled = true
+external_snapshot_url = https://snapshots-origin.raintank.io
+external_snapshot_name = Publish to snapshot.raintank.io
+
+# Set to true to enable this Grafana instance act as an external snapshot server and allow unauthenticated requests for
+# creating and deleting snapshots.
+public_mode = false
+
+# remove expired snapshot
+snapshot_remove_expired = true
+
+#################################### Dashboards ##################
+
+[dashboards]
+# Number dashboard versions to keep (per dashboard). Default: 20, Minimum: 1
+versions_to_keep = 20
+
+# Minimum dashboard refresh interval. When set, this will restrict users to set the refresh interval of a dashboard lower than given interval. Per default this is 5 seconds.
+# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
+min_refresh_interval = 1s
+
+# Path to the default home dashboard. If this value is empty, then Grafana uses StaticRootPath + "dashboards/home.json"
+default_home_dashboard_path =
+
+################################### Data sources #########################
+[datasources]
+# Upper limit of data sources that Grafana will return. This limit is a temporary configuration and it will be deprecated when pagination will be introduced on the list data sources API.
+datasource_limit = 5000
+
+#################################### Users ###############################
+[users]
+# disable user signup / registration
+allow_sign_up = false
+
+# Allow non admin users to create organizations
+allow_org_create = false
+
+# Set to true to automatically assign new users to the default organization (id 1)
+auto_assign_org = true
+
+# Set this value to automatically add new users to the provided organization (if auto_assign_org above is set to true)
+auto_assign_org_id = 1
+
+# Default role new users will be automatically assigned (if auto_assign_org above is set to true)
+auto_assign_org_role = Viewer
+
+# Require email validation before sign up completes
+verify_email_enabled = false
+
+# Background text for the user field on the login page
+login_hint = email or username
+password_hint = password
+
+# Default UI theme ("dark" or "light")
+default_theme = dark
+
+# External user management
+external_manage_link_url =
+external_manage_link_name =
+external_manage_info =
+
+# Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard.
+viewers_can_edit = false
+
+# Editors can administrate dashboard, folders and teams they create
+editors_can_admin = false
+
+# The duration in time a user invitation remains valid before expiring. This setting should be expressed as a duration. Examples: 6h (hours), 2d (days), 1w (week). Default is 24h (24 hours). The minimum supported duration is 15m (15 minutes).
+user_invite_max_lifetime_duration = 24h
+
+# Enter a comma-separated list of usernames to hide them in the Grafana UI. These users are shown to Grafana admins and to themselves.
+hidden_users =
+
+[auth]
+# Login cookie name
+login_cookie_name = grafana_session
+
+# The maximum lifetime (duration) an authenticated user can be inactive before being required to login at next visit. Default is 7 days (7d). This setting should be expressed as a duration, e.g. 5m (minutes), 6h (hours), 10d (days), 2w (weeks), 1M (month). The lifetime resets at each successful token rotation (token_rotation_interval_minutes).
+login_maximum_inactive_lifetime_duration =
+
+# The maximum lifetime (duration) an authenticated user can be logged in since login time before being required to login. Default is 30 days (30d). This setting should be expressed as a duration, e.g. 5m (minutes), 6h (hours), 10d (days), 2w (weeks), 1M (month).
+login_maximum_lifetime_duration =
+
+# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes.
+token_rotation_interval_minutes = 10
+
+# Set to true to disable (hide) the login form, useful if you use OAuth
+disable_login_form = false
+
+# Set to true to disable the signout link in the side menu. useful if you use auth.proxy
+disable_signout_menu = false
+
+# URL to redirect the user to after sign out
+signout_redirect_url =
+
+# Set to true to attempt login with OAuth automatically, skipping the login screen.
+# This setting is ignored if multiple OAuth providers are configured.
+oauth_auto_login = false
+
+# OAuth state max age cookie duration in seconds. Defaults to 600 seconds.
+oauth_state_cookie_max_age = 600
+
+# limit of api_key seconds to live before expiration
+api_key_max_seconds_to_live = -1
+
+# Set to true to enable SigV4 authentication option for HTTP-based datasources
+sigv4_auth_enabled = false
+
+#################################### Anonymous Auth ######################
+[auth.anonymous]
+# enable anonymous access
+enabled = false
+
+# specify organization name that should be used for unauthenticated users
+org_name = Main Org.
+
+# specify role for unauthenticated users
+org_role = Viewer
+
+# mask the Grafana version number for unauthenticated users
+hide_version = false
+
+#################################### GitHub Auth #########################
+[auth.github]
+enabled = false
+allow_sign_up = true
+client_id = some_id
+client_secret =
+scopes = user:email,read:org
+auth_url = https://github.com/login/oauth/authorize
+token_url = https://github.com/login/oauth/access_token
+api_url = https://api.github.com/user
+allowed_domains =
+team_ids =
+allowed_organizations =
+
+#################################### GitLab Auth #########################
+[auth.gitlab]
+enabled = false
+allow_sign_up = true
+client_id = some_id
+client_secret =
+scopes = api
+auth_url = https://gitlab.com/oauth/authorize
+token_url = https://gitlab.com/oauth/token
+api_url = https://gitlab.com/api/v4
+allowed_domains =
+allowed_groups =
+
+#################################### Google Auth #########################
+[auth.google]
+enabled = false
+allow_sign_up = true
+client_id = some_client_id
+client_secret =
+scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email
+auth_url = https://accounts.google.com/o/oauth2/auth
+token_url = https://accounts.google.com/o/oauth2/token
+api_url = https://www.googleapis.com/oauth2/v1/userinfo
+allowed_domains =
+hosted_domain =
+
+#################################### Grafana.com Auth ####################
+# legacy key names (so they work in env variables)
+[auth.grafananet]
+enabled = false
+allow_sign_up = true
+client_id = some_id
+client_secret =
+scopes = user:email
+allowed_organizations =
+
+[auth.grafana_com]
+enabled = false
+allow_sign_up = true
+client_id = some_id
+client_secret =
+scopes = user:email
+allowed_organizations =
+
+#################################### Azure AD OAuth #######################
+[auth.azuread]
+name = Azure AD
+enabled = false
+allow_sign_up = true
+client_id = some_client_id
+client_secret =
+scopes = openid email profile
+auth_url = https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize
+token_url = https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
+allowed_domains =
+allowed_groups =
+
+#################################### Okta OAuth #######################
+[auth.okta]
+name = Okta
+enabled = false
+allow_sign_up = true
+client_id = some_id
+client_secret =
+scopes = openid profile email groups
+auth_url = https://<tenant-id>.okta.com/oauth2/v1/authorize
+token_url = https://<tenant-id>.okta.com/oauth2/v1/token
+api_url = https://<tenant-id>.okta.com/oauth2/v1/userinfo
+allowed_domains =
+allowed_groups =
+role_attribute_path =
+
+#################################### Generic OAuth #######################
+[auth.generic_oauth]
+name = OAuth
+enabled = false
+allow_sign_up = true
+client_id = some_id
+client_secret =
+scopes = user:email
+email_attribute_name = email:primary
+email_attribute_path =
+login_attribute_path =
+name_attribute_path =
+role_attribute_path =
+id_token_attribute_name =
+auth_url =
+token_url =
+api_url =
+allowed_domains =
+team_ids =
+allowed_organizations =
+tls_skip_verify_insecure = false
+tls_client_cert =
+tls_client_key =
+tls_client_ca =
+
+#################################### Basic Auth ##########################
+[auth.basic]
+enabled = true
+
+#################################### Auth Proxy ##########################
+[auth.proxy]
+enabled = false
+header_name = X-WEBAUTH-USER
+header_property = username
+auto_sign_up = true
+# Deprecated, use sync_ttl instead
+ldap_sync_ttl = 60
+sync_ttl = 60
+whitelist =
+headers =
+enable_login_token = false
+
+#################################### Auth LDAP ###########################
+[auth.ldap]
+enabled = false
+config_file = /etc/grafana/ldap.toml
+allow_sign_up = true
+
+# LDAP background sync (Enterprise only)
+# At 1 am every day
+sync_cron = "0 0 1 * * *"
+active_sync_enabled = true
+
+#################################### SMTP / Emailing #####################
+[smtp]
+enabled = false
+host = localhost:25
+user =
+# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
+password =
+cert_file =
+key_file =
+skip_verify = false
+from_address = admin@grafana.localhost
+from_name = Grafana
+ehlo_identity =
+startTLS_policy =
+
+[emails]
+welcome_email_on_sign_up = false
+templates_pattern = emails/*.html
+
+#################################### Logging ##########################
+[log]
+# Either "console", "file", "syslog". Default is console and file
+# Use space to separate multiple modes, e.g. "console file"
+mode = console file
+
+# Either "debug", "info", "warn", "error", "critical", default is "info"
+level = info
+
+# optional settings to set different levels for specific loggers. Ex filters = sqlstore:debug
+filters =
+
+# For "console" mode only
+[log.console]
+level =
+
+# log line format, valid options are text, console and json
+format = console
+
+# For "file" mode only
+[log.file]
+level =
+
+# log line format, valid options are text, console and json
+format = text
+
+# This enables automated log rotate(switch of following options), default is true
+log_rotate = true
+
+# Max line number of single file, default is 1000000
+max_lines = 1000000
+
+# Max size shift of single file, default is 28 means 1 << 28, 256MB
+max_size_shift = 28
+
+# Segment log daily, default is true
+daily_rotate = true
+
+# Expired days of log file(delete after max days), default is 7
+max_days = 7
+
+[log.syslog]
+level =
+
+# log line format, valid options are text, console and json
+format = text
+
+# Syslog network type and address. This can be udp, tcp, or unix. If left blank, the default unix endpoints will be used.
+network =
+address =
+
+# Syslog facility. user, daemon and local0 through local7 are valid.
+facility =
+
+# Syslog tag. By default, the process' argv[0] is used.
+tag =
+
+[log.frontend]
+# Should Sentry javascript agent be initialized
+enabled = false
+
+# Sentry DSN if you want to send events to Sentry.
+sentry_dsn =
+
+# Custom HTTP endpoint to send events captured by the Sentry agent to. Default will log the events to stdout.
+custom_endpoint = /log
+
+# Rate of events to be reported between 0 (none) and 1 (all), float
+sample_rate = 1.0
+
+# Requests per second limit enforced per an extended period, for Grafana backend log ingestion endpoint (/log).
+log_endpoint_requests_per_second_limit = 3
+
+# Max requests accepted per short interval of time for Grafana backend log ingestion endpoint (/log)
+log_endpoint_burst_limit = 15
+
+#################################### Usage Quotas ########################
+[quota]
+enabled = false
+
+#### set quotas to -1 to make unlimited. ####
+# limit number of users per Org.
+org_user = 10
+
+# limit number of dashboards per Org.
+org_dashboard = 100
+
+# limit number of data_sources per Org.
+org_data_source = 10
+
+# limit number of api_keys per Org.
+org_api_key = 10
+
+# limit number of orgs a user can create.
+user_org = 10
+
+# Global limit of users.
+global_user = -1
+
+# global limit of orgs.
+global_org = -1
+
+# global limit of dashboards
+global_dashboard = -1
+
+# global limit of api_keys
+global_api_key = -1
+
+# global limit on number of logged in users.
+global_session = -1
+
+#################################### Alerting ############################
+[alerting]
+# Disable alerting engine & UI features
+enabled = true
+# Makes it possible to turn off alert rule execution but alerting UI is visible
+execute_alerts = true
+
+# Default setting for new alert rules. Defaults to categorize error and timeouts as alerting. (alerting, keep_state)
+error_or_timeout = alerting
+
+# Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok)
+nodata_or_nullvalues = no_data
+
+# Alert notifications can include images, but rendering many images at the same time can overload the server
+# This limit will protect the server from render overloading and make sure notifications are sent out quickly
+concurrent_render_limit = 5
+
+# Default setting for alert calculation timeout. Default value is 30
+evaluation_timeout_seconds = 30
+
+# Default setting for alert notification timeout. Default value is 30
+notification_timeout_seconds = 30
+
+# Default setting for max attempts to sending alert notifications. Default value is 3
+max_attempts = 3
+
+# Makes it possible to enforce a minimal interval between evaluations, to reduce load on the backend
+min_interval_seconds = 1
+
+# Configures for how long alert annotations are stored. Default is 0, which keeps them forever.
+# This setting should be expressed as an duration. Ex 6h (hours), 10d (days), 2w (weeks), 1M (month).
+max_annotation_age =
+
+# Configures max number of alert annotations that Grafana stores. Default value is 0, which keeps all alert annotations.
+max_annotations_to_keep =
+
+#################################### Annotations #########################
+
+[annotations.dashboard]
+# Dashboard annotations means that annotations are associated with the dashboard they are created on.
+
+# Configures how long dashboard annotations are stored. Default is 0, which keeps them forever.
+# This setting should be expressed as a duration. Examples: 6h (hours), 10d (days), 2w (weeks), 1M (month).
+max_age =
+
+# Configures max number of dashboard annotations that Grafana stores. Default value is 0, which keeps all dashboard annotations.
+max_annotations_to_keep =
+
+[annotations.api]
+# API annotations means that the annotations have been created using the API without any
+# association with a dashboard.
+
+# Configures how long Grafana stores API annotations. Default is 0, which keeps them forever.
+# This setting should be expressed as a duration. Examples: 6h (hours), 10d (days), 2w (weeks), 1M (month).
+max_age =
+
+# Configures max number of API annotations that Grafana keeps. Default value is 0, which keeps all API annotations.
+max_annotations_to_keep =
+
+#################################### Explore #############################
+[explore]
+# Enable the Explore section
+enabled = true
+
+#################################### Internal Grafana Metrics ############
+# Metrics available at HTTP API Url /metrics
+[metrics]
+enabled = true
+interval_seconds = 10
+# Disable total stats (stat_totals_*) metrics to be generated
+disable_total_stats = false
+
+#If both are set, basic auth will be required for the metrics endpoint.
+basic_auth_username =
+basic_auth_password =
+
+# Metrics environment info adds dimensions to the `grafana_environment_info` metric, which
+# can expose more information about the Grafana instance.
+[metrics.environment_info]
+#exampleLabel1 = exampleValue1
+#exampleLabel2 = exampleValue2
+
+# Send internal Grafana metrics to graphite
+[metrics.graphite]
+# Enable by setting the address setting (ex localhost:2003)
+address =
+prefix = prod.grafana.%(instance_name)s.
+
+#################################### Grafana.com integration ##########################
+[grafana_net]
+url = https://grafana.com
+
+[grafana_com]
+url = https://grafana.com
+
+#################################### Distributed tracing ############
+[tracing.jaeger]
+# jaeger destination (ex localhost:6831)
+address =
+# tag that will always be included in when creating new spans. ex (tag1:value1,tag2:value2)
+always_included_tag =
+# Type specifies the type of the sampler: const, probabilistic, rateLimiting, or remote
+sampler_type = const
+# jaeger samplerconfig param
+# for "const" sampler, 0 or 1 for always false/true respectively
+# for "probabilistic" sampler, a probability between 0 and 1
+# for "rateLimiting" sampler, the number of spans per second
+# for "remote" sampler, param is the same as for "probabilistic"
+# and indicates the initial sampling rate before the actual one
+# is received from the mothership
+sampler_param = 1
+# sampling_server_url is the URL of a sampling manager providing a sampling strategy.
+sampling_server_url =
+# Whether or not to use Zipkin span propagation (x-b3- HTTP headers).
+zipkin_propagation = false
+# Setting this to true disables shared RPC spans.
+# Not disabling is the most common setting when using Zipkin elsewhere in your infrastructure.
+disable_shared_zipkin_spans = false
+
+#################################### External Image Storage ##############
+[external_image_storage]
+# Used for uploading images to public servers so they can be included in slack/email messages.
+# You can choose between (s3, webdav, gcs, azure_blob, local)
+provider =
+
+[external_image_storage.s3]
+endpoint =
+path_style_access =
+bucket_url =
+bucket =
+region =
+path =
+access_key =
+secret_key =
+
+[external_image_storage.webdav]
+url =
+username =
+password =
+public_url =
+
+[external_image_storage.gcs]
+key_file =
+bucket =
+path =
+enable_signed_urls = false
+signed_url_expiration =
+
+[external_image_storage.azure_blob]
+account_name =
+account_key =
+container_name =
+
+[external_image_storage.local]
+# does not require any configuration
+
+[rendering]
+# Options to configure a remote HTTP image rendering service, e.g. using https://github.com/grafana/grafana-image-renderer.
+# URL to a remote HTTP image renderer service, e.g. http://localhost:8081/render, will enable Grafana to render panels and dashboards to PNG-images using HTTP requests to an external service.
+server_url =
+# If the remote HTTP image renderer service runs on a different server than the Grafana server you may have to configure this to a URL where Grafana is reachable, e.g. http://grafana.domain/.
+callback_url =
+# Concurrent render request limit affects when the /render HTTP endpoint is used. Rendering many images at the same time can overload the server,
+# which this setting can help protect against by only allowing a certain amount of concurrent requests.
+concurrent_render_request_limit = 30
+
+[panels]
+# here for to support old env variables, can remove after a few months
+enable_alpha = false
+disable_sanitize_html = false
+
+[plugins]
+enable_alpha = false
+app_tls_skip_verify_insecure = false
+# Enter a comma-separated list of plugin identifiers to identify plugins that are allowed to be loaded even if they lack a valid signature.
+allow_loading_unsigned_plugins =
+marketplace_url = https://grafana.com/grafana/plugins/
+
+#################################### Grafana Image Renderer Plugin ##########################
+[plugin.grafana-image-renderer]
+# Instruct headless browser instance to use a default timezone when not provided by Grafana, e.g. when rendering panel image of alert.
+# See ICU’s metaZones.txt (https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt) for a list of supported
+# timezone IDs. Fallbacks to TZ environment variable if not set.
+rendering_timezone =
+
+# Instruct headless browser instance to use a default language when not provided by Grafana, e.g. when rendering panel image of alert.
+# Please refer to the HTTP header Accept-Language to understand how to format this value, e.g. 'fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5'.
+rendering_language =
+
+# Instruct headless browser instance to use a default device scale factor when not provided by Grafana, e.g. when rendering panel image of alert.
+# Default is 1. Using a higher value will produce more detailed images (higher DPI), but will require more disk space to store an image.
+rendering_viewport_device_scale_factor =
+
+# Instruct headless browser instance whether to ignore HTTPS errors during navigation. Per default HTTPS errors are not ignored. Due to
+# the security risk it's not recommended to ignore HTTPS errors.
+rendering_ignore_https_errors =
+
+# Instruct headless browser instance whether to capture and log verbose information when rendering an image. Default is false and will
+# only capture and log error messages. When enabled, debug messages are captured and logged as well.
+# For the verbose information to be included in the Grafana server log you have to adjust the rendering log level to debug, configure
+# [log].filter = rendering:debug.
+rendering_verbose_logging =
+
+# Instruct headless browser instance whether to output its debug and error messages into running process of remote rendering service.
+# Default is false. This can be useful to enable (true) when troubleshooting.
+rendering_dumpio =
+
+# Additional arguments to pass to the headless browser instance. Default is --no-sandbox. The list of Chromium flags can be found
+# here (https://peter.sh/experiments/chromium-command-line-switches/). Multiple arguments is separated with comma-character.
+rendering_args =
+
+# You can configure the plugin to use a different browser binary instead of the pre-packaged version of Chromium.
+# Please note that this is not recommended, since you may encounter problems if the installed version of Chrome/Chromium is not
+# compatible with the plugin.
+rendering_chrome_bin =
+
+# Instruct how headless browser instances are created. Default is 'default' and will create a new browser instance on each request.
+# Mode 'clustered' will make sure that only a maximum of browsers/incognito pages can execute concurrently.
+# Mode 'reusable' will have one browser instance and will create a new incognito page on each request.
+rendering_mode =
+
+# When rendering_mode = clustered you can instruct how many browsers or incognito pages can execute concurrently. Default is 'browser'
+# and will cluster using browser instances.
+# Mode 'context' will cluster using incognito pages.
+rendering_clustering_mode =
+# When rendering_mode = clustered you can define maximum number of browser instances/incognito pages that can execute concurrently..
+rendering_clustering_max_concurrency =
+
+# Limit the maximum viewport width, height and device scale factor that can be requested.
+rendering_viewport_max_width =
+rendering_viewport_max_height =
+rendering_viewport_max_device_scale_factor =
+
+# Change the listening host and port of the gRPC server. Default host is 127.0.0.1 and default port is 0 and will automatically assign
+# a port not in use.
+grpc_host =
+grpc_port =
+
+[enterprise]
+license_path =
+
+[feature_toggles]
+# enable features, separated by spaces
+enable =
+
+[date_formats]
+# For information on what formatting patterns that are supported https://momentjs.com/docs/#/displaying/
+
+# Default system date format used in time range picker and other places where full time is displayed
+full_date = YYYY-MM-DD HH:mm:ss
+
+# Used by graph and other places where we only show small intervals
+interval_second = HH:mm:ss
+interval_minute = HH:mm
+interval_hour = MM/DD HH:mm
+interval_day = MM/DD
+interval_month = YYYY-MM
+interval_year = YYYY
+
+# Experimental feature
+use_browser_locale = false
+
+# Default timezone for user preferences. Options are 'browser' for the browser local timezone or a timezone name from IANA Time Zone database, e.g. 'UTC' or 'Europe/Amsterdam' etc.
+default_timezone = browser
+
+[expressions]
+# Enable or disable the expressions functionality.
+enabled = true
diff --git a/examples/basyx.grafana/lib/grafana.db b/examples/basyx.grafana/lib/grafana.db
new file mode 100644
index 0000000..9304575
--- /dev/null
+++ b/examples/basyx.grafana/lib/grafana.db
Binary files differ
diff --git a/examples/basyx.grafana/readme.txt b/examples/basyx.grafana/readme.txt
new file mode 100644
index 0000000..cd5669e
--- /dev/null
+++ b/examples/basyx.grafana/readme.txt
@@ -0,0 +1,28 @@
+HowTo: First Setup
+------------------
+
+1. Download SimpleJSON datasource from Grafana
+- Download .zip-file at
+ https://grafana.com/grafana/plugins/grafana-simple-json-datasource/installation
+- Unzip it in /lib/plugins:
+ /lib/plugins/grafana-simple-json-datasource/ should directly contain its files (e.g. package.json)
+
+2. Start docker-compose
+- Run "docker-compose up" in the /grafana/ folder
+
+3. Login (admin/admin)
+- http://localhost:3000/
+
+4. Add datasource
+- Configuration -> Datasources -> Add datasource -> Others: SimpleJson
+
+5. Set URL Configuration in SimpleJson Datasource:
+URL -> http://aas-wrapper:6500/grafana/
+
+6. Import Dashboard
+- Dashboards -> Manage -> Import -> Upload dashboard.json
+
+7. Open BaSyx Dashboard
+- Dashboards -> Manage -> BaSyx Temperature
+- Optional: Set auto-refresh to 1s
+
diff --git a/examples/basyx.grafana/start.bat b/examples/basyx.grafana/start.bat
new file mode 100644
index 0000000..7d0dc6c
--- /dev/null
+++ b/examples/basyx.grafana/start.bat
@@ -0,0 +1 @@
+docker-compose up
\ No newline at end of file
diff --git a/examples/basyx.grafana/start.sh b/examples/basyx.grafana/start.sh
new file mode 100644
index 0000000..d935e43
--- /dev/null
+++ b/examples/basyx.grafana/start.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+docker-compose up
\ No newline at end of file
diff --git a/examples/basyx.grafana/stop.bat b/examples/basyx.grafana/stop.bat
new file mode 100644
index 0000000..58694d0
--- /dev/null
+++ b/examples/basyx.grafana/stop.bat
@@ -0,0 +1 @@
+docker-compose down
\ No newline at end of file
diff --git a/examples/basyx.grafana/stop.sh b/examples/basyx.grafana/stop.sh
new file mode 100644
index 0000000..f5139e2
--- /dev/null
+++ b/examples/basyx.grafana/stop.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+docker-compose down
\ No newline at end of file
diff --git a/examples/basyx.nodered/.gitignore b/examples/basyx.nodered/.gitignore
new file mode 100644
index 0000000..a52c20a
--- /dev/null
+++ b/examples/basyx.nodered/.gitignore
@@ -0,0 +1 @@
+/mosquitto/mosquitto.log
\ No newline at end of file
diff --git a/examples/basyx.nodered/Dockerfile b/examples/basyx.nodered/Dockerfile
new file mode 100644
index 0000000..4ccce8f
--- /dev/null
+++ b/examples/basyx.nodered/Dockerfile
@@ -0,0 +1,17 @@
+FROM nodered/node-red
+USER root
+
+# Copy package.json to the WORKDIR so npm builds all
+# of your added nodes modules for Node-RED
+COPY package.json .
+COPY ./node-red-contrib-aas-connect ./node-red-contrib-aas-connect
+RUN npm install --unsafe-perm --no-update-notifier --no-fund --only=production --quiet
+RUN npm install ./node-red-contrib-aas-connect
+
+# Copy _your_ Node-RED project files into place
+# NOTE: This will only work if you DO NOT later mount /data as an external volume.
+# If you need to use an external volume for persistence then
+# copy your settings and flows files to that volume instead.
+COPY settings.js /data/settings.js
+# COPY flows_cred.json /data/flows_cred.json
+COPY flows.json /data/flows.json
\ No newline at end of file
diff --git a/examples/basyx.nodered/build.bat b/examples/basyx.nodered/build.bat
new file mode 100644
index 0000000..6e3726f
--- /dev/null
+++ b/examples/basyx.nodered/build.bat
@@ -0,0 +1 @@
+docker build -t basyx/node-red:0.1.0-SNAPSHOT .
\ No newline at end of file
diff --git a/examples/basyx.nodered/build.sh b/examples/basyx.nodered/build.sh
new file mode 100644
index 0000000..ce0a3c0
--- /dev/null
+++ b/examples/basyx.nodered/build.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+docker build -t basyx/node-red:0.1.0-SNAPSHOT .
\ No newline at end of file
diff --git a/examples/basyx.nodered/docker-compose.yml b/examples/basyx.nodered/docker-compose.yml
new file mode 100644
index 0000000..eade085
--- /dev/null
+++ b/examples/basyx.nodered/docker-compose.yml
@@ -0,0 +1,41 @@
+version: '3'
+services:
+
+ registry:
+ image: basyx/registry:0.1.0-SNAPSHOT
+ container_name: dashboard-registry
+ ports:
+ - 4000:4000
+
+ dashboard-aas:
+ image: basyx/dashboard-aas:0.1.0-SNAPSHOT
+ container_name: dashboard-aas
+ environment:
+ - BaSyxDashboardSubmodel_Min=15
+# - BaSyxDashboardSubmodel_Max=30
+ ports:
+ - 6400:6400
+
+ aas-wrapper:
+ image: basyx/aas-wrapper:0.1.0-SNAPSHOT
+ container_name: aas-wrapper
+ ports:
+ - 6500:6500
+
+ node-red:
+ image: basyx/node-red:0.1.0
+ container_name: node-red
+ ports:
+ - 1880:1880
+ volumes:
+ - ./node-red:/data
+
+ mosquitto:
+ image: eclipse-mosquitto:latest
+ container_name: mosquitto
+ ports:
+ - 1883:1883
+ - 9001:9001
+ volumes:
+ - ./mosquitto/config:/mosquitto/config
+ - ./mosquitto/log:/mosquitto/log
\ No newline at end of file
diff --git a/examples/basyx.nodered/flows.json b/examples/basyx.nodered/flows.json
new file mode 100644
index 0000000..0e22486
--- /dev/null
+++ b/examples/basyx.nodered/flows.json
@@ -0,0 +1 @@
+[{"id":"ac89e902.a48598","type":"tab","label":"AAS Temperature","disabled":false,"info":""},{"id":"91383a96.5b2e48","type":"tab","label":"Test","disabled":false,"info":""},{"id":"1b4daff8.52358","type":"mqtt-broker","z":"","name":"Streamsheets Mosquitto","broker":"http://streamsheets","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"2289cf21.0a21b","type":"inject","z":"ac89e902.a48598","name":"Start","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":110,"y":120,"wires":[["c3659321.46995"]]},{"id":"c3659321.46995","type":"get-aas-property","z":"ac89e902.a48598","name":"Get Temperature","property":"temp","period":1,"x":270,"y":120,"wires":[["a56bd8e1.1777c8","2c13d59c.208cea","8c41e115.ac913"]]},{"id":"4e72f0db.c2a7a","type":"inject","z":"91383a96.5b2e48","name":"Data","props":[{"p":"values","v":"[22.344,23.434,24.342,21.098]","vt":"json"},{"p":"timestamp","v":"[121323,2112231,321331,321321]","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":140,"y":100,"wires":[["a53d7930.6109f8","4a734a5f.b22314","ab2d648a.1767f8"]]},{"id":"a53d7930.6109f8","type":"debug","z":"91383a96.5b2e48","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":250,"y":240,"wires":[]},{"id":"4a734a5f.b22314","type":"function","z":"91383a96.5b2e48","name":"Calculate Average","func":"msg.avg = msg.values.reduce((a,b) => a + b, 0) / msg.values.length;\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":450,"y":120,"wires":[["6b28c27b.5547bc"]]},{"id":"6b28c27b.5547bc","type":"debug","z":"91383a96.5b2e48","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":710,"y":120,"wires":[]},{"id":"ab2d648a.1767f8","type":"function","z":"91383a96.5b2e48","name":"Calculate Fahrenheit","func":"msg.fahrenheit = msg.values.map(v => v * 1.8 + 32);\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":460,"y":180,"wires":[["5e614070.a223f"]]},{"id":"5e614070.a223f","type":"debug","z":"91383a96.5b2e48","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":710,"y":180,"wires":[]},{"id":"e733d05.63a9c3","type":"iot-datasource","z":"ac89e902.a48598","name":"Temperature Datasource","tstampField":"tstamp","dataField":"data","disableDiscover":false,"x":730,"y":240,"wires":[[]]},{"id":"8e627da8.65b74","type":"mqtt out","z":"ac89e902.a48598","name":"MQTT Avg Temp Publisher","topic":"temperature/average","qos":"","retain":"","broker":"1b4daff8.52358","x":740,"y":120,"wires":[]},{"id":"7aa9e937.6575c8","type":"mqtt in","z":"ac89e902.a48598","name":"MQTT Avg Temp Consumer","topic":"temperature/average","qos":"2","datatype":"json","broker":"1b4daff8.52358","x":160,"y":360,"wires":[["f96c72b.3be659"]]},{"id":"f96c72b.3be659","type":"debug","z":"ac89e902.a48598","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload.average","targetType":"msg","statusVal":"","statusType":"auto","x":420,"y":360,"wires":[]},{"id":"a56bd8e1.1777c8","type":"function","z":"ac89e902.a48598","name":"Calculate Average","func":"const payload = msg.payload;\nlet average = payload.data.reduce((a,b) => a + b, 0) / payload.data.length;\npayload.average = average;\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":490,"y":120,"wires":[["8e627da8.65b74"]]},{"id":"2c13d59c.208cea","type":"function","z":"ac89e902.a48598","name":"Calculate Fahrenheit","func":"const payload = msg.payload;\nlet fahrenheit = payload.data.map(v => v * 1.8 + 32);\npayload.fahrenheit = fahrenheit;\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":500,"y":180,"wires":[["a3733cf8.97468"]]},{"id":"a3733cf8.97468","type":"mqtt out","z":"ac89e902.a48598","name":"MQTT Fahrenheit Temp Publisher","topic":"temperature/fahrenheit","qos":"","retain":"","broker":"1b4daff8.52358","x":760,"y":180,"wires":[]},{"id":"8c41e115.ac913","type":"function","z":"ac89e902.a48598","name":"Get most recent","func":"msg.payload.tstamp = new Date(msg.payload.tstamp.pop()).getTime();\nmsg.payload.data = msg.payload.data.pop();\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":480,"y":240,"wires":[["e733d05.63a9c3"]]}]
\ No newline at end of file
diff --git a/examples/basyx.nodered/mosquitto/config/mosquitto.conf b/examples/basyx.nodered/mosquitto/config/mosquitto.conf
new file mode 100644
index 0000000..9bf5429
--- /dev/null
+++ b/examples/basyx.nodered/mosquitto/config/mosquitto.conf
@@ -0,0 +1,988 @@
+# Config file for mosquitto
+#
+# See mosquitto.conf(5) for more information.
+#
+# Default values are shown, uncomment to change.
+#
+# Use the # character to indicate a comment, but only if it is the
+# very first character on the line.
+
+# =================================================================
+# General configuration
+# =================================================================
+
+# Use per listener security settings.
+#
+# It is recommended this option be set before any other options.
+#
+# If this option is set to true, then all authentication and access control
+# options are controlled on a per listener basis. The following options are
+# affected:
+#
+# password_file acl_file psk_file auth_plugin auth_opt_* allow_anonymous
+# auto_id_prefix allow_zero_length_clientid
+#
+# Note that if set to true, then a durable client (i.e. with clean session set
+# to false) that has disconnected will use the ACL settings defined for the
+# listener that it was most recently connected to.
+#
+# The default behaviour is for this to be set to false, which maintains the
+# setting behaviour from previous versions of mosquitto.
+#per_listener_settings false
+
+
+# If a client is subscribed to multiple subscriptions that overlap, e.g. foo/#
+# and foo/+/baz , then MQTT expects that when the broker receives a message on
+# a topic that matches both subscriptions, such as foo/bar/baz, then the client
+# should only receive the message once.
+# Mosquitto keeps track of which clients a message has been sent to in order to
+# meet this requirement. The allow_duplicate_messages option allows this
+# behaviour to be disabled, which may be useful if you have a large number of
+# clients subscribed to the same set of topics and are very concerned about
+# minimising memory usage.
+# It can be safely set to true if you know in advance that your clients will
+# never have overlapping subscriptions, otherwise your clients must be able to
+# correctly deal with duplicate messages even when then have QoS=2.
+#allow_duplicate_messages false
+
+# This option controls whether a client is allowed to connect with a zero
+# length client id or not. This option only affects clients using MQTT v3.1.1
+# and later. If set to false, clients connecting with a zero length client id
+# are disconnected. If set to true, clients will be allocated a client id by
+# the broker. This means it is only useful for clients with clean session set
+# to true.
+#allow_zero_length_clientid true
+
+# If allow_zero_length_clientid is true, this option allows you to set a prefix
+# to automatically generated client ids to aid visibility in logs.
+# Defaults to 'auto-'
+#auto_id_prefix auto-
+
+# This option affects the scenario when a client subscribes to a topic that has
+# retained messages. It is possible that the client that published the retained
+# message to the topic had access at the time they published, but that access
+# has been subsequently removed. If check_retain_source is set to true, the
+# default, the source of a retained message will be checked for access rights
+# before it is republished. When set to false, no check will be made and the
+# retained message will always be published. This affects all listeners.
+#check_retain_source true
+
+# QoS 1 and 2 messages will be allowed inflight per client until this limit
+# is exceeded. Defaults to 0. (No maximum)
+# See also max_inflight_messages
+#max_inflight_bytes 0
+
+# The maximum number of QoS 1 and 2 messages currently inflight per
+# client.
+# This includes messages that are partway through handshakes and
+# those that are being retried. Defaults to 20. Set to 0 for no
+# maximum. Setting to 1 will guarantee in-order delivery of QoS 1
+# and 2 messages.
+#max_inflight_messages 20
+
+# For MQTT v5 clients, it is possible to have the server send a "server
+# keepalive" value that will override the keepalive value set by the client.
+# This is intended to be used as a mechanism to say that the server will
+# disconnect the client earlier than it anticipated, and that the client should
+# use the new keepalive value. The max_keepalive option allows you to specify
+# that clients may only connect with keepalive less than or equal to this
+# value, otherwise they will be sent a server keepalive telling them to use
+# max_keepalive. This only applies to MQTT v5 clients. The maximum value
+# allowable is 65535. Do not set below 10.
+#max_keepalive 65535
+
+# For MQTT v5 clients, it is possible to have the server send a "maximum packet
+# size" value that will instruct the client it will not accept MQTT packets
+# with size greater than max_packet_size bytes. This applies to the full MQTT
+# packet, not just the payload. Setting this option to a positive value will
+# set the maximum packet size to that number of bytes. If a client sends a
+# packet which is larger than this value, it will be disconnected. This applies
+# to all clients regardless of the protocol version they are using, but v3.1.1
+# and earlier clients will of course not have received the maximum packet size
+# information. Defaults to no limit. Setting below 20 bytes is forbidden
+# because it is likely to interfere with ordinary client operation, even with
+# very small payloads.
+#max_packet_size 0
+
+# QoS 1 and 2 messages above those currently in-flight will be queued per
+# client until this limit is exceeded. Defaults to 0. (No maximum)
+# See also max_queued_messages.
+# If both max_queued_messages and max_queued_bytes are specified, packets will
+# be queued until the first limit is reached.
+#max_queued_bytes 0
+
+# The maximum number of QoS 1 and 2 messages to hold in a queue per client
+# above those that are currently in-flight. Defaults to 100. Set
+# to 0 for no maximum (not recommended).
+# See also queue_qos0_messages.
+# See also max_queued_bytes.
+#max_queued_messages 100
+#
+# This option sets the maximum number of heap memory bytes that the broker will
+# allocate, and hence sets a hard limit on memory use by the broker. Memory
+# requests that exceed this value will be denied. The effect will vary
+# depending on what has been denied. If an incoming message is being processed,
+# then the message will be dropped and the publishing client will be
+# disconnected. If an outgoing message is being sent, then the individual
+# message will be dropped and the receiving client will be disconnected.
+# Defaults to no limit.
+#memory_limit 0
+
+# This option sets the maximum publish payload size that the broker will allow.
+# Received messages that exceed this size will not be accepted by the broker.
+# The default value is 0, which means that all valid MQTT messages are
+# accepted. MQTT imposes a maximum payload size of 268435455 bytes.
+#message_size_limit 0
+
+# This option allows persistent clients (those with clean session set to false)
+# to be removed if they do not reconnect within a certain time frame.
+#
+# This is a non-standard option in MQTT V3.1 but allowed in MQTT v3.1.1.
+#
+# Badly designed clients may set clean session to false whilst using a randomly
+# generated client id. This leads to persistent clients that will never
+# reconnect. This option allows these clients to be removed.
+#
+# The expiration period should be an integer followed by one of h d w m y for
+# hour, day, week, month and year respectively. For example
+#
+# persistent_client_expiration 2m
+# persistent_client_expiration 14d
+# persistent_client_expiration 1y
+#
+# The default if not set is to never expire persistent clients.
+#persistent_client_expiration
+
+# Write process id to a file. Default is a blank string which means
+# a pid file shouldn't be written.
+# This should be set to /var/run/mosquitto.pid if mosquitto is
+# being run automatically on boot with an init script and
+# start-stop-daemon or similar.
+#pid_file
+
+# Set to true to queue messages with QoS 0 when a persistent client is
+# disconnected. These messages are included in the limit imposed by
+# max_queued_messages and max_queued_bytes
+# Defaults to false.
+# This is a non-standard option for the MQTT v3.1 spec but is allowed in
+# v3.1.1.
+#queue_qos0_messages false
+
+# Set to false to disable retained message support. If a client publishes a
+# message with the retain bit set, it will be disconnected if this is set to
+# false.
+#retain_available true
+
+# Disable Nagle's algorithm on client sockets. This has the effect of reducing
+# latency of individual messages at the potential cost of increasing the number
+# of packets being sent.
+#set_tcp_nodelay false
+
+# Time in seconds between updates of the $SYS tree.
+# Set to 0 to disable the publishing of the $SYS tree.
+#sys_interval 10
+
+# The MQTT specification requires that the QoS of a message delivered to a
+# subscriber is never upgraded to match the QoS of the subscription. Enabling
+# this option changes this behaviour. If upgrade_outgoing_qos is set true,
+# messages sent to a subscriber will always match the QoS of its subscription.
+# This is a non-standard option explicitly disallowed by the spec.
+#upgrade_outgoing_qos false
+
+# When run as root, drop privileges to this user and its primary
+# group.
+# Set to root to stay as root, but this is not recommended.
+# If run as a non-root user, this setting has no effect.
+# Note that on Windows this has no effect and so mosquitto should
+# be started by the user you wish it to run as.
+#user mosquitto
+
+# =================================================================
+# Default listener
+# =================================================================
+
+# IP address/hostname to bind the default listener to. If not
+# given, the default listener will not be bound to a specific
+# address and so will be accessible to all network interfaces.
+# bind_address ip-address/host name
+#bind_address
+
+# Port to use for the default listener.
+#port 1883
+
+# Bind the listener to a specific interface. This is similar to
+# bind_address above but is useful when an interface has multiple addresses or
+# the address may change. It is valid to use this with the bind_address option,
+# but take care that the interface you are binding to contains the address you
+# are binding to, otherwise you will not be able to connect.
+# Example: bind_interface eth0
+#bind_interface
+
+# When a listener is using the websockets protocol, it is possible to serve
+# http data as well. Set http_dir to a directory which contains the files you
+# wish to serve. If this option is not specified, then no normal http
+# connections will be possible.
+#http_dir
+
+# The maximum number of client connections to allow. This is
+# a per listener setting.
+# Default is -1, which means unlimited connections.
+# Note that other process limits mean that unlimited connections
+# are not really possible. Typically the default maximum number of
+# connections possible is around 1024.
+#max_connections -1
+
+# Choose the protocol to use when listening.
+# This can be either mqtt or websockets.
+# Websockets support is currently disabled by default at compile time.
+# Certificate based TLS may be used with websockets, except that
+# only the cafile, certfile, keyfile and ciphers options are supported.
+#protocol mqtt
+
+# Set use_username_as_clientid to true to replace the clientid that a client
+# connected with with its username. This allows authentication to be tied to
+# the clientid, which means that it is possible to prevent one client
+# disconnecting another by using the same clientid.
+# If a client connects with no username it will be disconnected as not
+# authorised when this option is set to true.
+# Do not use in conjunction with clientid_prefixes.
+# See also use_identity_as_username.
+#use_username_as_clientid
+
+# -----------------------------------------------------------------
+# Certificate based SSL/TLS support
+# -----------------------------------------------------------------
+# The following options can be used to enable SSL/TLS support for
+# this listener. Note that the recommended port for MQTT over TLS
+# is 8883, but this must be set manually.
+#
+# See also the mosquitto-tls man page.
+
+# At least one of cafile or capath must be defined. They both
+# define methods of accessing the PEM encoded Certificate
+# Authority certificates that have signed your server certificate
+# and that you wish to trust.
+# cafile defines the path to a file containing the CA certificates.
+# capath defines a directory that will be searched for files
+# containing the CA certificates. For capath to work correctly, the
+# certificate files must have ".crt" as the file ending and you must run
+# "openssl rehash <path to capath>" each time you add/remove a certificate.
+#cafile
+#capath
+
+# Path to the PEM encoded server certificate.
+#certfile
+
+# Path to the PEM encoded keyfile.
+#keyfile
+
+
+# If you have require_certificate set to true, you can create a certificate
+# revocation list file to revoke access to particular client certificates. If
+# you have done this, use crlfile to point to the PEM encoded revocation file.
+#crlfile
+
+# If you wish to control which encryption ciphers are used, use the ciphers
+# option. The list of available ciphers can be obtained using the "openssl
+# ciphers" command and should be provided in the same format as the output of
+# that command.
+# If unset defaults to DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2:@STRENGTH
+#ciphers DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2:@STRENGTH
+
+# To allow the use of ephemeral DH key exchange, which provides forward
+# security, the listener must load DH parameters. This can be specified with
+# the dhparamfile option. The dhparamfile can be generated with the command
+# e.g. "openssl dhparam -out dhparam.pem 2048"
+#dhparamfile
+
+# By default a TLS enabled listener will operate in a similar fashion to a
+# https enabled web server, in that the server has a certificate signed by a CA
+# and the client will verify that it is a trusted certificate. The overall aim
+# is encryption of the network traffic. By setting require_certificate to true,
+# the client must provide a valid certificate in order for the network
+# connection to proceed. This allows access to the broker to be controlled
+# outside of the mechanisms provided by MQTT.
+#require_certificate false
+
+# This option defines the version of the TLS protocol to use for this listener.
+# The default value allows all of v1.3, v1.2 and v1.1. The valid values are
+# tlsv1.3 tlsv1.2 and tlsv1.1.
+#tls_version
+
+# If require_certificate is true, you may set use_identity_as_username to true
+# to use the CN value from the client certificate as a username. If this is
+# true, the password_file option will not be used for this listener.
+# This takes priority over use_subject_as_username.
+# See also use_subject_as_username.
+#use_identity_as_username false
+
+# If require_certificate is true, you may set use_subject_as_username to true
+# to use the complete subject value from the client certificate as a username.
+# If this is true, the password_file option will not be used for this listener.
+# See also use_identity_as_username
+#use_subject_as_username false
+
+# -----------------------------------------------------------------
+# Pre-shared-key based SSL/TLS support
+# -----------------------------------------------------------------
+# The following options can be used to enable PSK based SSL/TLS support for
+# this listener. Note that the recommended port for MQTT over TLS is 8883, but
+# this must be set manually.
+#
+# See also the mosquitto-tls man page and the "Certificate based SSL/TLS
+# support" section. Only one of certificate or PSK encryption support can be
+# enabled for any listener.
+
+# The psk_hint option enables pre-shared-key support for this listener and also
+# acts as an identifier for this listener. The hint is sent to clients and may
+# be used locally to aid authentication. The hint is a free form string that
+# doesn't have much meaning in itself, so feel free to be creative.
+# If this option is provided, see psk_file to define the pre-shared keys to be
+# used or create a security plugin to handle them.
+#psk_hint
+
+# When using PSK, the encryption ciphers used will be chosen from the list of
+# available PSK ciphers. If you want to control which ciphers are available,
+# use the "ciphers" option. The list of available ciphers can be obtained
+# using the "openssl ciphers" command and should be provided in the same format
+# as the output of that command.
+#ciphers
+
+# Set use_identity_as_username to have the psk identity sent by the client used
+# as its username. Authentication will be carried out using the PSK rather than
+# the MQTT username/password and so password_file will not be used for this
+# listener.
+#use_identity_as_username false
+
+
+# =================================================================
+# Extra listeners
+# =================================================================
+
+# Listen on a port/ip address combination. By using this variable
+# multiple times, mosquitto can listen on more than one port. If
+# this variable is used and neither bind_address nor port given,
+# then the default listener will not be started.
+# The port number to listen on must be given. Optionally, an ip
+# address or host name may be supplied as a second argument. In
+# this case, mosquitto will attempt to bind the listener to that
+# address and so restrict access to the associated network and
+# interface. By default, mosquitto will listen on all interfaces.
+# Note that for a websockets listener it is not possible to bind to a host
+# name.
+# listener port-number [ip address/host name]
+#listener
+
+# Bind the listener to a specific interface. This is similar to
+# the [ip address/host name] part of the listener definition, but is useful
+# when an interface has multiple addresses or the address may change. It is
+# valid to use this with the [ip address/host name] part of the listener
+# definition, but take care that the interface you are binding to contains the
+# address you are binding to, otherwise you will not be able to connect.
+# Only available on Linux and requires elevated privileges.
+#
+# Example: bind_interface eth0
+#bind_interface
+
+# When a listener is using the websockets protocol, it is possible to serve
+# http data as well. Set http_dir to a directory which contains the files you
+# wish to serve. If this option is not specified, then no normal http
+# connections will be possible.
+#http_dir
+
+# The maximum number of client connections to allow. This is
+# a per listener setting.
+# Default is -1, which means unlimited connections.
+# Note that other process limits mean that unlimited connections
+# are not really possible. Typically the default maximum number of
+# connections possible is around 1024.
+#max_connections -1
+
+# The listener can be restricted to operating within a topic hierarchy using
+# the mount_point option. This is achieved be prefixing the mount_point string
+# to all topics for any clients connected to this listener. This prefixing only
+# happens internally to the broker; the client will not see the prefix.
+#mount_point
+
+# Choose the protocol to use when listening.
+# This can be either mqtt or websockets.
+# Certificate based TLS may be used with websockets, except that only the
+# cafile, certfile, keyfile and ciphers options are supported.
+#protocol mqtt
+
+# Set use_username_as_clientid to true to replace the clientid that a client
+# connected with with its username. This allows authentication to be tied to
+# the clientid, which means that it is possible to prevent one client
+# disconnecting another by using the same clientid.
+# If a client connects with no username it will be disconnected as not
+# authorised when this option is set to true.
+# Do not use in conjunction with clientid_prefixes.
+# See also use_identity_as_username.
+#use_username_as_clientid
+
+# Change the websockets headers size. This is a global option, it is not
+# possible to set per listener. This option sets the size of the buffer used in
+# the libwebsockets library when reading HTTP headers. If you are passing large
+# header data such as cookies then you may need to increase this value. If left
+# unset, or set to 0, then the default of 1024 bytes will be used.
+#websockets_headers_size
+
+# -----------------------------------------------------------------
+# Certificate based SSL/TLS support
+# -----------------------------------------------------------------
+# The following options can be used to enable certificate based SSL/TLS support
+# for this listener. Note that the recommended port for MQTT over TLS is 8883,
+# but this must be set manually.
+#
+# See also the mosquitto-tls man page and the "Pre-shared-key based SSL/TLS
+# support" section. Only one of certificate or PSK encryption support can be
+# enabled for any listener.
+
+# At least one of cafile or capath must be defined to enable certificate based
+# TLS encryption. They both define methods of accessing the PEM encoded
+# Certificate Authority certificates that have signed your server certificate
+# and that you wish to trust.
+# cafile defines the path to a file containing the CA certificates.
+# capath defines a directory that will be searched for files
+# containing the CA certificates. For capath to work correctly, the
+# certificate files must have ".crt" as the file ending and you must run
+# "openssl rehash <path to capath>" each time you add/remove a certificate.
+#cafile
+#capath
+
+# Path to the PEM encoded server certificate.
+#certfile
+
+# Path to the PEM encoded keyfile.
+#keyfile
+
+
+# If you wish to control which encryption ciphers are used, use the ciphers
+# option. The list of available ciphers can be optained using the "openssl
+# ciphers" command and should be provided in the same format as the output of
+# that command.
+#ciphers
+
+# If you have require_certificate set to true, you can create a certificate
+# revocation list file to revoke access to particular client certificates. If
+# you have done this, use crlfile to point to the PEM encoded revocation file.
+#crlfile
+
+# To allow the use of ephemeral DH key exchange, which provides forward
+# security, the listener must load DH parameters. This can be specified with
+# the dhparamfile option. The dhparamfile can be generated with the command
+# e.g. "openssl dhparam -out dhparam.pem 2048"
+#dhparamfile
+
+# By default an TLS enabled listener will operate in a similar fashion to a
+# https enabled web server, in that the server has a certificate signed by a CA
+# and the client will verify that it is a trusted certificate. The overall aim
+# is encryption of the network traffic. By setting require_certificate to true,
+# the client must provide a valid certificate in order for the network
+# connection to proceed. This allows access to the broker to be controlled
+# outside of the mechanisms provided by MQTT.
+#require_certificate false
+
+# If require_certificate is true, you may set use_identity_as_username to true
+# to use the CN value from the client certificate as a username. If this is
+# true, the password_file option will not be used for this listener.
+#use_identity_as_username false
+
+# -----------------------------------------------------------------
+# Pre-shared-key based SSL/TLS support
+# -----------------------------------------------------------------
+# The following options can be used to enable PSK based SSL/TLS support for
+# this listener. Note that the recommended port for MQTT over TLS is 8883, but
+# this must be set manually.
+#
+# See also the mosquitto-tls man page and the "Certificate based SSL/TLS
+# support" section. Only one of certificate or PSK encryption support can be
+# enabled for any listener.
+
+# The psk_hint option enables pre-shared-key support for this listener and also
+# acts as an identifier for this listener. The hint is sent to clients and may
+# be used locally to aid authentication. The hint is a free form string that
+# doesn't have much meaning in itself, so feel free to be creative.
+# If this option is provided, see psk_file to define the pre-shared keys to be
+# used or create a security plugin to handle them.
+#psk_hint
+
+# When using PSK, the encryption ciphers used will be chosen from the list of
+# available PSK ciphers. If you want to control which ciphers are available,
+# use the "ciphers" option. The list of available ciphers can be optained
+# using the "openssl ciphers" command and should be provided in the same format
+# as the output of that command.
+#ciphers
+
+# Set use_identity_as_username to have the psk identity sent by the client used
+# as its username. Authentication will be carried out using the PSK rather than
+# the MQTT username/password and so password_file will not be used for this
+# listener.
+#use_identity_as_username false
+
+
+# =================================================================
+# Persistence
+# =================================================================
+
+# If persistence is enabled, save the in-memory database to disk
+# every autosave_interval seconds. If set to 0, the persistence
+# database will only be written when mosquitto exits. See also
+# autosave_on_changes.
+# Note that writing of the persistence database can be forced by
+# sending mosquitto a SIGUSR1 signal.
+#autosave_interval 1800
+
+# If true, mosquitto will count the number of subscription changes, retained
+# messages received and queued messages and if the total exceeds
+# autosave_interval then the in-memory database will be saved to disk.
+# If false, mosquitto will save the in-memory database to disk by treating
+# autosave_interval as a time in seconds.
+#autosave_on_changes false
+
+# Save persistent message data to disk (true/false).
+# This saves information about all messages, including
+# subscriptions, currently in-flight messages and retained
+# messages.
+# retained_persistence is a synonym for this option.
+#persistence false
+
+# The filename to use for the persistent database, not including
+# the path.
+#persistence_file mosquitto.db
+
+# Location for persistent database. Must include trailing /
+# Default is an empty string (current directory).
+# Set to e.g. /var/lib/mosquitto/ if running as a proper service on Linux or
+# similar.
+#persistence_location
+
+
+# =================================================================
+# Logging
+# =================================================================
+
+# Places to log to. Use multiple log_dest lines for multiple
+# logging destinations.
+# Possible destinations are: stdout stderr syslog topic file
+#
+# stdout and stderr log to the console on the named output.
+#
+# syslog uses the userspace syslog facility which usually ends up
+# in /var/log/messages or similar.
+#
+# topic logs to the broker topic '$SYS/broker/log/<severity>',
+# where severity is one of D, E, W, N, I, M which are debug, error,
+# warning, notice, information and message. Message type severity is used by
+# the subscribe/unsubscribe log_types and publishes log messages to
+# $SYS/broker/log/M/susbcribe or $SYS/broker/log/M/unsubscribe.
+#
+# The file destination requires an additional parameter which is the file to be
+# logged to, e.g. "log_dest file /var/log/mosquitto.log". The file will be
+# closed and reopened when the broker receives a HUP signal. Only a single file
+# destination may be configured.
+#
+# Note that if the broker is running as a Windows service it will default to
+# "log_dest none" and neither stdout nor stderr logging is available.
+# Use "log_dest none" if you wish to disable logging.
+log_dest file /mosquitto/log/mosquitto.log
+
+# Types of messages to log. Use multiple log_type lines for logging
+# multiple types of messages.
+# Possible types are: debug, error, warning, notice, information,
+# none, subscribe, unsubscribe, websockets, all.
+# Note that debug type messages are for decoding the incoming/outgoing
+# network packets. They are not logged in "topics".
+#log_type error
+#log_type warning
+#log_type notice
+#log_type information
+
+
+# If set to true, client connection and disconnection messages will be included
+# in the log.
+#connection_messages true
+
+# If using syslog logging (not on Windows), messages will be logged to the
+# "daemon" facility by default. Use the log_facility option to choose which of
+# local0 to local7 to log to instead. The option value should be an integer
+# value, e.g. "log_facility 5" to use local5.
+#log_facility
+
+# If set to true, add a timestamp value to each log message.
+#log_timestamp true
+
+# Set the format of the log timestamp. If left unset, this is the number of
+# seconds since the Unix epoch.
+# This is a free text string which will be passed to the strftime function. To
+# get an ISO 8601 datetime, for example:
+# log_timestamp_format %Y-%m-%dT%H:%M:%S
+#log_timestamp_format
+
+# Change the websockets logging level. This is a global option, it is not
+# possible to set per listener. This is an integer that is interpreted by
+# libwebsockets as a bit mask for its lws_log_levels enum. See the
+# libwebsockets documentation for more details. "log_type websockets" must also
+# be enabled.
+#websockets_log_level 0
+
+
+# =================================================================
+# Security
+# =================================================================
+
+# If set, only clients that have a matching prefix on their
+# clientid will be allowed to connect to the broker. By default,
+# all clients may connect.
+# For example, setting "secure-" here would mean a client "secure-
+# client" could connect but another with clientid "mqtt" couldn't.
+#clientid_prefixes
+
+# Boolean value that determines whether clients that connect
+# without providing a username are allowed to connect. If set to
+# false then a password file should be created (see the
+# password_file option) to control authenticated client access.
+#
+# Defaults to true if no other security options are set. If `password_file` or
+# `psk_file` is set, or if an authentication plugin is loaded which implements
+# username/password or TLS-PSK checks, then `allow_anonymous` defaults to
+# false.
+#
+#allow_anonymous true
+
+# -----------------------------------------------------------------
+# Default authentication and topic access control
+# -----------------------------------------------------------------
+
+# Control access to the broker using a password file. This file can be
+# generated using the mosquitto_passwd utility. If TLS support is not compiled
+# into mosquitto (it is recommended that TLS support should be included) then
+# plain text passwords are used, in which case the file should be a text file
+# with lines in the format:
+# username:password
+# The password (and colon) may be omitted if desired, although this
+# offers very little in the way of security.
+#
+# See the TLS client require_certificate and use_identity_as_username options
+# for alternative authentication options. If an auth_plugin is used as well as
+# password_file, the auth_plugin check will be made first.
+#password_file
+
+# Access may also be controlled using a pre-shared-key file. This requires
+# TLS-PSK support and a listener configured to use it. The file should be text
+# lines in the format:
+# identity:key
+# The key should be in hexadecimal format without a leading "0x".
+# If an auth_plugin is used as well, the auth_plugin check will be made first.
+#psk_file
+
+# Control access to topics on the broker using an access control list
+# file. If this parameter is defined then only the topics listed will
+# have access.
+# If the first character of a line of the ACL file is a # it is treated as a
+# comment.
+# Topic access is added with lines of the format:
+#
+# topic [read|write|readwrite] <topic>
+#
+# The access type is controlled using "read", "write" or "readwrite". This
+# parameter is optional (unless <topic> contains a space character) - if not
+# given then the access is read/write. <topic> can contain the + or #
+# wildcards as in subscriptions.
+#
+# The first set of topics are applied to anonymous clients, assuming
+# allow_anonymous is true. User specific topic ACLs are added after a
+# user line as follows:
+#
+# user <username>
+#
+# The username referred to here is the same as in password_file. It is
+# not the clientid.
+#
+#
+# If is also possible to define ACLs based on pattern substitution within the
+# topic. The patterns available for substition are:
+#
+# %c to match the client id of the client
+# %u to match the username of the client
+#
+# The substitution pattern must be the only text for that level of hierarchy.
+#
+# The form is the same as for the topic keyword, but using pattern as the
+# keyword.
+# Pattern ACLs apply to all users even if the "user" keyword has previously
+# been given.
+#
+# If using bridges with usernames and ACLs, connection messages can be allowed
+# with the following pattern:
+# pattern write $SYS/broker/connection/%c/state
+#
+# pattern [read|write|readwrite] <topic>
+#
+# Example:
+#
+# pattern write sensor/%u/data
+#
+# If an auth_plugin is used as well as acl_file, the auth_plugin check will be
+# made first.
+#acl_file
+
+# -----------------------------------------------------------------
+# External authentication and topic access plugin options
+# -----------------------------------------------------------------
+
+# External authentication and access control can be supported with the
+# auth_plugin option. This is a path to a loadable plugin. See also the
+# auth_opt_* options described below.
+#
+# The auth_plugin option can be specified multiple times to load multiple
+# plugins. The plugins will be processed in the order that they are specified
+# here. If the auth_plugin option is specified alongside either of
+# password_file or acl_file then the plugin checks will be made first.
+#
+#auth_plugin
+
+# If the auth_plugin option above is used, define options to pass to the
+# plugin here as described by the plugin instructions. All options named
+# using the format auth_opt_* will be passed to the plugin, for example:
+#
+# auth_opt_db_host
+# auth_opt_db_port
+# auth_opt_db_username
+# auth_opt_db_password
+
+
+# =================================================================
+# Bridges
+# =================================================================
+
+# A bridge is a way of connecting multiple MQTT brokers together.
+# Create a new bridge using the "connection" option as described below. Set
+# options for the bridges using the remaining parameters. You must specify the
+# address and at least one topic to subscribe to.
+#
+# Each connection must have a unique name.
+#
+# The address line may have multiple host address and ports specified. See
+# below in the round_robin description for more details on bridge behaviour if
+# multiple addresses are used. Note that if you use an IPv6 address, then you
+# are required to specify a port.
+#
+# The direction that the topic will be shared can be chosen by
+# specifying out, in or both, where the default value is out.
+# The QoS level of the bridged communication can be specified with the next
+# topic option. The default QoS level is 0, to change the QoS the topic
+# direction must also be given.
+#
+# The local and remote prefix options allow a topic to be remapped when it is
+# bridged to/from the remote broker. This provides the ability to place a topic
+# tree in an appropriate location.
+#
+# For more details see the mosquitto.conf man page.
+#
+# Multiple topics can be specified per connection, but be careful
+# not to create any loops.
+#
+# If you are using bridges with cleansession set to false (the default), then
+# you may get unexpected behaviour from incoming topics if you change what
+# topics you are subscribing to. This is because the remote broker keeps the
+# subscription for the old topic. If you have this problem, connect your bridge
+# with cleansession set to true, then reconnect with cleansession set to false
+# as normal.
+#connection <name>
+#address <host>[:<port>] [<host>[:<port>]]
+#topic <topic> [[[out | in | both] qos-level] local-prefix remote-prefix]
+
+
+# If a bridge has topics that have "out" direction, the default behaviour is to
+# send an unsubscribe request to the remote broker on that topic. This means
+# that changing a topic direction from "in" to "out" will not keep receiving
+# incoming messages. Sending these unsubscribe requests is not always
+# desirable, setting bridge_attempt_unsubscribe to false will disable sending
+# the unsubscribe request.
+#bridge_attempt_unsubscribe true
+
+# Set the version of the MQTT protocol to use with for this bridge. Can be one
+# of mqttv311 or mqttv11. Defaults to mqttv311.
+#bridge_protocol_version mqttv311
+
+# Set the clean session variable for this bridge.
+# When set to true, when the bridge disconnects for any reason, all
+# messages and subscriptions will be cleaned up on the remote
+# broker. Note that with cleansession set to true, there may be a
+# significant amount of retained messages sent when the bridge
+# reconnects after losing its connection.
+# When set to false, the subscriptions and messages are kept on the
+# remote broker, and delivered when the bridge reconnects.
+#cleansession false
+
+# Set the amount of time a bridge using the lazy start type must be idle before
+# it will be stopped. Defaults to 60 seconds.
+#idle_timeout 60
+
+# Set the keepalive interval for this bridge connection, in
+# seconds.
+#keepalive_interval 60
+
+# Set the clientid to use on the local broker. If not defined, this defaults to
+# 'local.<clientid>'. If you are bridging a broker to itself, it is important
+# that local_clientid and clientid do not match.
+#local_clientid
+
+# If set to true, publish notification messages to the local and remote brokers
+# giving information about the state of the bridge connection. Retained
+# messages are published to the topic $SYS/broker/connection/<clientid>/state
+# unless the notification_topic option is used.
+# If the message is 1 then the connection is active, or 0 if the connection has
+# failed.
+# This uses the last will and testament feature.
+#notifications true
+
+# Choose the topic on which notification messages for this bridge are
+# published. If not set, messages are published on the topic
+# $SYS/broker/connection/<clientid>/state
+#notification_topic
+
+# Set the client id to use on the remote end of this bridge connection. If not
+# defined, this defaults to 'name.hostname' where name is the connection name
+# and hostname is the hostname of this computer.
+# This replaces the old "clientid" option to avoid confusion. "clientid"
+# remains valid for the time being.
+#remote_clientid
+
+# Set the password to use when connecting to a broker that requires
+# authentication. This option is only used if remote_username is also set.
+# This replaces the old "password" option to avoid confusion. "password"
+# remains valid for the time being.
+#remote_password
+
+# Set the username to use when connecting to a broker that requires
+# authentication.
+# This replaces the old "username" option to avoid confusion. "username"
+# remains valid for the time being.
+#remote_username
+
+# Set the amount of time a bridge using the automatic start type will wait
+# until attempting to reconnect.
+# This option can be configured to use a constant delay time in seconds, or to
+# use a backoff mechanism based on "Decorrelated Jitter", which adds a degree
+# of randomness to when the restart occurs.
+#
+# Set a constant timeout of 20 seconds:
+# restart_timeout 20
+#
+# Set backoff with a base (start value) of 10 seconds and a cap (upper limit) of
+# 60 seconds:
+# restart_timeout 10 30
+#
+# Defaults to jitter with a base of 5 and cap of 30
+#restart_timeout 5 30
+
+# If the bridge has more than one address given in the address/addresses
+# configuration, the round_robin option defines the behaviour of the bridge on
+# a failure of the bridge connection. If round_robin is false, the default
+# value, then the first address is treated as the main bridge connection. If
+# the connection fails, the other secondary addresses will be attempted in
+# turn. Whilst connected to a secondary bridge, the bridge will periodically
+# attempt to reconnect to the main bridge until successful.
+# If round_robin is true, then all addresses are treated as equals. If a
+# connection fails, the next address will be tried and if successful will
+# remain connected until it fails
+#round_robin false
+
+# Set the start type of the bridge. This controls how the bridge starts and
+# can be one of three types: automatic, lazy and once. Note that RSMB provides
+# a fourth start type "manual" which isn't currently supported by mosquitto.
+#
+# "automatic" is the default start type and means that the bridge connection
+# will be started automatically when the broker starts and also restarted
+# after a short delay (30 seconds) if the connection fails.
+#
+# Bridges using the "lazy" start type will be started automatically when the
+# number of queued messages exceeds the number set with the "threshold"
+# parameter. It will be stopped automatically after the time set by the
+# "idle_timeout" parameter. Use this start type if you wish the connection to
+# only be active when it is needed.
+#
+# A bridge using the "once" start type will be started automatically when the
+# broker starts but will not be restarted if the connection fails.
+#start_type automatic
+
+# Set the number of messages that need to be queued for a bridge with lazy
+# start type to be restarted. Defaults to 10 messages.
+# Must be less than max_queued_messages.
+#threshold 10
+
+# If try_private is set to true, the bridge will attempt to indicate to the
+# remote broker that it is a bridge not an ordinary client. If successful, this
+# means that loop detection will be more effective and that retained messages
+# will be propagated correctly. Not all brokers support this feature so it may
+# be necessary to set try_private to false if your bridge does not connect
+# properly.
+#try_private true
+
+# -----------------------------------------------------------------
+# Certificate based SSL/TLS support
+# -----------------------------------------------------------------
+# Either bridge_cafile or bridge_capath must be defined to enable TLS support
+# for this bridge.
+# bridge_cafile defines the path to a file containing the
+# Certificate Authority certificates that have signed the remote broker
+# certificate.
+# bridge_capath defines a directory that will be searched for files containing
+# the CA certificates. For bridge_capath to work correctly, the certificate
+# files must have ".crt" as the file ending and you must run "openssl rehash
+# <path to capath>" each time you add/remove a certificate.
+#bridge_cafile
+#bridge_capath
+
+
+# If the remote broker has more than one protocol available on its port, e.g.
+# MQTT and WebSockets, then use bridge_alpn to configure which protocol is
+# requested. Note that WebSockets support for bridges is not yet available.
+#bridge_alpn
+
+# When using certificate based encryption, bridge_insecure disables
+# verification of the server hostname in the server certificate. This can be
+# useful when testing initial server configurations, but makes it possible for
+# a malicious third party to impersonate your server through DNS spoofing, for
+# example. Use this option in testing only. If you need to resort to using this
+# option in a production environment, your setup is at fault and there is no
+# point using encryption.
+#bridge_insecure false
+
+# Path to the PEM encoded client certificate, if required by the remote broker.
+#bridge_certfile
+
+# Path to the PEM encoded client private key, if required by the remote broker.
+#bridge_keyfile
+
+# -----------------------------------------------------------------
+# PSK based SSL/TLS support
+# -----------------------------------------------------------------
+# Pre-shared-key encryption provides an alternative to certificate based
+# encryption. A bridge can be configured to use PSK with the bridge_identity
+# and bridge_psk options. These are the client PSK identity, and pre-shared-key
+# in hexadecimal format with no "0x". Only one of certificate and PSK based
+# encryption can be used on one
+# bridge at once.
+#bridge_identity
+#bridge_psk
+
+
+# =================================================================
+# External config files
+# =================================================================
+
+# External configuration files may be included by using the
+# include_dir option. This defines a directory that will be searched
+# for config files. All files that end in '.conf' will be loaded as
+# a configuration file. It is best to have this as the last option
+# in the main file. This option will only be processed from the main
+# configuration file. The directory specified must not contain the
+# main configuration file.
+# Files within include_dir will be loaded sorted in case-sensitive
+# alphabetical order, with capital letters ordered first. If this option is
+# given multiple times, all of the files from the first instance will be
+# processed before the next instance. See the man page for examples.
+#include_dir
diff --git a/examples/basyx.nodered/node-red-contrib-aas-connect/get-aas-property.html b/examples/basyx.nodered/node-red-contrib-aas-connect/get-aas-property.html
new file mode 100644
index 0000000..540724e
--- /dev/null
+++ b/examples/basyx.nodered/node-red-contrib-aas-connect/get-aas-property.html
@@ -0,0 +1,50 @@
+<script type="text/javascript">
+ RED.nodes.registerType('get-aas-property', {
+ category: 'network',
+ defaults: {
+ name: { value: '' },
+ property: { value: 'temperature', required: true },
+ period: { value: 1, required: true }
+ },
+ inputs: 1,
+ outputs: 1,
+ color: '#C3D6F2',
+ align: 'left',
+ label: function() {
+ return this.name || "Get AAS property";
+ },
+ palettelabel: function() {
+ return this.name || "Get AAS property";
+ },
+ icon: 'logo-BaSys4.png',
+ })
+</script>
+
+<script type="text/html" data-template-name="get-aas-property">
+ <div class="form-row">
+ <label for="node-input-name"><i class="fa fa-tag"></i> Node name</label>
+ <input type="text" id="node-input-name" placeholder="Name">
+ </div>
+ <div class="form-row">
+ <label for="node-input-property"><i class="fa fa-rss"></i> Property</label>
+ <input type="text" id="node-input-property" placeholder="Property">
+ </div>
+ <div class="form-row">
+ <label for="node-input-period">
+ <i class="fa fa-clock-o"></i> Interval <br/><span style="font-size:0.8em">(in seconds)</span>
+ </label>
+ <input type="text" id="node-input-period" placeholder="Interval">
+ </div>
+ <!-- <div class="form-tips"><b>Tip:</b> This is here to help.</div> -->
+</script>
+
+<script type="text/html" data-help-name="get-aas-property">
+ <p>This node monitors (periodically retrives) the value of an AAS property</p>
+ <h3>Outputs</h3>
+ <dl class="message-properties">
+ <dt>payload
+ <span class="property-type">object</span>
+ </dt>
+ <dd> the payload of the message containing the property value (in the 'value' key) and the timestamp (in the 'tstamp' key). </dd>
+ </dl>
+ </script>
\ No newline at end of file
diff --git a/examples/basyx.nodered/node-red-contrib-aas-connect/get-aas-property.js b/examples/basyx.nodered/node-red-contrib-aas-connect/get-aas-property.js
new file mode 100644
index 0000000..09fdd0e
--- /dev/null
+++ b/examples/basyx.nodered/node-red-contrib-aas-connect/get-aas-property.js
@@ -0,0 +1,60 @@
+module.exports = function(RED) {
+
+ const request = require('request');
+
+ function GetAASProperty(config) {
+ let property = config.property || 'temp';
+ let endpoint = `http://aas-wrapper:6500/streamsheets/${property}`;
+ let period = Number(config.period) || 1;
+ let msperiod = period * 1000;
+
+ RED.nodes.createNode(this, config);
+
+ let node = this;
+ let interval;
+ node.on('input', function(msg, send, done) {
+ interval = setInterval(() => {
+ request(endpoint, { json: true }, (err, res, body) => {
+ if (err) {
+ node.error(err);
+ if (done) done(err);
+ return;
+ }
+
+ if (!body.success) {
+ node.error(body.messages);
+ if (done) done(body.messages);
+ return;
+ }
+
+ // let mostRecentTimestamp = body.timestamp.pop();
+ // let mostRecentValue = body.content.pop();
+
+ let data = {
+ tstamp: body.timestamp,
+ data: body.content,
+ }
+
+ msg.payload = data;
+
+ // For maximum backwards compatibility, check that send exists.
+ // If this node is installed in Node-RED 0.x, it will need to
+ // fallback to using `node.send`
+ send = send || function() { node.send.apply(node, msg) }
+ send(msg);
+
+ if (done) done();
+ });
+ }, msperiod);
+
+ });
+
+ node.on('close', function(done) {
+ clearInterval(interval);
+ if (done) done();
+ });
+
+ }
+
+ RED.nodes.registerType('get-aas-property', GetAASProperty);
+}
\ No newline at end of file
diff --git a/examples/basyx.nodered/node-red-contrib-aas-connect/icons/logo-BaSys4.png b/examples/basyx.nodered/node-red-contrib-aas-connect/icons/logo-BaSys4.png
new file mode 100644
index 0000000..56d0dac
--- /dev/null
+++ b/examples/basyx.nodered/node-red-contrib-aas-connect/icons/logo-BaSys4.png
Binary files differ
diff --git a/examples/basyx.nodered/node-red-contrib-aas-connect/icons/machine.png b/examples/basyx.nodered/node-red-contrib-aas-connect/icons/machine.png
new file mode 100644
index 0000000..56db813
--- /dev/null
+++ b/examples/basyx.nodered/node-red-contrib-aas-connect/icons/machine.png
Binary files differ
diff --git a/examples/basyx.nodered/node-red-contrib-aas-connect/package.json b/examples/basyx.nodered/node-red-contrib-aas-connect/package.json
new file mode 100644
index 0000000..882bb64
--- /dev/null
+++ b/examples/basyx.nodered/node-red-contrib-aas-connect/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "node-red-contrib-aas-connect",
+ "version": "1.0.0",
+ "description": "",
+ "main": "get-aas-property.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "node-red": {
+ "nodes": {
+ "get-aas-property": "get-aas-property.js",
+ "set-aas-property": "set-aas-property.js"
+ }
+ },
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "request": "^2.88.2"
+ }
+}
diff --git a/examples/basyx.nodered/node-red-contrib-aas-connect/set-aas-property.html b/examples/basyx.nodered/node-red-contrib-aas-connect/set-aas-property.html
new file mode 100644
index 0000000..db9b93a
--- /dev/null
+++ b/examples/basyx.nodered/node-red-contrib-aas-connect/set-aas-property.html
@@ -0,0 +1,36 @@
+<script type="text/javascript">
+ RED.nodes.registerType('set-aas-property', {
+ category: 'network',
+ defaults: {
+ name: { value: '' },
+ property: { value: 'temp', required: true },
+ },
+ inputs: 1,
+ outputs: 0,
+ color: '#C3D6F2',
+ align: 'right',
+ label: function() {
+ return this.name || "Set AAS property";
+ },
+ palettelabel: function() {
+ return this.name || "Set AAS property";
+ },
+ icon: 'logo-BaSys4.png',
+ })
+</script>
+
+<script type="text/html" data-template-name="set-aas-property">
+ <div class="form-row">
+ <label for="node-input-name"><i class="fa fa-tag"></i> Node name</label>
+ <input type="text" id="node-input-name" placeholder="Name">
+ </div>
+ <div class="form-row">
+ <label for="node-input-property"><i class="fa fa-rss"></i> Property</label>
+ <input type="text" id="node-input-property" placeholder="Property">
+ </div>
+ <!-- <div class="form-tips"><b>Tip:</b> This is here to help.</div> -->
+</script>
+
+<script type="text/html" data-help-name="set-aas-property">
+ <p>This node sets the value of an AAS property</p>
+ </script>
\ No newline at end of file
diff --git a/examples/basyx.nodered/node-red-contrib-aas-connect/set-aas-property.js b/examples/basyx.nodered/node-red-contrib-aas-connect/set-aas-property.js
new file mode 100644
index 0000000..d8af768
--- /dev/null
+++ b/examples/basyx.nodered/node-red-contrib-aas-connect/set-aas-property.js
@@ -0,0 +1,40 @@
+module.exports = function(RED) {
+
+ const request = require('request');
+
+ function SetAASProperty(config) {
+ let property = config.property || 'temp';
+ let endpoint = `http://aas-wrapper:6500/streamsheets/${property}`;
+
+ RED.nodes.createNode(this, config);
+
+ let node = this;
+ node.on('input', function(msg, send, done) {
+ let newValue = msg.payload;
+
+ request({
+ uri: endpoint,
+ method: 'POST',
+ json: true,
+ body: { value: newValue }
+ }, (err, res, body) => {
+ if (err) {
+ node.error(err);
+ if (done) done(err);
+ return;
+ }
+
+ if (!body.success) {
+ node.error(body.error);
+ if (done) done(body.error);
+ return;
+ }
+
+ if (done) done();
+ });
+ });
+ }
+
+ RED.nodes.registerType('set-aas-property', SetAASProperty);
+
+}
\ No newline at end of file
diff --git a/examples/basyx.nodered/node-red/.config.json b/examples/basyx.nodered/node-red/.config.json
new file mode 100644
index 0000000..c8d13d4
--- /dev/null
+++ b/examples/basyx.nodered/node-red/.config.json
@@ -0,0 +1,456 @@
+{
+ "nodes": {
+ "node-red": {
+ "name": "node-red",
+ "version": "1.1.2",
+ "local": false,
+ "nodes": {
+ "inject": {
+ "name": "inject",
+ "types": [
+ "inject"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/common/20-inject.js"
+ },
+ "debug": {
+ "name": "debug",
+ "types": [
+ "debug"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/common/21-debug.js"
+ },
+ "complete": {
+ "name": "complete",
+ "types": [
+ "complete"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/common/24-complete.js"
+ },
+ "catch": {
+ "name": "catch",
+ "types": [
+ "catch"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/common/25-catch.js"
+ },
+ "status": {
+ "name": "status",
+ "types": [
+ "status"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/common/25-status.js"
+ },
+ "link": {
+ "name": "link",
+ "types": [
+ "link in",
+ "link out"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/common/60-link.js"
+ },
+ "comment": {
+ "name": "comment",
+ "types": [
+ "comment"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/common/90-comment.js"
+ },
+ "unknown": {
+ "name": "unknown",
+ "types": [
+ "unknown"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/common/98-unknown.js"
+ },
+ "function": {
+ "name": "function",
+ "types": [
+ "function"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/function/10-function.js"
+ },
+ "switch": {
+ "name": "switch",
+ "types": [
+ "switch"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/function/10-switch.js"
+ },
+ "change": {
+ "name": "change",
+ "types": [
+ "change"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/function/15-change.js"
+ },
+ "range": {
+ "name": "range",
+ "types": [
+ "range"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/function/16-range.js"
+ },
+ "template": {
+ "name": "template",
+ "types": [
+ "template"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/function/80-template.js"
+ },
+ "delay": {
+ "name": "delay",
+ "types": [
+ "delay"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/function/89-delay.js"
+ },
+ "trigger": {
+ "name": "trigger",
+ "types": [
+ "trigger"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/function/89-trigger.js"
+ },
+ "exec": {
+ "name": "exec",
+ "types": [
+ "exec"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/function/90-exec.js"
+ },
+ "tls": {
+ "name": "tls",
+ "types": [
+ "tls-config"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/network/05-tls.js"
+ },
+ "httpproxy": {
+ "name": "httpproxy",
+ "types": [
+ "http proxy"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/network/06-httpproxy.js"
+ },
+ "mqtt": {
+ "name": "mqtt",
+ "types": [
+ "mqtt in",
+ "mqtt out",
+ "mqtt-broker"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/network/10-mqtt.js"
+ },
+ "httpin": {
+ "name": "httpin",
+ "types": [
+ "http in",
+ "http response"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/network/21-httpin.js"
+ },
+ "httprequest": {
+ "name": "httprequest",
+ "types": [
+ "http request"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/network/21-httprequest.js"
+ },
+ "websocket": {
+ "name": "websocket",
+ "types": [
+ "websocket in",
+ "websocket out",
+ "websocket-listener",
+ "websocket-client"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/network/22-websocket.js"
+ },
+ "tcpin": {
+ "name": "tcpin",
+ "types": [
+ "tcp in",
+ "tcp out",
+ "tcp request"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/network/31-tcpin.js"
+ },
+ "udp": {
+ "name": "udp",
+ "types": [
+ "udp in",
+ "udp out"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/network/32-udp.js"
+ },
+ "CSV": {
+ "name": "CSV",
+ "types": [
+ "csv"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/parsers/70-CSV.js"
+ },
+ "HTML": {
+ "name": "HTML",
+ "types": [
+ "html"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/parsers/70-HTML.js"
+ },
+ "JSON": {
+ "name": "JSON",
+ "types": [
+ "json"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/parsers/70-JSON.js"
+ },
+ "XML": {
+ "name": "XML",
+ "types": [
+ "xml"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/parsers/70-XML.js"
+ },
+ "YAML": {
+ "name": "YAML",
+ "types": [
+ "yaml"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/parsers/70-YAML.js"
+ },
+ "split": {
+ "name": "split",
+ "types": [
+ "split",
+ "join"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/sequence/17-split.js"
+ },
+ "sort": {
+ "name": "sort",
+ "types": [
+ "sort"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/sequence/18-sort.js"
+ },
+ "batch": {
+ "name": "batch",
+ "types": [
+ "batch"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/sequence/19-batch.js"
+ },
+ "file": {
+ "name": "file",
+ "types": [
+ "file",
+ "file in"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/storage/10-file.js"
+ },
+ "watch": {
+ "name": "watch",
+ "types": [
+ "watch"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red",
+ "file": "/usr/src/node-red/node_modules/@node-red/nodes/core/storage/23-watch.js"
+ }
+ }
+ },
+ "node-red-contrib-aas-connect": {
+ "name": "node-red-contrib-aas-connect",
+ "version": "1.0.0",
+ "local": false,
+ "nodes": {
+ "get-aas-property": {
+ "name": "get-aas-property",
+ "types": [
+ "get-aas-property"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red-contrib-aas-connect",
+ "file": "/usr/src/node-red/node_modules/node-red-contrib-aas-connect/get-aas-property.js"
+ },
+ "set-aas-property": {
+ "name": "set-aas-property",
+ "types": [
+ "set-aas-property"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red-contrib-aas-connect",
+ "file": "/usr/src/node-red/node_modules/node-red-contrib-aas-connect/set-aas-property.js"
+ }
+ }
+ },
+ "node-red-contrib-graphs": {
+ "name": "node-red-contrib-graphs",
+ "version": "0.3.5",
+ "local": false,
+ "nodes": {
+ "datasource": {
+ "name": "datasource",
+ "types": [
+ "iot-datasource"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red-contrib-graphs",
+ "file": "/usr/src/node-red/node_modules/node-red-contrib-graphs/datasource.js"
+ }
+ }
+ },
+ "node-red-node-rbe": {
+ "name": "node-red-node-rbe",
+ "version": "0.2.9",
+ "local": false,
+ "nodes": {
+ "rbe": {
+ "name": "rbe",
+ "types": [
+ "rbe"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red-node-rbe",
+ "file": "/usr/src/node-red/node_modules/node-red-node-rbe/rbe.js"
+ }
+ }
+ },
+ "node-red-node-tail": {
+ "name": "node-red-node-tail",
+ "version": "0.1.1",
+ "local": false,
+ "nodes": {
+ "tail": {
+ "name": "tail",
+ "types": [
+ "tail"
+ ],
+ "enabled": true,
+ "local": false,
+ "module": "node-red-node-tail",
+ "file": "/usr/src/node-red/node_modules/node-red-node-tail/28-tail.js"
+ }
+ }
+ }
+ },
+ "_credentialSecret": "c028885f2cb96099498adca00919db2df73313d916d037a5a1c8eb931b43c03e",
+ "users": {
+ "_": {
+ "editor": {
+ "view": {
+ "view-show-grid": true,
+ "view-snap-grid": true,
+ "view-grid-size": 20,
+ "view-node-status": true,
+ "view-node-show-label": true,
+ "view-show-tips": false
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/basyx.nodered/node-red/.dash/config_default.json b/examples/basyx.nodered/node-red/.dash/config_default.json
new file mode 100644
index 0000000..711d715
--- /dev/null
+++ b/examples/basyx.nodered/node-red/.dash/config_default.json
@@ -0,0 +1,3 @@
+{
+ "dashboards": []
+}
\ No newline at end of file
diff --git a/examples/basyx.nodered/node-red/flows.json b/examples/basyx.nodered/node-red/flows.json
new file mode 100644
index 0000000..0e22486
--- /dev/null
+++ b/examples/basyx.nodered/node-red/flows.json
@@ -0,0 +1 @@
+[{"id":"ac89e902.a48598","type":"tab","label":"AAS Temperature","disabled":false,"info":""},{"id":"91383a96.5b2e48","type":"tab","label":"Test","disabled":false,"info":""},{"id":"1b4daff8.52358","type":"mqtt-broker","z":"","name":"Streamsheets Mosquitto","broker":"http://streamsheets","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"2289cf21.0a21b","type":"inject","z":"ac89e902.a48598","name":"Start","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":110,"y":120,"wires":[["c3659321.46995"]]},{"id":"c3659321.46995","type":"get-aas-property","z":"ac89e902.a48598","name":"Get Temperature","property":"temp","period":1,"x":270,"y":120,"wires":[["a56bd8e1.1777c8","2c13d59c.208cea","8c41e115.ac913"]]},{"id":"4e72f0db.c2a7a","type":"inject","z":"91383a96.5b2e48","name":"Data","props":[{"p":"values","v":"[22.344,23.434,24.342,21.098]","vt":"json"},{"p":"timestamp","v":"[121323,2112231,321331,321321]","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":140,"y":100,"wires":[["a53d7930.6109f8","4a734a5f.b22314","ab2d648a.1767f8"]]},{"id":"a53d7930.6109f8","type":"debug","z":"91383a96.5b2e48","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":250,"y":240,"wires":[]},{"id":"4a734a5f.b22314","type":"function","z":"91383a96.5b2e48","name":"Calculate Average","func":"msg.avg = msg.values.reduce((a,b) => a + b, 0) / msg.values.length;\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":450,"y":120,"wires":[["6b28c27b.5547bc"]]},{"id":"6b28c27b.5547bc","type":"debug","z":"91383a96.5b2e48","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":710,"y":120,"wires":[]},{"id":"ab2d648a.1767f8","type":"function","z":"91383a96.5b2e48","name":"Calculate Fahrenheit","func":"msg.fahrenheit = msg.values.map(v => v * 1.8 + 32);\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":460,"y":180,"wires":[["5e614070.a223f"]]},{"id":"5e614070.a223f","type":"debug","z":"91383a96.5b2e48","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":710,"y":180,"wires":[]},{"id":"e733d05.63a9c3","type":"iot-datasource","z":"ac89e902.a48598","name":"Temperature Datasource","tstampField":"tstamp","dataField":"data","disableDiscover":false,"x":730,"y":240,"wires":[[]]},{"id":"8e627da8.65b74","type":"mqtt out","z":"ac89e902.a48598","name":"MQTT Avg Temp Publisher","topic":"temperature/average","qos":"","retain":"","broker":"1b4daff8.52358","x":740,"y":120,"wires":[]},{"id":"7aa9e937.6575c8","type":"mqtt in","z":"ac89e902.a48598","name":"MQTT Avg Temp Consumer","topic":"temperature/average","qos":"2","datatype":"json","broker":"1b4daff8.52358","x":160,"y":360,"wires":[["f96c72b.3be659"]]},{"id":"f96c72b.3be659","type":"debug","z":"ac89e902.a48598","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload.average","targetType":"msg","statusVal":"","statusType":"auto","x":420,"y":360,"wires":[]},{"id":"a56bd8e1.1777c8","type":"function","z":"ac89e902.a48598","name":"Calculate Average","func":"const payload = msg.payload;\nlet average = payload.data.reduce((a,b) => a + b, 0) / payload.data.length;\npayload.average = average;\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":490,"y":120,"wires":[["8e627da8.65b74"]]},{"id":"2c13d59c.208cea","type":"function","z":"ac89e902.a48598","name":"Calculate Fahrenheit","func":"const payload = msg.payload;\nlet fahrenheit = payload.data.map(v => v * 1.8 + 32);\npayload.fahrenheit = fahrenheit;\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":500,"y":180,"wires":[["a3733cf8.97468"]]},{"id":"a3733cf8.97468","type":"mqtt out","z":"ac89e902.a48598","name":"MQTT Fahrenheit Temp Publisher","topic":"temperature/fahrenheit","qos":"","retain":"","broker":"1b4daff8.52358","x":760,"y":180,"wires":[]},{"id":"8c41e115.ac913","type":"function","z":"ac89e902.a48598","name":"Get most recent","func":"msg.payload.tstamp = new Date(msg.payload.tstamp.pop()).getTime();\nmsg.payload.data = msg.payload.data.pop();\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":480,"y":240,"wires":[["e733d05.63a9c3"]]}]
\ No newline at end of file
diff --git a/examples/basyx.nodered/node-red/flows_cred.json b/examples/basyx.nodered/node-red/flows_cred.json
new file mode 100644
index 0000000..4c13969
--- /dev/null
+++ b/examples/basyx.nodered/node-red/flows_cred.json
@@ -0,0 +1 @@
+{"$":"39276bec4323f4ed09a2dd89a418c8dbzuTmQVz1IqyWc+XsMg2dPUwV6wsK1UkHOK9rPFDgSV45EMgO7pc7LYzFXg=="}
\ No newline at end of file
diff --git a/examples/basyx.nodered/node-red/package.json b/examples/basyx.nodered/node-red/package.json
new file mode 100644
index 0000000..b788fd4
--- /dev/null
+++ b/examples/basyx.nodered/node-red/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "node-red-project",
+ "description": "A Node-RED Project",
+ "version": "0.0.1",
+ "private": true
+}
\ No newline at end of file
diff --git a/examples/basyx.nodered/node-red/settings.js b/examples/basyx.nodered/node-red/settings.js
new file mode 100644
index 0000000..f3c93f5
--- /dev/null
+++ b/examples/basyx.nodered/node-red/settings.js
@@ -0,0 +1,299 @@
+/**
+ * Copyright JS Foundation and other contributors, http://js.foundation
+ *
+ * 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.
+ **/
+
+module.exports = {
+ // the tcp port that the Node-RED web server is listening on
+ uiPort: process.env.PORT || 1880,
+
+ // By default, the Node-RED UI accepts connections on all IPv4 interfaces.
+ // To listen on all IPv6 addresses, set uiHost to "::",
+ // The following property can be used to listen on a specific interface. For
+ // example, the following would only allow connections from the local machine.
+ //uiHost: "127.0.0.1",
+
+ // Retry time in milliseconds for MQTT connections
+ mqttReconnectTime: 15000,
+
+ // Retry time in milliseconds for Serial port connections
+ serialReconnectTime: 15000,
+
+ // Retry time in milliseconds for TCP socket connections
+ //socketReconnectTime: 10000,
+
+ // Timeout in milliseconds for TCP server socket connections
+ // defaults to no timeout
+ //socketTimeout: 120000,
+
+ // Maximum number of messages to wait in queue while attempting to connect to TCP socket
+ // defaults to 1000
+ //tcpMsgQueueSize: 2000,
+
+ // Timeout in milliseconds for HTTP request connections
+ // defaults to 120 seconds
+ //httpRequestTimeout: 120000,
+
+ // The maximum length, in characters, of any message sent to the debug sidebar tab
+ debugMaxLength: 1000,
+
+ // The maximum number of messages nodes will buffer internally as part of their
+ // operation. This applies across a range of nodes that operate on message sequences.
+ // defaults to no limit. A value of 0 also means no limit is applied.
+ //nodeMessageBufferMaxLength: 0,
+
+ // To disable the option for using local files for storing keys and certificates in the TLS configuration
+ // node, set this to true
+ //tlsConfigDisableLocalFiles: true,
+
+ // Colourise the console output of the debug node
+ //debugUseColors: true,
+
+ // The file containing the flows. If not set, it defaults to flows_<hostname>.json
+ //flowFile: 'flows.json',
+
+ // To enabled pretty-printing of the flow within the flow file, set the following
+ // property to true:
+ //flowFilePretty: true,
+
+ // By default, credentials are encrypted in storage using a generated key. To
+ // specify your own secret, set the following property.
+ // If you want to disable encryption of credentials, set this property to false.
+ // Note: once you set this property, do not change it - doing so will prevent
+ // node-red from being able to decrypt your existing credentials and they will be
+ // lost.
+ //credentialSecret: "a-secret-key",
+
+ // By default, all user data is stored in a directory called `.node-red` under
+ // the user's home directory. To use a different location, the following
+ // property can be used
+ //userDir: '/home/nol/.node-red/',
+
+ // Node-RED scans the `nodes` directory in the userDir to find local node files.
+ // The following property can be used to specify an additional directory to scan.
+ //nodesDir: '/home/nol/.node-red/nodes',
+
+ // By default, the Node-RED UI is available at http://localhost:1880/
+ // The following property can be used to specify a different root path.
+ // If set to false, this is disabled.
+ //httpAdminRoot: '/admin',
+
+ // Some nodes, such as HTTP In, can be used to listen for incoming http requests.
+ // By default, these are served relative to '/'. The following property
+ // can be used to specifiy a different root path. If set to false, this is
+ // disabled.
+ //httpNodeRoot: '/red-nodes',
+
+ // The following property can be used in place of 'httpAdminRoot' and 'httpNodeRoot',
+ // to apply the same root to both parts.
+ //httpRoot: '/red',
+
+ // When httpAdminRoot is used to move the UI to a different root path, the
+ // following property can be used to identify a directory of static content
+ // that should be served at http://localhost:1880/.
+ //httpStatic: '/home/nol/node-red-static/',
+
+ // The maximum size of HTTP request that will be accepted by the runtime api.
+ // Default: 5mb
+ //apiMaxLength: '5mb',
+
+ // If you installed the optional node-red-dashboard you can set it's path
+ // relative to httpRoot
+ //ui: { path: "ui" },
+
+ // Securing Node-RED
+ // -----------------
+ // To password protect the Node-RED editor and admin API, the following
+ // property can be used. See http://nodered.org/docs/security.html for details.
+ //adminAuth: {
+ // type: "credentials",
+ // users: [{
+ // username: "admin",
+ // password: "$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN.",
+ // permissions: "*"
+ // }]
+ //},
+
+ // To password protect the node-defined HTTP endpoints (httpNodeRoot), or
+ // the static content (httpStatic), the following properties can be used.
+ // The pass field is a bcrypt hash of the password.
+ // See http://nodered.org/docs/security.html#generating-the-password-hash
+ //httpNodeAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."},
+ //httpStaticAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."},
+
+ // The following property can be used to enable HTTPS
+ // See http://nodejs.org/api/https.html#https_https_createserver_options_requestlistener
+ // for details on its contents.
+ // This property can be either an object, containing both a (private) key and a (public) certificate,
+ // or a function that returns such an object:
+ //// https object:
+ //https: {
+ // key: require("fs").readFileSync('privkey.pem'),
+ // cert: require("fs").readFileSync('cert.pem')
+ //},
+ ////https function:
+ // https: function() {
+ // // This function should return the options object, or a Promise
+ // // that resolves to the options object
+ // return {
+ // key: require("fs").readFileSync('privkey.pem'),
+ // cert: require("fs").readFileSync('cert.pem')
+ // }
+ // },
+
+ // The following property can be used to refresh the https settings at a
+ // regular time interval in hours.
+ // This requires:
+ // - the `https` setting to be a function that can be called to get
+ // the refreshed settings.
+ // - Node.js 11 or later.
+ //httpsRefreshInterval : 12,
+
+ // The following property can be used to cause insecure HTTP connections to
+ // be redirected to HTTPS.
+ //requireHttps: true,
+
+ // The following property can be used to disable the editor. The admin API
+ // is not affected by this option. To disable both the editor and the admin
+ // API, use either the httpRoot or httpAdminRoot properties
+ //disableEditor: false,
+
+ // The following property can be used to configure cross-origin resource sharing
+ // in the HTTP nodes.
+ // See https://github.com/troygoode/node-cors#configuration-options for
+ // details on its contents. The following is a basic permissive set of options:
+ //httpNodeCors: {
+ // origin: "*",
+ // methods: "GET,PUT,POST,DELETE"
+ //},
+
+ // If you need to set an http proxy please set an environment variable
+ // called http_proxy (or HTTP_PROXY) outside of Node-RED in the operating system.
+ // For example - http_proxy=http://myproxy.com:8080
+ // (Setting it here will have no effect)
+ // You may also specify no_proxy (or NO_PROXY) to supply a comma separated
+ // list of domains to not proxy, eg - no_proxy=.acme.co,.acme.co.uk
+
+ // The following property can be used to add a custom middleware function
+ // in front of all http in nodes. This allows custom authentication to be
+ // applied to all http in nodes, or any other sort of common request processing.
+ //httpNodeMiddleware: function(req,res,next) {
+ // // Handle/reject the request, or pass it on to the http in node by calling next();
+ // // Optionally skip our rawBodyParser by setting this to true;
+ // //req.skipRawBodyParser = true;
+ // next();
+ //},
+
+
+ // The following property can be used to add a custom middleware function
+ // in front of all admin http routes. For example, to set custom http
+ // headers
+ // httpAdminMiddleware: function(req,res,next) {
+ // // Set the X-Frame-Options header to limit where the editor
+ // // can be embedded
+ // //res.set('X-Frame-Options', 'sameorigin');
+ // next();
+ // },
+
+ // The following property can be used to pass custom options to the Express.js
+ // server used by Node-RED. For a full list of available options, refer
+ // to http://expressjs.com/en/api.html#app.settings.table
+ //httpServerOptions: { },
+
+ // The following property can be used to verify websocket connection attempts.
+ // This allows, for example, the HTTP request headers to be checked to ensure
+ // they include valid authentication information.
+ //webSocketNodeVerifyClient: function(info) {
+ // // 'info' has three properties:
+ // // - origin : the value in the Origin header
+ // // - req : the HTTP request
+ // // - secure : true if req.connection.authorized or req.connection.encrypted is set
+ // //
+ // // The function should return true if the connection should be accepted, false otherwise.
+ // //
+ // // Alternatively, if this function is defined to accept a second argument, callback,
+ // // it can be used to verify the client asynchronously.
+ // // The callback takes three arguments:
+ // // - result : boolean, whether to accept the connection or not
+ // // - code : if result is false, the HTTP error status to return
+ // // - reason: if result is false, the HTTP reason string to return
+ //},
+
+ // The following property can be used to seed Global Context with predefined
+ // values. This allows extra node modules to be made available with the
+ // Function node.
+ // For example,
+ // functionGlobalContext: { os:require('os') }
+ // can be accessed in a function block as:
+ // global.get("os")
+ functionGlobalContext: {
+ // os:require('os'),
+ // jfive:require("johnny-five"),
+ // j5board:require("johnny-five").Board({repl:false})
+ },
+ // `global.keys()` returns a list of all properties set in global context.
+ // This allows them to be displayed in the Context Sidebar within the editor.
+ // In some circumstances it is not desirable to expose them to the editor. The
+ // following property can be used to hide any property set in `functionGlobalContext`
+ // from being list by `global.keys()`.
+ // By default, the property is set to false to avoid accidental exposure of
+ // their values. Setting this to true will cause the keys to be listed.
+ exportGlobalContextKeys: false,
+
+
+ // Context Storage
+ // The following property can be used to enable context storage. The configuration
+ // provided here will enable file-based context that flushes to disk every 30 seconds.
+ // Refer to the documentation for further options: https://nodered.org/docs/api/context/
+ //
+ //contextStorage: {
+ // default: {
+ // module:"localfilesystem"
+ // },
+ //},
+
+ // The following property can be used to order the categories in the editor
+ // palette. If a node's category is not in the list, the category will get
+ // added to the end of the palette.
+ // If not set, the following default order is used:
+ //paletteCategories: ['subflows', 'common', 'function', 'network', 'sequence', 'parser', 'storage'],
+
+ // Configure the logging output
+ logging: {
+ // Only console logging is currently supported
+ console: {
+ // Level of logging to be recorded. Options are:
+ // fatal - only those errors which make the application unusable should be recorded
+ // error - record errors which are deemed fatal for a particular request + fatal errors
+ // warn - record problems which are non fatal + errors + fatal errors
+ // info - record information about the general running of the application + warn + error + fatal errors
+ // debug - record information which is more verbose than info + info + warn + error + fatal errors
+ // trace - record very detailed logging + debug + info + warn + error + fatal errors
+ // off - turn off all logging (doesn't affect metrics or audit)
+ level: "info",
+ // Whether or not to include metric events in the log output
+ metrics: false,
+ // Whether or not to include audit events in the log output
+ audit: false
+ }
+ },
+
+ // Customising the editor
+ editorTheme: {
+ projects: {
+ // To enable the Projects feature, set this value to true
+ enabled: false
+ }
+ }
+}
diff --git a/examples/basyx.nodered/package.json b/examples/basyx.nodered/package.json
new file mode 100644
index 0000000..03f658f
--- /dev/null
+++ b/examples/basyx.nodered/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "node-red-docker",
+ "version": "1.1.2",
+ "description": "Low-code programming for event-driven applications",
+ "homepage": "http://nodered.org",
+ "license": "Apache-2.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/node-red/node-red-docker.git"
+ },
+ "main": "node_modules/node-red/red/red.js",
+ "scripts": {
+ "start": "node $NODE_OPTIONS node_modules/node-red/red.js $FLOWS",
+ "debug": "node --inspect=0.0.0.0:9229 $NODE_OPTIONS node_modules/node-red/red.js $FLOWS",
+ "debug_brk": "node --inspect=0.0.0.0:9229 --inspect-brk $NODE_OPTIONS node_modules/node-red/red.js $FLOWS"
+ },
+ "contributors": [
+ {
+ "name": "Dave Conway-Jones"
+ },
+ {
+ "name": "Nick O'Leary"
+ },
+ {
+ "name": "James Thomas"
+ },
+ { "name": "Raymond Mouthaan" } ],
+ "dependencies": {
+ "node-red": "1.1.2",
+ "node-red-contrib-graphs": "0.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+}
\ No newline at end of file
diff --git a/examples/basyx.nodered/settings.js b/examples/basyx.nodered/settings.js
new file mode 100644
index 0000000..f3c93f5
--- /dev/null
+++ b/examples/basyx.nodered/settings.js
@@ -0,0 +1,299 @@
+/**
+ * Copyright JS Foundation and other contributors, http://js.foundation
+ *
+ * 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.
+ **/
+
+module.exports = {
+ // the tcp port that the Node-RED web server is listening on
+ uiPort: process.env.PORT || 1880,
+
+ // By default, the Node-RED UI accepts connections on all IPv4 interfaces.
+ // To listen on all IPv6 addresses, set uiHost to "::",
+ // The following property can be used to listen on a specific interface. For
+ // example, the following would only allow connections from the local machine.
+ //uiHost: "127.0.0.1",
+
+ // Retry time in milliseconds for MQTT connections
+ mqttReconnectTime: 15000,
+
+ // Retry time in milliseconds for Serial port connections
+ serialReconnectTime: 15000,
+
+ // Retry time in milliseconds for TCP socket connections
+ //socketReconnectTime: 10000,
+
+ // Timeout in milliseconds for TCP server socket connections
+ // defaults to no timeout
+ //socketTimeout: 120000,
+
+ // Maximum number of messages to wait in queue while attempting to connect to TCP socket
+ // defaults to 1000
+ //tcpMsgQueueSize: 2000,
+
+ // Timeout in milliseconds for HTTP request connections
+ // defaults to 120 seconds
+ //httpRequestTimeout: 120000,
+
+ // The maximum length, in characters, of any message sent to the debug sidebar tab
+ debugMaxLength: 1000,
+
+ // The maximum number of messages nodes will buffer internally as part of their
+ // operation. This applies across a range of nodes that operate on message sequences.
+ // defaults to no limit. A value of 0 also means no limit is applied.
+ //nodeMessageBufferMaxLength: 0,
+
+ // To disable the option for using local files for storing keys and certificates in the TLS configuration
+ // node, set this to true
+ //tlsConfigDisableLocalFiles: true,
+
+ // Colourise the console output of the debug node
+ //debugUseColors: true,
+
+ // The file containing the flows. If not set, it defaults to flows_<hostname>.json
+ //flowFile: 'flows.json',
+
+ // To enabled pretty-printing of the flow within the flow file, set the following
+ // property to true:
+ //flowFilePretty: true,
+
+ // By default, credentials are encrypted in storage using a generated key. To
+ // specify your own secret, set the following property.
+ // If you want to disable encryption of credentials, set this property to false.
+ // Note: once you set this property, do not change it - doing so will prevent
+ // node-red from being able to decrypt your existing credentials and they will be
+ // lost.
+ //credentialSecret: "a-secret-key",
+
+ // By default, all user data is stored in a directory called `.node-red` under
+ // the user's home directory. To use a different location, the following
+ // property can be used
+ //userDir: '/home/nol/.node-red/',
+
+ // Node-RED scans the `nodes` directory in the userDir to find local node files.
+ // The following property can be used to specify an additional directory to scan.
+ //nodesDir: '/home/nol/.node-red/nodes',
+
+ // By default, the Node-RED UI is available at http://localhost:1880/
+ // The following property can be used to specify a different root path.
+ // If set to false, this is disabled.
+ //httpAdminRoot: '/admin',
+
+ // Some nodes, such as HTTP In, can be used to listen for incoming http requests.
+ // By default, these are served relative to '/'. The following property
+ // can be used to specifiy a different root path. If set to false, this is
+ // disabled.
+ //httpNodeRoot: '/red-nodes',
+
+ // The following property can be used in place of 'httpAdminRoot' and 'httpNodeRoot',
+ // to apply the same root to both parts.
+ //httpRoot: '/red',
+
+ // When httpAdminRoot is used to move the UI to a different root path, the
+ // following property can be used to identify a directory of static content
+ // that should be served at http://localhost:1880/.
+ //httpStatic: '/home/nol/node-red-static/',
+
+ // The maximum size of HTTP request that will be accepted by the runtime api.
+ // Default: 5mb
+ //apiMaxLength: '5mb',
+
+ // If you installed the optional node-red-dashboard you can set it's path
+ // relative to httpRoot
+ //ui: { path: "ui" },
+
+ // Securing Node-RED
+ // -----------------
+ // To password protect the Node-RED editor and admin API, the following
+ // property can be used. See http://nodered.org/docs/security.html for details.
+ //adminAuth: {
+ // type: "credentials",
+ // users: [{
+ // username: "admin",
+ // password: "$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN.",
+ // permissions: "*"
+ // }]
+ //},
+
+ // To password protect the node-defined HTTP endpoints (httpNodeRoot), or
+ // the static content (httpStatic), the following properties can be used.
+ // The pass field is a bcrypt hash of the password.
+ // See http://nodered.org/docs/security.html#generating-the-password-hash
+ //httpNodeAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."},
+ //httpStaticAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."},
+
+ // The following property can be used to enable HTTPS
+ // See http://nodejs.org/api/https.html#https_https_createserver_options_requestlistener
+ // for details on its contents.
+ // This property can be either an object, containing both a (private) key and a (public) certificate,
+ // or a function that returns such an object:
+ //// https object:
+ //https: {
+ // key: require("fs").readFileSync('privkey.pem'),
+ // cert: require("fs").readFileSync('cert.pem')
+ //},
+ ////https function:
+ // https: function() {
+ // // This function should return the options object, or a Promise
+ // // that resolves to the options object
+ // return {
+ // key: require("fs").readFileSync('privkey.pem'),
+ // cert: require("fs").readFileSync('cert.pem')
+ // }
+ // },
+
+ // The following property can be used to refresh the https settings at a
+ // regular time interval in hours.
+ // This requires:
+ // - the `https` setting to be a function that can be called to get
+ // the refreshed settings.
+ // - Node.js 11 or later.
+ //httpsRefreshInterval : 12,
+
+ // The following property can be used to cause insecure HTTP connections to
+ // be redirected to HTTPS.
+ //requireHttps: true,
+
+ // The following property can be used to disable the editor. The admin API
+ // is not affected by this option. To disable both the editor and the admin
+ // API, use either the httpRoot or httpAdminRoot properties
+ //disableEditor: false,
+
+ // The following property can be used to configure cross-origin resource sharing
+ // in the HTTP nodes.
+ // See https://github.com/troygoode/node-cors#configuration-options for
+ // details on its contents. The following is a basic permissive set of options:
+ //httpNodeCors: {
+ // origin: "*",
+ // methods: "GET,PUT,POST,DELETE"
+ //},
+
+ // If you need to set an http proxy please set an environment variable
+ // called http_proxy (or HTTP_PROXY) outside of Node-RED in the operating system.
+ // For example - http_proxy=http://myproxy.com:8080
+ // (Setting it here will have no effect)
+ // You may also specify no_proxy (or NO_PROXY) to supply a comma separated
+ // list of domains to not proxy, eg - no_proxy=.acme.co,.acme.co.uk
+
+ // The following property can be used to add a custom middleware function
+ // in front of all http in nodes. This allows custom authentication to be
+ // applied to all http in nodes, or any other sort of common request processing.
+ //httpNodeMiddleware: function(req,res,next) {
+ // // Handle/reject the request, or pass it on to the http in node by calling next();
+ // // Optionally skip our rawBodyParser by setting this to true;
+ // //req.skipRawBodyParser = true;
+ // next();
+ //},
+
+
+ // The following property can be used to add a custom middleware function
+ // in front of all admin http routes. For example, to set custom http
+ // headers
+ // httpAdminMiddleware: function(req,res,next) {
+ // // Set the X-Frame-Options header to limit where the editor
+ // // can be embedded
+ // //res.set('X-Frame-Options', 'sameorigin');
+ // next();
+ // },
+
+ // The following property can be used to pass custom options to the Express.js
+ // server used by Node-RED. For a full list of available options, refer
+ // to http://expressjs.com/en/api.html#app.settings.table
+ //httpServerOptions: { },
+
+ // The following property can be used to verify websocket connection attempts.
+ // This allows, for example, the HTTP request headers to be checked to ensure
+ // they include valid authentication information.
+ //webSocketNodeVerifyClient: function(info) {
+ // // 'info' has three properties:
+ // // - origin : the value in the Origin header
+ // // - req : the HTTP request
+ // // - secure : true if req.connection.authorized or req.connection.encrypted is set
+ // //
+ // // The function should return true if the connection should be accepted, false otherwise.
+ // //
+ // // Alternatively, if this function is defined to accept a second argument, callback,
+ // // it can be used to verify the client asynchronously.
+ // // The callback takes three arguments:
+ // // - result : boolean, whether to accept the connection or not
+ // // - code : if result is false, the HTTP error status to return
+ // // - reason: if result is false, the HTTP reason string to return
+ //},
+
+ // The following property can be used to seed Global Context with predefined
+ // values. This allows extra node modules to be made available with the
+ // Function node.
+ // For example,
+ // functionGlobalContext: { os:require('os') }
+ // can be accessed in a function block as:
+ // global.get("os")
+ functionGlobalContext: {
+ // os:require('os'),
+ // jfive:require("johnny-five"),
+ // j5board:require("johnny-five").Board({repl:false})
+ },
+ // `global.keys()` returns a list of all properties set in global context.
+ // This allows them to be displayed in the Context Sidebar within the editor.
+ // In some circumstances it is not desirable to expose them to the editor. The
+ // following property can be used to hide any property set in `functionGlobalContext`
+ // from being list by `global.keys()`.
+ // By default, the property is set to false to avoid accidental exposure of
+ // their values. Setting this to true will cause the keys to be listed.
+ exportGlobalContextKeys: false,
+
+
+ // Context Storage
+ // The following property can be used to enable context storage. The configuration
+ // provided here will enable file-based context that flushes to disk every 30 seconds.
+ // Refer to the documentation for further options: https://nodered.org/docs/api/context/
+ //
+ //contextStorage: {
+ // default: {
+ // module:"localfilesystem"
+ // },
+ //},
+
+ // The following property can be used to order the categories in the editor
+ // palette. If a node's category is not in the list, the category will get
+ // added to the end of the palette.
+ // If not set, the following default order is used:
+ //paletteCategories: ['subflows', 'common', 'function', 'network', 'sequence', 'parser', 'storage'],
+
+ // Configure the logging output
+ logging: {
+ // Only console logging is currently supported
+ console: {
+ // Level of logging to be recorded. Options are:
+ // fatal - only those errors which make the application unusable should be recorded
+ // error - record errors which are deemed fatal for a particular request + fatal errors
+ // warn - record problems which are non fatal + errors + fatal errors
+ // info - record information about the general running of the application + warn + error + fatal errors
+ // debug - record information which is more verbose than info + info + warn + error + fatal errors
+ // trace - record very detailed logging + debug + info + warn + error + fatal errors
+ // off - turn off all logging (doesn't affect metrics or audit)
+ level: "info",
+ // Whether or not to include metric events in the log output
+ metrics: false,
+ // Whether or not to include audit events in the log output
+ audit: false
+ }
+ },
+
+ // Customising the editor
+ editorTheme: {
+ projects: {
+ // To enable the Projects feature, set this value to true
+ enabled: false
+ }
+ }
+}
diff --git a/examples/basyx.nodered/start.bat b/examples/basyx.nodered/start.bat
new file mode 100644
index 0000000..83ed0d0
--- /dev/null
+++ b/examples/basyx.nodered/start.bat
@@ -0,0 +1 @@
+docker-compose up
diff --git a/examples/basyx.nodered/start.sh b/examples/basyx.nodered/start.sh
new file mode 100644
index 0000000..d935e43
--- /dev/null
+++ b/examples/basyx.nodered/start.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+docker-compose up
\ No newline at end of file
diff --git a/examples/basyx.nodered/stop.bat b/examples/basyx.nodered/stop.bat
new file mode 100644
index 0000000..58694d0
--- /dev/null
+++ b/examples/basyx.nodered/stop.bat
@@ -0,0 +1 @@
+docker-compose down
\ No newline at end of file
diff --git a/examples/basyx.nodered/stop.sh b/examples/basyx.nodered/stop.sh
new file mode 100644
index 0000000..f5139e2
--- /dev/null
+++ b/examples/basyx.nodered/stop.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+docker-compose down
\ No newline at end of file
diff --git a/examples/basyx.streamsheets/.gitignore b/examples/basyx.streamsheets/.gitignore
new file mode 100644
index 0000000..5e2ad3c
--- /dev/null
+++ b/examples/basyx.streamsheets/.gitignore
@@ -0,0 +1,2 @@
+/mosquitto
+/streamsheets
\ No newline at end of file
diff --git a/examples/basyx.streamsheets/AAS Property Dashboard.json b/examples/basyx.streamsheets/AAS Property Dashboard.json
new file mode 100644
index 0000000..ecf6f0c
--- /dev/null
+++ b/examples/basyx.streamsheets/AAS Property Dashboard.json
@@ -0,0 +1,5225 @@
+{
+ "machines": [
+ {
+ "machine": {
+ "id": "S1-EB3RpyP",
+ "name": "AAS Property Dashboard",
+ "state": "stopped",
+ "metadata": {
+ "owner": "anon",
+ "lastModified": 1599658322212,
+ "lastModifiedBy": "unknown",
+ "machineservice": {
+ "id": "machines-HkgWO-Rp1v"
+ }
+ },
+ "streamsheets": [
+ {
+ "id": "Sk7ErnRTJD",
+ "name": "S1",
+ "loop": {
+ "path": "",
+ "enabled": false
+ },
+ "inbox": {
+ "max": 20,
+ "type": "MessageBox",
+ "id": "SyENH3Aakw",
+ "stream": null
+ },
+ "sheet": {
+ "cells": {
+ "G1": {
+ "value": "",
+ "type": "string",
+ "level": 0
+ },
+ "A3": {
+ "formula": "REST.REQUEST(|AASRESTTestProducer,\"http://aas-wrapper:6500/streamsheets/temp\",\"GET\",INBOX(\"dataElement\"))",
+ "value": "B14q-zULED",
+ "type": "undefined",
+ "level": 0,
+ "references": [
+ "|AASRESTTestProducer"
+ ]
+ },
+ "B3": {
+ "formula": "READ(INBOXDATA(,,\"content\"),,\"Array\")",
+ "value": "content",
+ "type": "undefined",
+ "level": 0
+ },
+ "C3": {
+ "formula": "READ(INBOXDATA(,,\"timestamp\"),,\"Array\")",
+ "value": "timestamp",
+ "type": "undefined",
+ "level": 0
+ },
+ "G3": {
+ "value": "Time",
+ "type": "string",
+ "level": 0
+ },
+ "H3": {
+ "value": "Temperature",
+ "type": "string",
+ "level": 0
+ },
+ "B4": {
+ "formula": "READ(INBOXDATA(,,B3,\"0\"),H4,\"Number\")",
+ "value": 0,
+ "type": "undefined",
+ "level": 0
+ },
+ "C4": {
+ "formula": "READ(INBOXDATA(,,C3,\"0\"),D4,\"String\")",
+ "value": 0,
+ "type": "undefined",
+ "level": 0
+ },
+ "D4": {
+ "value": "2020-09-09T13:00:40.409Z",
+ "type": "string",
+ "level": 0
+ },
+ "G4": {
+ "formula": "JSONTIME2EXCEL(D4)",
+ "value": 44083.54213436342,
+ "type": "undefined",
+ "level": 0
+ },
+ "H4": {
+ "value": 21.07154194806827,
+ "type": "number",
+ "level": 0
+ },
+ "B5": {
+ "formula": "READ(INBOXDATA(,,B3,\"1\"),H5,\"Number\")",
+ "value": 1,
+ "type": "undefined",
+ "level": 0
+ },
+ "C5": {
+ "formula": "READ(INBOXDATA(,,C3,\"1\"),D5,\"String\")",
+ "value": 1,
+ "type": "undefined",
+ "level": 0
+ },
+ "D5": {
+ "value": "2020-09-09T13:00:41.415Z",
+ "type": "string",
+ "level": 0
+ },
+ "G5": {
+ "formula": "JSONTIME2EXCEL(D5)",
+ "value": 44083.54213436342,
+ "type": "undefined",
+ "level": 0
+ },
+ "H5": {
+ "value": 28.496771926635027,
+ "type": "number",
+ "level": 0
+ },
+ "B6": {
+ "formula": "READ(INBOXDATA(,,B3,\"2\"),H6,\"Number\")",
+ "value": 2,
+ "type": "undefined",
+ "level": 0
+ },
+ "C6": {
+ "formula": "READ(INBOXDATA(,,C3,\"2\"),D6,\"String\")",
+ "value": 2,
+ "type": "undefined",
+ "level": 0
+ },
+ "D6": {
+ "value": "2020-09-09T13:00:42.422Z",
+ "type": "string",
+ "level": 0
+ },
+ "G6": {
+ "formula": "JSONTIME2EXCEL(D6)",
+ "value": 44083.54213436342,
+ "type": "undefined",
+ "level": 0
+ },
+ "H6": {
+ "value": 27.348393994685246,
+ "type": "number",
+ "level": 0
+ },
+ "B7": {
+ "formula": "READ(INBOXDATA(,,B3,\"3\"),H7,\"Number\")",
+ "value": 3,
+ "type": "undefined",
+ "level": 0
+ },
+ "C7": {
+ "formula": "READ(INBOXDATA(,,C3,\"3\"),D7,\"String\")",
+ "value": 3,
+ "type": "undefined",
+ "level": 0
+ },
+ "D7": {
+ "value": "2020-09-09T13:00:43.427Z",
+ "type": "string",
+ "level": 0
+ },
+ "G7": {
+ "formula": "JSONTIME2EXCEL(D7)",
+ "value": 44083.54213436342,
+ "type": "undefined",
+ "level": 0
+ },
+ "H7": {
+ "value": 21.369693237856616,
+ "type": "number",
+ "level": 0
+ },
+ "B8": {
+ "formula": "READ(INBOXDATA(,,B3,\"4\"),H8,\"Number\")",
+ "value": 4,
+ "type": "undefined",
+ "level": 0
+ },
+ "C8": {
+ "formula": "READ(INBOXDATA(,,C3,\"4\"),D8,\"String\")",
+ "value": 4,
+ "type": "undefined",
+ "level": 0
+ },
+ "D8": {
+ "value": "2020-09-09T13:00:44.436Z",
+ "type": "string",
+ "level": 0
+ },
+ "G8": {
+ "formula": "JSONTIME2EXCEL(D8)",
+ "value": 44083.54213436342,
+ "type": "undefined",
+ "level": 0
+ },
+ "H8": {
+ "value": 24.00643356528727,
+ "type": "number",
+ "level": 0
+ },
+ "B9": {
+ "formula": "READ(INBOXDATA(,,B3,\"5\"),H9,\"Number\")",
+ "value": 5,
+ "type": "undefined",
+ "level": 0
+ },
+ "C9": {
+ "formula": "READ(INBOXDATA(,,C3,\"5\"),D9,\"String\")",
+ "value": 5,
+ "type": "undefined",
+ "level": 0
+ },
+ "D9": {
+ "value": "2020-09-09T13:00:45.443Z",
+ "type": "string",
+ "level": 0
+ },
+ "G9": {
+ "formula": "JSONTIME2EXCEL(D9)",
+ "value": 44083.54213436342,
+ "type": "undefined",
+ "level": 0
+ },
+ "H9": {
+ "value": 29.263190638856287,
+ "type": "number",
+ "level": 0
+ },
+ "B10": {
+ "formula": "READ(INBOXDATA(,,B3,\"6\"),H10,\"Number\")",
+ "value": 6,
+ "type": "undefined",
+ "level": 0
+ },
+ "C10": {
+ "formula": "READ(INBOXDATA(,,C3,\"6\"),D10,\"String\")",
+ "value": 6,
+ "type": "undefined",
+ "level": 0
+ },
+ "D10": {
+ "value": "2020-09-09T13:00:46.449Z",
+ "type": "string",
+ "level": 0
+ },
+ "G10": {
+ "formula": "JSONTIME2EXCEL(D10)",
+ "value": 44083.54213436342,
+ "type": "undefined",
+ "level": 0
+ },
+ "H10": {
+ "value": 20.655619621525332,
+ "type": "number",
+ "level": 0
+ },
+ "B11": {
+ "formula": "READ(INBOXDATA(,,B3,\"7\"),H11,\"Number\")",
+ "value": 7,
+ "type": "undefined",
+ "level": 0
+ },
+ "C11": {
+ "formula": "READ(INBOXDATA(,,C3,\"7\"),D11,\"String\")",
+ "value": 7,
+ "type": "undefined",
+ "level": 0
+ },
+ "D11": {
+ "value": "2020-09-09T13:00:47.454Z",
+ "type": "string",
+ "level": 0
+ },
+ "G11": {
+ "formula": "JSONTIME2EXCEL(D11)",
+ "value": 44083.54213436342,
+ "type": "undefined",
+ "level": 0
+ },
+ "H11": {
+ "value": 27.183770324976013,
+ "type": "number",
+ "level": 0
+ },
+ "B12": {
+ "formula": "READ(INBOXDATA(,,B3,\"8\"),H12,\"Number\")",
+ "value": 8,
+ "type": "undefined",
+ "level": 0
+ },
+ "C12": {
+ "formula": "READ(INBOXDATA(,,C3,\"8\"),D12,\"String\")",
+ "value": 8,
+ "type": "undefined",
+ "level": 0
+ },
+ "D12": {
+ "value": "2020-09-09T13:00:48.464Z",
+ "type": "string",
+ "level": 0
+ },
+ "G12": {
+ "formula": "JSONTIME2EXCEL(D12)",
+ "value": 44083.54213436342,
+ "type": "undefined",
+ "level": 0
+ },
+ "H12": {
+ "value": 25.686867950571894,
+ "type": "number",
+ "level": 0
+ },
+ "B13": {
+ "formula": "READ(INBOXDATA(,,B3,\"9\"),H13,\"Number\")",
+ "value": 9,
+ "type": "undefined",
+ "level": 0
+ },
+ "C13": {
+ "formula": "READ(INBOXDATA(,,C3,\"9\"),D13,\"String\")",
+ "value": 9,
+ "type": "undefined",
+ "level": 0
+ },
+ "D13": {
+ "value": "2020-09-09T13:00:49.469Z",
+ "type": "string",
+ "level": 0
+ },
+ "G13": {
+ "formula": "JSONTIME2EXCEL(D13)",
+ "value": 44083.54213436342,
+ "type": "undefined",
+ "level": 0
+ },
+ "H13": {
+ "value": 20.187588152170466,
+ "type": "number",
+ "level": 0
+ },
+ "F24": {
+ "value": "",
+ "type": "string",
+ "level": 0
+ },
+ "F4": {
+ "value": 1,
+ "type": "number",
+ "level": 0
+ },
+ "F5": {
+ "value": 2,
+ "type": "number",
+ "level": 0
+ },
+ "F6": {
+ "value": 3,
+ "type": "number",
+ "level": 0
+ },
+ "F7": {
+ "value": 4,
+ "type": "number",
+ "level": 0
+ },
+ "F8": {
+ "value": 5,
+ "type": "number",
+ "level": 0
+ },
+ "F9": {
+ "value": 6,
+ "type": "number",
+ "level": 0
+ },
+ "F10": {
+ "value": 7,
+ "type": "number",
+ "level": 0
+ },
+ "F11": {
+ "value": 8,
+ "type": "number",
+ "level": 0
+ },
+ "F12": {
+ "value": 9,
+ "type": "number",
+ "level": 0
+ },
+ "F13": {
+ "value": 10,
+ "type": "number",
+ "level": 0
+ },
+ "H18": {
+ "value": "Temperature Sensor",
+ "type": "string",
+ "level": 0
+ }
+ },
+ "namedCells": {},
+ "graphCells": {
+ "70847796661": {
+ "formula": "DRAW.POLYGON(\"70847796661\",,\"Polygon1\",12289,2675,18415,3493,,\"#345678\")",
+ "type": "string",
+ "value": "#CALC"
+ },
+ "555401109981": {
+ "formula": "DRAW.CHART(\"555401109981\",,\"Chart1\",15262,7480,8149,5080,,,,,,,\"column\",S1!G4:H13)",
+ "type": "string",
+ "value": "#CALC"
+ },
+ "60201705755": {
+ "formula": "DRAW.RECTANGLE(\"60201705755\",,\"Rectangle9\",12355,2700,7091,1984,\"None\",FILLPATTERN(\"https://www.basys40.de/wp-content/uploads/2019/12/Logo_BaSys4_1024px-300x79.png\"))",
+ "type": "string",
+ "value": "#CALC"
+ },
+ "488440667154": {
+ "formula": "DRAW.RECTANGLE(\"488440667154\",,\"Rectangle10\",3967,9421,1773,10001,,\"#345678\")",
+ "type": "string",
+ "value": "#CALC"
+ },
+ "919991966926": {
+ "formula": "DRAW.RECTANGLE(\"919991966926\",,\"Rectangle11\",20623,9433,1746,10001,,\"#345678\")",
+ "type": "string",
+ "value": "#CALC"
+ },
+ "962757771226": {
+ "formula": "DRAW.RECTANGLE(\"962757771226\",,\"Rectangle12\",12302,11081,14896,1376,,\"#345678\")",
+ "type": "string",
+ "value": "#CALC"
+ },
+ "692699117567": {
+ "formula": "DRAW.BEZIER(\"692699117567\",,\"Bezier1\",17647,13394,212,1164)",
+ "type": "boolean",
+ "value": true
+ },
+ "426951475952": {
+ "formula": "DRAW.ELLIPSE(\"426951475952\",\"Bezier1\",\"Ellipse1\",106,1032,529,529,,\"#ff0000\")",
+ "type": "string",
+ "value": "#CALC"
+ }
+ },
+ "properties": {
+ "sheet": {},
+ "cols": {},
+ "rows": {},
+ "cells": {}
+ },
+ "settings": {
+ "minrow": 1,
+ "maxrow": 100,
+ "mincol": -2,
+ "maxcol": 50,
+ "protected": false
+ },
+ "drawings": {},
+ "graphItems": {}
+ },
+ "trigger": {
+ "type": "start",
+ "repeat": "endless"
+ }
+ }
+ ],
+ "settings": {
+ "locale": "en",
+ "isOPCUA": false,
+ "cycletime": 200
+ },
+ "className": "Machine",
+ "namedCells": {
+ "|AASRESTTestProducer": {
+ "value": {
+ "id": "HJZR0U1xSI",
+ "name": "AASRESTTestProducer",
+ "type": "producer",
+ "state": "connected",
+ "timestamp": 1599656146504
+ },
+ "type": "object",
+ "info": {},
+ "level": 0
+ },
+ "|MQTT_Consumer": {
+ "value": {
+ "id": "CONSUMER_MQTT",
+ "name": "MQTT_Consumer",
+ "type": "stream",
+ "state": "disconnected",
+ "timestamp": 1599656156501
+ },
+ "type": "object",
+ "info": {},
+ "level": 0
+ },
+ "|Wind_Data": {
+ "value": {
+ "id": "HkZRIJtchH",
+ "name": "Wind_Data",
+ "type": "stream",
+ "state": "disconnected",
+ "timestamp": 1599656156513
+ },
+ "type": "object",
+ "info": {},
+ "level": 0
+ },
+ "|MQTT_Producer": {
+ "value": {
+ "id": "PRODUCER_MQTT",
+ "name": "MQTT_Producer",
+ "type": "producer",
+ "state": "disconnected",
+ "timestamp": 1599656156514
+ },
+ "type": "object",
+ "info": {},
+ "level": 0
+ },
+ "|SMTP_Sender": {
+ "value": {
+ "id": "B1-zoM0x1v",
+ "name": "SMTP_Sender",
+ "type": "producer",
+ "state": "disconnected",
+ "timestamp": 1599656167573
+ },
+ "type": "object",
+ "info": {},
+ "level": 0
+ }
+ },
+ "defproperties": {
+ "attributes": {
+ "cell": {
+ "level": 0,
+ "protected": true,
+ "visible": true,
+ "key": false
+ },
+ "sheet": {
+ "defaultsectionsize": 2000,
+ "initialsection": 0
+ }
+ },
+ "formats": {
+ "styles": {
+ "brightness": 0,
+ "fillstyle": 0,
+ "fillcolor": "#FFFFFF",
+ "gradientcolor": "#CCCCCC",
+ "gradientcolorstops": "",
+ "gradientangle": 0,
+ "gradienttype": 0,
+ "gradientoffset_x": 0,
+ "gradientoffset_y": 0,
+ "linecolor": "#000000",
+ "linewidth": -1,
+ "linestyle": 1,
+ "lineshape": 0,
+ "linetransparency": 100,
+ "pattern": "",
+ "patternstyle": 0,
+ "transparency": 100,
+ "leftbordercolor": "#000000",
+ "leftborderstyle": 0,
+ "leftborderwidth": 1,
+ "topbordercolor": "#000000",
+ "topborderstyle": 0,
+ "topborderwidth": 1,
+ "rightbordercolor": "#000000",
+ "rightborderstyle": 0,
+ "rightborderwidth": 1,
+ "bottombordercolor": "#000000",
+ "bottomborderstyle": 0,
+ "bottomborderwidth": 1
+ },
+ "text": {
+ "baseline": "alphabetic",
+ "valign": 2,
+ "halign": 3,
+ "fontsize": 9,
+ "fontname": "Verdana",
+ "fontcolor": "#000000",
+ "fontstyle": 0,
+ "vposition": 3,
+ "hpostion": 3,
+ "richtext": true,
+ "lineheight": 1.2,
+ "numberformat": "General",
+ "localculture": "general"
+ }
+ }
+ }
+ },
+ "graph": {
+ "id": "SkHS206JD",
+ "graphdef": {
+ "type": "machinegraph",
+ "version": "2",
+ "uniqueid": "1",
+ "o-outbox": {
+ "split": "5000",
+ "width": "5000"
+ },
+ "a-graphitem": [
+ {
+ "id": "1000",
+ "o-attributes": {
+ "o-sheetid": {
+ "v": "Sk7ErnRTJD",
+ "t": "s"
+ }
+ },
+ "o-pin": {
+ "o-p": {
+ "o-x": {
+ "v": "16933"
+ },
+ "o-y": {
+ "v": "7814"
+ }
+ },
+ "o-lp": {
+ "o-x": {
+ "f": "WIDTH%20*%200.5",
+ "v": "16433"
+ },
+ "o-y": {
+ "f": "HEIGHT%20*%200.5",
+ "v": "7314"
+ }
+ }
+ },
+ "o-size": {
+ "o-w": {
+ "v": "32867"
+ },
+ "o-h": {
+ "v": "14628"
+ }
+ },
+ "o-inbox": {
+ "split": "5000",
+ "width": "6111"
+ },
+ "o-processsheet": {
+ "o-attributes": {
+ "o-calcondemand": {
+ "v": "true",
+ "t": "b"
+ }
+ },
+ "o-name": {
+ "v": "S1",
+ "t": "s"
+ },
+ "o-rows": {
+ "a-section": [
+ {
+ "index": "0",
+ "size": "2140",
+ "visible": "1"
+ },
+ {
+ "index": "1",
+ "size": "2458",
+ "visible": "1"
+ },
+ {
+ "index": "2",
+ "size": "632",
+ "visible": "1"
+ },
+ {
+ "index": "15",
+ "size": "1003",
+ "visible": "1"
+ },
+ {
+ "index": "16",
+ "size": "738",
+ "visible": "1"
+ },
+ {
+ "index": "17",
+ "size": "738",
+ "visible": "1"
+ },
+ {
+ "index": "18",
+ "size": "1161",
+ "visible": "1"
+ }
+ ]
+ },
+ "o-columns": {
+ "a-section": [
+ {
+ "index": "0",
+ "size": "0",
+ "visible": "0"
+ },
+ {
+ "index": "1",
+ "size": "515",
+ "visible": "1"
+ },
+ {
+ "index": "2",
+ "size": "0",
+ "visible": "0"
+ },
+ {
+ "index": "3",
+ "size": "0",
+ "visible": "0"
+ },
+ {
+ "index": "4",
+ "size": "0",
+ "visible": "0"
+ },
+ {
+ "index": "5",
+ "size": "0",
+ "visible": "0"
+ },
+ {
+ "index": "6",
+ "size": "4175",
+ "visible": "1"
+ },
+ {
+ "index": "7",
+ "size": "1086",
+ "visible": "1"
+ },
+ {
+ "index": "8",
+ "size": "1921",
+ "visible": "1"
+ },
+ {
+ "index": "9",
+ "size": "3561",
+ "visible": "1"
+ },
+ {
+ "index": "13",
+ "size": "1418",
+ "visible": "1"
+ }
+ ]
+ },
+ "o-drawings": {
+ "a-graphitem": [
+ {
+ "id": "70847796661",
+ "parentid": "1028",
+ "o-al": {
+ "n": "model.attributes",
+ "cl": "JSG.AttributeList",
+ "a-al": [
+ {
+ "n": "format",
+ "cl": "FormatAttributes",
+ "a-al": [
+ {
+ "n": "fillcolor",
+ "o-vl": {
+ "v": "%23345678",
+ "t": "s"
+ }
+ }
+ ]
+ },
+ {
+ "n": "graphitem",
+ "cl": "ItemAttributes",
+ "a-al": [
+ {
+ "n": "sheetformula",
+ "o-vl": {
+ "f": "DRAW.POLYGON(~2270847796661~22%2C%2C~22Polygon1~22%2C12289%2C2675%2C18415%2C3493%2C%2C~22%23345678~22)",
+ "v": "0"
+ }
+ },
+ {
+ "n": "sheetsource",
+ "cl": "StringAttribute",
+ "o-vl": {
+ "v": "name",
+ "t": "s"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "o-type": {
+ "v": "rectCornerCutSame",
+ "t": "s"
+ },
+ "o-name": {
+ "v": "Polygon1",
+ "t": "s"
+ },
+ "o-pin": {
+ "o-p": {
+ "o-x": {
+ "v": "12289"
+ },
+ "o-y": {
+ "v": "2675"
+ }
+ },
+ "o-lp": {
+ "o-x": {
+ "f": "WIDTH%20*%200.5",
+ "v": "9208"
+ },
+ "o-y": {
+ "f": "HEIGHT%20*%200.5",
+ "v": "1746"
+ }
+ }
+ },
+ "o-size": {
+ "o-w": {
+ "v": "18415"
+ },
+ "o-h": {
+ "v": "3493"
+ }
+ },
+ "o-rscoordinates": {
+ "a-c": [
+ {
+ "name": "CUTTOP",
+ "xtype": "13",
+ "ytype": "-1",
+ "xMax": "0.5",
+ "o-x": {
+ "v": "0.10"
+ },
+ "o-y": {
+ "v": "0.00"
+ }
+ },
+ {
+ "name": "CUTBOTTOM",
+ "xtype": "13",
+ "ytype": "-1",
+ "xMax": "0.5",
+ "yMin": "1",
+ "yMax": "1",
+ "o-x": {
+ "v": "0.00"
+ },
+ "o-y": {
+ "v": "1.00"
+ }
+ }
+ ]
+ },
+ "o-shape": {
+ "type": "polygon",
+ "o-cs": {
+ "a-c": [
+ {
+ "o-x": {
+ "f": "MIN(WIDTH%2C%20HEIGHT)%20*%20CUTTOP",
+ "v": "0"
+ },
+ "o-y": {
+ "v": "0"
+ }
+ },
+ {
+ "o-x": {
+ "v": "0"
+ },
+ "o-y": {
+ "f": "MIN(WIDTH%2C%20HEIGHT)%20*%20CUTTOP",
+ "v": "0"
+ }
+ },
+ {
+ "o-x": {
+ "v": "0"
+ },
+ "o-y": {
+ "f": "HEIGHT%20-%20MIN(WIDTH%2C%20HEIGHT)%20*%20CUTBOTTOM",
+ "v": "0"
+ }
+ },
+ {
+ "o-x": {
+ "f": "MIN(WIDTH%2C%20HEIGHT)%20*%20CUTBOTTOM",
+ "v": "0"
+ },
+ "o-y": {
+ "f": "HEIGHT",
+ "v": "3493"
+ }
+ },
+ {
+ "o-x": {
+ "f": "WIDTH%20-%20MIN(WIDTH%2C%20HEIGHT)%20*%20CUTBOTTOM",
+ "v": "0"
+ },
+ "o-y": {
+ "f": "HEIGHT",
+ "v": "3493"
+ }
+ },
+ {
+ "o-x": {
+ "f": "WIDTH",
+ "v": "18415"
+ },
+ "o-y": {
+ "f": "HEIGHT%20-%20MIN(WIDTH%2C%20HEIGHT)%20*%20CUTBOTTOM",
+ "v": "0"
+ }
+ },
+ {
+ "o-x": {
+ "f": "WIDTH",
+ "v": "18415"
+ },
+ "o-y": {
+ "f": "MIN(WIDTH%2C%20HEIGHT)%20*%20CUTTOP",
+ "v": "0"
+ }
+ },
+ {
+ "o-x": {
+ "f": "WIDTH%20-%20MIN(WIDTH%2C%20HEIGHT)%20*%20CUTTOP",
+ "v": "0"
+ },
+ "o-y": {
+ "v": "0"
+ }
+ }
+ ]
+ }
+ },
+ "type": "node"
+ },
+ {
+ "id": "555401109981",
+ "parentid": "1028",
+ "o-al": {
+ "n": "model.attributes",
+ "cl": "JSG.AttributeList",
+ "a-al": [
+ {
+ "n": "graphitem",
+ "cl": "ItemAttributes",
+ "a-al": [
+ {
+ "n": "portmode",
+ "o-vl": {
+ "v": "0"
+ }
+ },
+ {
+ "n": "sheetformula",
+ "o-vl": {
+ "f": "DRAW.CHART(~22555401109981~22%2C%2C~22Chart1~22%2C15262%2C7480%2C8149%2C5080%2C%2C%2C%2C%2C%2C%2C~22column~22%2CS1!G4%3AH13)",
+ "v": "0"
+ }
+ },
+ {
+ "n": "sheetsource",
+ "cl": "StringAttribute",
+ "o-vl": {
+ "v": "name",
+ "t": "s"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "o-name": {
+ "v": "Chart1",
+ "t": "s"
+ },
+ "o-pin": {
+ "o-p": {
+ "o-x": {
+ "v": "15262"
+ },
+ "o-y": {
+ "v": "7480"
+ }
+ },
+ "o-lp": {
+ "o-x": {
+ "f": "WIDTH%20*%200.5",
+ "v": "4074"
+ },
+ "o-y": {
+ "f": "HEIGHT%20*%200.5",
+ "v": "2540"
+ }
+ }
+ },
+ "o-size": {
+ "o-w": {
+ "v": "8149"
+ },
+ "o-h": {
+ "v": "5080"
+ }
+ },
+ "o-shape": {
+ "type": "rectangle"
+ },
+ "type": "chartnode",
+ "data": "{"chartType":"column","coherent":true,"hasCategoryLabels":"first","hasSeriesLabels":"first","categoryLabelData":[" 14:46:20 GM"," 14:46:21 GM"," 14:46:22 GM"," 14:46:23 GM"," 14:46:24 GM"," 14:46:25 GM"," 14:46:26 GM"," 14:46:27 GM"," 14:46:28 GM"," 14:46:29 GM"],"direction":"columns","range":"=S1!G4:H13","formatRange":"","categoryRange":"=E5:E14","stacked":false,"smooth":true,"step":false,"fill":false,"hideEmpty":"none","angle":6.283185307179586,"series":[{"categoryLabels":"=E5:E14","seriesLabelRange":"=F4","data":"=F5:F14","seriesLabel":"Temperature","fillColor":"#4a90e2","lineColor":"#4a90e2"}]}",
+ "title": "{"title":"","font":{"fontName":"Verdana","fontSize":"12","bold":false,"italic":false,"color":"#000000"}}",
+ "legend": "{"position":"top","font":{"fontName":"Verdana","fontSize":"7","bold":false,"italic":false,"color":"#000000"}}",
+ "scales": "{"xAxes":[{"id":"XAxis1","ticks":{"fontFamily":"Verdana","fontSize":"7","fontColor":"#000000","fontStyle":"normal","minRotation":0,"maxRotation":90,"reverse":false},"gridLines":{"display":false,"color":"#DDDDDD"},"stacked":false,"type":"category","position":"bottom","scaleLabel":{"labelString":"Time","display":true}}],"yAxes":[{"id":"YAxis1","ticks":{"fontFamily":"Verdana","fontSize":"7","fontColor":"#000000","fontStyle":"bold","minRotation":0,"maxRotation":90,"reverse":false,"min":20},"gridLines":{"display":true,"color":"#DDDDDD"},"stacked":false,"type":"linear","position":"left","scaleLabel":{"labelString":"Temperature (ºC)","display":true}}]}"
+ },
+ {
+ "id": "60201705755",
+ "parentid": "1028",
+ "o-al": {
+ "n": "model.attributes",
+ "cl": "JSG.AttributeList",
+ "a-al": [
+ {
+ "n": "format",
+ "cl": "FormatAttributes",
+ "a-al": [
+ {
+ "n": "fillstyle",
+ "o-vl": {
+ "v": "3"
+ }
+ },
+ {
+ "n": "linecolor",
+ "o-vl": {
+ "v": "None",
+ "t": "s"
+ }
+ },
+ {
+ "n": "linestyle",
+ "o-vl": {
+ "v": "0"
+ }
+ }
+ ]
+ },
+ {
+ "n": "graphitem",
+ "cl": "ItemAttributes",
+ "a-al": [
+ {
+ "n": "sheetformula",
+ "o-vl": {
+ "f": "DRAW.RECTANGLE(~2260201705755~22%2C%2C~22Rectangle9~22%2C12355%2C2700%2C7091%2C1984%2C~22None~22%2CFILLPATTERN(~22https%3A%2F%2Fwww.basys40.de%2Fwp-content%2Fuploads%2F2019%2F12%2FLogo_BaSys4_1024px-300x79.png~22))",
+ "v": "0"
+ }
+ },
+ {
+ "n": "sheetsource",
+ "cl": "StringAttribute",
+ "o-vl": {
+ "v": "name",
+ "t": "s"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "o-name": {
+ "v": "Rectangle9",
+ "t": "s"
+ },
+ "o-pin": {
+ "o-p": {
+ "o-x": {
+ "v": "12355"
+ },
+ "o-y": {
+ "v": "2700"
+ }
+ },
+ "o-lp": {
+ "o-x": {
+ "f": "WIDTH%20*%200.5",
+ "v": "3545"
+ },
+ "o-y": {
+ "f": "HEIGHT%20*%200.5",
+ "v": "992"
+ }
+ }
+ },
+ "o-size": {
+ "o-w": {
+ "v": "7091"
+ },
+ "o-h": {
+ "v": "1984"
+ }
+ },
+ "o-shape": {
+ "type": "rectangle"
+ },
+ "type": "node"
+ },
+ {
+ "id": "488440667154",
+ "parentid": "1028",
+ "o-al": {
+ "n": "model.attributes",
+ "cl": "JSG.AttributeList",
+ "a-al": [
+ {
+ "n": "format",
+ "cl": "FormatAttributes",
+ "a-al": [
+ {
+ "n": "fillcolor",
+ "o-vl": {
+ "v": "%23345678",
+ "t": "s"
+ }
+ }
+ ]
+ },
+ {
+ "n": "graphitem",
+ "cl": "ItemAttributes",
+ "a-al": [
+ {
+ "n": "sheetformula",
+ "o-vl": {
+ "f": "DRAW.RECTANGLE(~22488440667154~22%2C%2C~22Rectangle10~22%2C3967%2C9421%2C1773%2C10001%2C%2C~22%23345678~22)",
+ "v": "0"
+ }
+ },
+ {
+ "n": "sheetsource",
+ "cl": "StringAttribute",
+ "o-vl": {
+ "v": "name",
+ "t": "s"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "o-name": {
+ "v": "Rectangle10",
+ "t": "s"
+ },
+ "o-pin": {
+ "o-p": {
+ "o-x": {
+ "v": "3967"
+ },
+ "o-y": {
+ "v": "9421"
+ }
+ },
+ "o-lp": {
+ "o-x": {
+ "f": "WIDTH%20*%200.5",
+ "v": "886"
+ },
+ "o-y": {
+ "f": "HEIGHT%20*%200.5",
+ "v": "5001"
+ }
+ }
+ },
+ "o-size": {
+ "o-w": {
+ "v": "1773"
+ },
+ "o-h": {
+ "v": "10001"
+ }
+ },
+ "o-shape": {
+ "type": "rectangle"
+ },
+ "type": "node"
+ },
+ {
+ "id": "919991966926",
+ "parentid": "1028",
+ "o-al": {
+ "n": "model.attributes",
+ "cl": "JSG.AttributeList",
+ "a-al": [
+ {
+ "n": "format",
+ "cl": "FormatAttributes",
+ "a-al": [
+ {
+ "n": "fillcolor",
+ "o-vl": {
+ "v": "%23345678",
+ "t": "s"
+ }
+ }
+ ]
+ },
+ {
+ "n": "graphitem",
+ "cl": "ItemAttributes",
+ "a-al": [
+ {
+ "n": "sheetformula",
+ "o-vl": {
+ "f": "DRAW.RECTANGLE(~22919991966926~22%2C%2C~22Rectangle11~22%2C20623%2C9433%2C1746%2C10001%2C%2C~22%23345678~22)",
+ "v": "0"
+ }
+ },
+ {
+ "n": "sheetsource",
+ "cl": "StringAttribute",
+ "o-vl": {
+ "v": "name",
+ "t": "s"
+ }
+ }
+ ]
+ },
+ {
+ "n": "textformat",
+ "cl": "TextFormatAttributes",
+ "pl": "tl:TextFormatAttributes.Template",
+ "a-al": [
+ {
+ "n": "richtext",
+ "o-vl": {
+ "v": "false",
+ "t": "b"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "o-name": {
+ "v": "Rectangle11",
+ "t": "s"
+ },
+ "o-pin": {
+ "o-p": {
+ "o-x": {
+ "v": "20623"
+ },
+ "o-y": {
+ "v": "9433"
+ }
+ },
+ "o-lp": {
+ "o-x": {
+ "f": "WIDTH%20*%200.5",
+ "v": "873"
+ },
+ "o-y": {
+ "f": "HEIGHT%20*%200.5",
+ "v": "5001"
+ }
+ }
+ },
+ "o-size": {
+ "o-w": {
+ "v": "1746"
+ },
+ "o-h": {
+ "v": "10001"
+ }
+ },
+ "o-shape": {
+ "type": "rectangle"
+ },
+ "type": "node"
+ },
+ {
+ "id": "962757771226",
+ "parentid": "1028",
+ "o-al": {
+ "n": "model.attributes",
+ "cl": "JSG.AttributeList",
+ "a-al": [
+ {
+ "n": "format",
+ "cl": "FormatAttributes",
+ "a-al": [
+ {
+ "n": "fillcolor",
+ "o-vl": {
+ "v": "%23345678",
+ "t": "s"
+ }
+ }
+ ]
+ },
+ {
+ "n": "graphitem",
+ "cl": "ItemAttributes",
+ "a-al": [
+ {
+ "n": "sheetformula",
+ "o-vl": {
+ "f": "DRAW.RECTANGLE(~22962757771226~22%2C%2C~22Rectangle12~22%2C12302%2C11081%2C14896%2C1376%2C%2C~22%23345678~22)",
+ "v": "0"
+ }
+ },
+ {
+ "n": "sheetsource",
+ "cl": "StringAttribute",
+ "o-vl": {
+ "v": "name",
+ "t": "s"
+ }
+ }
+ ]
+ },
+ {
+ "n": "textformat",
+ "cl": "TextFormatAttributes",
+ "pl": "tl:TextFormatAttributes.Template",
+ "a-al": [
+ {
+ "n": "richtext",
+ "o-vl": {
+ "v": "false",
+ "t": "b"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "o-name": {
+ "v": "Rectangle12",
+ "t": "s"
+ },
+ "o-pin": {
+ "o-p": {
+ "o-x": {
+ "v": "12302"
+ },
+ "o-y": {
+ "v": "11081"
+ }
+ },
+ "o-lp": {
+ "o-x": {
+ "f": "WIDTH%20*%200.5",
+ "v": "7448"
+ },
+ "o-y": {
+ "f": "HEIGHT%20*%200.5",
+ "v": "688"
+ }
+ }
+ },
+ "o-size": {
+ "o-w": {
+ "v": "14896"
+ },
+ "o-h": {
+ "v": "1376"
+ }
+ },
+ "o-shape": {
+ "type": "rectangle"
+ },
+ "type": "node"
+ },
+ {
+ "id": "692699117567",
+ "parentid": "1028",
+ "o-al": {
+ "n": "model.attributes",
+ "cl": "JSG.AttributeList",
+ "a-al": [
+ {
+ "n": "graphitem",
+ "cl": "ItemAttributes",
+ "a-al": [
+ {
+ "n": "sheetformula",
+ "o-vl": {
+ "f": "DRAW.BEZIER(~22692699117567~22%2C%2C~22Bezier1~22%2C17647%2C13394%2C212%2C1164)",
+ "v": "0"
+ }
+ },
+ {
+ "n": "sheetsource",
+ "cl": "StringAttribute",
+ "o-vl": {
+ "v": "name",
+ "t": "s"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "o-type": {
+ "v": "roundRectCornerCutSame",
+ "t": "s"
+ },
+ "o-name": {
+ "v": "Bezier1",
+ "t": "s"
+ },
+ "o-pin": {
+ "o-p": {
+ "o-x": {
+ "v": "17647"
+ },
+ "o-y": {
+ "v": "13394"
+ }
+ },
+ "o-lp": {
+ "o-x": {
+ "f": "WIDTH%20*%200.5",
+ "v": "106"
+ },
+ "o-y": {
+ "f": "HEIGHT%20*%200.5",
+ "v": "582"
+ }
+ }
+ },
+ "o-size": {
+ "o-w": {
+ "v": "212"
+ },
+ "o-h": {
+ "v": "1164"
+ }
+ },
+ "o-rscoordinates": {
+ "a-c": [
+ {
+ "name": "ROUND",
+ "xtype": "14",
+ "ytype": "-1",
+ "xMax": "0.5",
+ "o-x": {
+ "v": "0.10"
+ },
+ "o-y": {
+ "v": "0.00"
+ }
+ },
+ {
+ "name": "ROUND2",
+ "xtype": "13",
+ "ytype": "-1",
+ "xMax": "0.5",
+ "yMin": "1",
+ "yMax": "1",
+ "o-x": {
+ "v": "0.00"
+ },
+ "o-y": {
+ "v": "1.00"
+ }
+ }
+ ]
+ },
+ "o-shape": {
+ "type": "bezier",
+ "o-cs": {
+ "a-c": [
+ {
+ "o-x": {
+ "f": "MIN(WIDTH%2C%20HEIGHT)%20*%20ROUND",
+ "v": "0"
+ },
+ "o-y": {
+ "v": "0"
+ }
+ },
+ {
+ "o-x": {
+ "f": "WIDTH%20-%20MIN(WIDTH%2C%20HEIGHT)%20*%20ROUND",
+ "v": "0"
+ },
+ "o-y": {
+ "v": "0"
+ }
+ },
+ {
+ "o-x": {
+ "f": "WIDTH",
+ "v": "212"
+ },
+ "o-y": {
+ "f": "MIN(WIDTH%2C%20HEIGHT)%20*%20ROUND",
+ "v": "0"
+ }
+ },
+ {
+ "o-x": {
+ "f": "WIDTH",
+ "v": "212"
+ },
+ "o-y": {
+ "f": "HEIGHT%20-%20ROUND2%20*%20MIN(WIDTH%2C%20HEIGHT)",
+ "v": "0"
+ }
+ },
+ {
+ "o-x": {
+ "f": "WIDTH%20-%20MIN(WIDTH%2C%20HEIGHT)%20*%20ROUND2",
+ "v": "0"
+ },
+ "o-y": {
+ "f": "HEIGHT",
+ "v": "1164"
+ }
+ },
+ {
+ "o-x": {
+ "f": "MIN(WIDTH%2C%20HEIGHT)%20*%20ROUND2",
+ "v": "0"
+ },
+ "o-y": {
+ "f": "HEIGHT",
+ "v": "1164"
+ }
+ },
+ {
+ "o-x": {
+ "v": "0"
+ },
+ "o-y": {
+ "f": "HEIGHT%20-%20MIN(WIDTH%2C%20HEIGHT)%20*%20ROUND2",
+ "v": "0"
+ }
+ },
+ {
+ "o-x": {
+ "v": "0"
+ },
+ "o-y": {
+ "f": "MIN(WIDTH%2C%20HEIGHT)%20*%20ROUND",
+ "v": "0"
+ }
+ }
+ ]
+ },
+ "o-cpfrom": {
+ "a-c": [
+ {
+ "o-x": {
+ "f": "MIN(WIDTH%2C%20HEIGHT)%20*%20ROUND%20*%200.45",
+ "v": "0"
+ },
+ "o-y": {
+ "v": "0"
+ }
+ },
+ {
+ "o-x": {
+ "f": "MIN(WIDTH%2C%20HEIGHT)%20*%200.6",
+ "v": "0"
+ },
+ "o-y": {
+ "v": "0"
+ }
+ },
+ {
+ "o-x": {
+ "f": "WIDTH",
+ "v": "212"
+ },
+ "o-y": {
+ "f": "MIN(WIDTH%2C%20HEIGHT)%20*%20ROUND%20*%200.45",
+ "v": "0"
+ }
+ },
+ {
+ "o-x": {
+ "f": "WIDTH",
+ "v": "212"
+ },
+ "o-y": {
+ "f": "MIN(WIDTH%2C%20HEIGHT)%20*%200.6",
+ "v": "0"
+ }
+ },
+ {
+ "o-x": {
+ "f": "WIDTH%20-%20MIN(WIDTH%2C%20HEIGHT)%20*%20ROUND2%20*%200.45",
+ "v": "0"
+ },
+ "o-y": {
+ "f": "HEIGHT",
+ "v": "1164"
+ }
+ },
+ {
+ "o-x": {
+ "f": "MIN(WIDTH%2C%20HEIGHT)%20*%200.4",
+ "v": "0"
+ },
+ "o-y": {
+ "f": "HEIGHT",
+ "v": "1164"
+ }
+ },
+ {
+ "o-x": {
+ "v": "0"
+ },
+ "o-y": {
+ "f": "HEIGHT%20-%20MIN(WIDTH%2C%20HEIGHT)%20*%20ROUND2%20*%200.45",
+ "v": "0"
+ }
+ },
+ {
+ "o-x": {
+ "v": "0"
+ },
+ "o-y": {
+ "f": "MIN(WIDTH%2C%20HEIGHT)%20*%200.4",
+ "v": "0"
+ }
+ }
+ ]
+ },
+ "o-cpto": {
+ "a-c": [
+ {
+ "o-x": {
+ "f": "MIN(WIDTH%2C%20HEIGHT)%20*%200.4",
+ "v": "0"
+ },
+ "o-y": {
+ "v": "0"
+ }
+ },
+ {
+ "o-x": {
+ "f": "WIDTH%20-%20MIN(WIDTH%2C%20HEIGHT)%20*%20ROUND%20*%200.45",
+ "v": "0"
+ },
+ "o-y": {
+ "v": "0"
+ }
+ },
+ {
+ "o-x": {
+ "f": "WIDTH",
+ "v": "212"
+ },
+ "o-y": {
+ "f": "MIN(WIDTH%2C%20HEIGHT)%20*%200.4",
+ "v": "0"
+ }
+ },
+ {
+ "o-x": {
+ "f": "WIDTH",
+ "v": "212"
+ },
+ "o-y": {
+ "f": "HEIGHT%20-%20MIN(WIDTH%2C%20HEIGHT)%20*%20ROUND2%20*%200.45",
+ "v": "0"
+ }
+ },
+ {
+ "o-x": {
+ "f": "MIN(WIDTH%2C%20HEIGHT)%20*%200.6",
+ "v": "0"
+ },
+ "o-y": {
+ "f": "HEIGHT",
+ "v": "1164"
+ }
+ },
+ {
+ "o-x": {
+ "f": "MIN(WIDTH%2C%20HEIGHT)%20*%20ROUND2%20*%200.45",
+ "v": "0"
+ },
+ "o-y": {
+ "f": "HEIGHT",
+ "v": "1164"
+ }
+ },
+ {
+ "o-x": {
+ "v": "0"
+ },
+ "o-y": {
+ "f": "MIN(WIDTH%2C%20HEIGHT)%20*%200.6",
+ "v": "0"
+ }
+ },
+ {
+ "o-x": {
+ "v": "0"
+ },
+ "o-y": {
+ "f": "MIN(WIDTH%2C%20HEIGHT)%20*%20ROUND%20*%200.45",
+ "v": "0"
+ }
+ }
+ ],
+ "pie": "false"
+ }
+ },
+ "a-graphitem": [
+ {
+ "id": "426951475952",
+ "parentid": "692699117567",
+ "o-al": {
+ "n": "model.attributes",
+ "cl": "JSG.AttributeList",
+ "a-al": [
+ {
+ "n": "format",
+ "cl": "FormatAttributes",
+ "a-al": [
+ {
+ "n": "fillcolor",
+ "o-vl": {
+ "v": "%23ff0000",
+ "t": "s"
+ }
+ }
+ ]
+ },
+ {
+ "n": "graphitem",
+ "cl": "ItemAttributes",
+ "a-al": [
+ {
+ "n": "sheetformula",
+ "o-vl": {
+ "f": "DRAW.ELLIPSE(~22426951475952~22%2C~22Bezier1~22%2C~22Ellipse1~22%2C106%2C1032%2C529%2C529%2C%2C~22%23ff0000~22)",
+ "v": "0"
+ }
+ },
+ {
+ "n": "sheetsource",
+ "cl": "StringAttribute",
+ "o-vl": {
+ "v": "name",
+ "t": "s"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ "o-name": {
+ "v": "Ellipse1",
+ "t": "s"
+ },
+ "o-pin": {
+ "o-p": {
+ "o-x": {
+ "v": "106"
+ },
+ "o-y": {
+ "v": "1032"
+ }
+ },
+ "o-lp": {
+ "o-x": {
+ "f": "WIDTH%20*%200.5",
+ "v": "265"
+ },
+ "o-y": {
+ "f": "HEIGHT%20*%200.5",
+ "v": "265"
+ }
+ }
+ },
+ "o-size": {
+ "o-w": {
+ "v": "529"
+ },
+ "o-h": {
+ "v": "529"
+ }
+ },
+ "o-shape": {
+ "type": "ellipse",
+ "o-cs": {
+ "a-c": [
+ {
+ "o-x": {
+ "f": "WIDTH%20*%200.5",
+ "v": "265"
+ },
+ "o-y": {
+ "v": "0"
+ }
+ },
+ {
+ "o-x": {
+ "f": "WIDTH",
+ "v": "529"
+ },
+ "o-y": {
+ "f": "HEIGHT%20*%200.5",
+ "v": "265"
+ }
+ },
+ {
+ "o-x": {
+ "f": "WIDTH%20*%200.5",
+ "v": "265"
+ },
+ "o-y": {
+ "f": "HEIGHT",
+ "v": "529"
+ }
+ },
+ {
+ "o-x": {
+ "v": "0"
+ },
+ "o-y": {
+ "f": "HEIGHT%20*%200.5",
+ "v": "265"
+ }
+ }
+ ]
+ },
+ "o-cpfrom": {
+ "a-c": [
+ {
+ "o-x": {
+ "f": "WIDTH%20*%200.225",
+ "v": "119"
+ },
+ "o-y": {
+ "v": "0"
+ }
+ },
+ {
+ "o-x": {
+ "f": "WIDTH",
+ "v": "529"
+ },
+ "o-y": {
+ "f": "HEIGHT%20*%200.225",
+ "v": "119"
+ }
+ },
+ {
+ "o-x": {
+ "f": "WIDTH%20*%200.775",
+ "v": "410"
+ },
+ "o-y": {
+ "f": "HEIGHT",
+ "v": "529"
+ }
+ },
+ {
+ "o-x": {
+ "v": "0"
+ },
+ "o-y": {
+ "f": "HEIGHT%20*%200.775",
+ "v": "410"
+ }
+ }
+ ]
+ },
+ "o-cpto": {
+ "a-c": [
+ {
+ "o-x": {
+ "f": "WIDTH%20*%200.775",
+ "v": "410"
+ },
+ "o-y": {
+ "v": "0"
+ }
+ },
+ {
+ "o-x": {
+ "f": "WIDTH",
+ "v": "529"
+ },
+ "o-y": {
+ "f": "HEIGHT%20*%200.775",
+ "v": "410"
+ }
+ },
+ {
+ "o-x": {
+ "f": "WIDTH%20*%200.225",
+ "v": "119"
+ },
+ "o-y": {
+ "f": "HEIGHT",
+ "v": "529"
+ }
+ },
+ {
+ "o-x": {
+ "v": "0"
+ },
+ "o-y": {
+ "f": "HEIGHT%20*%200.225",
+ "v": "119"
+ }
+ }
+ ],
+ "pie": "false"
+ }
+ },
+ "type": "node"
+ }
+ ],
+ "type": "node"
+ }
+ ]
+ },
+ "o-defaultcell": {
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23ffffff",
+ "t": "s"
+ },
+ "o-fillstyle": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ "o-data": {
+ "a-r": [
+ {
+ "n": "0",
+ "a-c": [
+ {
+ "n": "6",
+ "o-cell": {
+ "o-t": {
+ "o-numberformat": {
+ "v": "h%3Amm%3Ass%20AM%2FPM",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "time%3Ben",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "7",
+ "o-cell": {
+ "o-t": {
+ "o-numberformat": {
+ "v": "h%3Amm%3Ass",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "time%3Ben",
+ "t": "s"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "1",
+ "a-c": [
+ {
+ "n": "3",
+ "o-cell": {
+ "o-t": {
+ "o-fontstyle": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "7",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "8",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "9",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "10",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "11",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "12",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "13",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "14",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "2",
+ "a-c": [
+ {
+ "n": "4",
+ "o-cell": {
+ "o-t": {
+ "o-fontstyle": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "7",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-fontsize": {
+ "v": "12"
+ },
+ "o-fontcolor": {
+ "v": "%230f4db5",
+ "t": "s"
+ }
+ },
+ "o-a": {
+ "o-leftbordercolor": {
+ "v": "%230887fb",
+ "t": "s"
+ },
+ "o-topbordercolor": {
+ "v": "%230887fb",
+ "t": "s"
+ },
+ "o-rightbordercolor": {
+ "v": "%230887fb",
+ "t": "s"
+ },
+ "o-bottombordercolor": {
+ "v": "%230887fb",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "8",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-halign": {
+ "v": "1"
+ },
+ "o-fontsize": {
+ "v": "12"
+ },
+ "o-fontcolor": {
+ "v": "%231758ab",
+ "t": "s"
+ }
+ },
+ "o-a": {
+ "o-leftbordercolor": {
+ "v": "%23f8e71c",
+ "t": "s"
+ },
+ "o-topbordercolor": {
+ "v": "%23f8e71c",
+ "t": "s"
+ },
+ "o-rightbordercolor": {
+ "v": "%23f8e71c",
+ "t": "s"
+ },
+ "o-bottombordercolor": {
+ "v": "%23f8e71c",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "9",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-halign": {
+ "v": "1"
+ },
+ "o-fontsize": {
+ "v": "12"
+ },
+ "o-fontcolor": {
+ "v": "%231758ab",
+ "t": "s"
+ }
+ },
+ "o-a": {
+ "o-leftbordercolor": {
+ "v": "%23f8e71c",
+ "t": "s"
+ },
+ "o-topbordercolor": {
+ "v": "%23f8e71c",
+ "t": "s"
+ },
+ "o-rightbordercolor": {
+ "v": "%23f8e71c",
+ "t": "s"
+ },
+ "o-bottombordercolor": {
+ "v": "%23f8e71c",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "10",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "11",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "12",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "13",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "14",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "3",
+ "a-c": [
+ {
+ "n": "3",
+ "o-cell": {
+ "o-a": {
+ "o-key": {
+ "v": "true",
+ "t": "b"
+ }
+ }
+ }
+ },
+ {
+ "n": "5",
+ "o-cell": {
+ "o-t": {
+ "o-numberformat": {
+ "v": "0",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "number%3B0%3Bfalse",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "6",
+ "o-cell": {
+ "o-t": {
+ "o-numberformat": {
+ "v": "h%3Amm%3Ass%20AM%2FPM",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "time%3Ben",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "7",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-fontcolor": {
+ "v": "%230f4db5",
+ "t": "s"
+ },
+ "o-fontsize": {
+ "v": "10"
+ }
+ },
+ "o-a": {
+ "o-key": {
+ "v": "true",
+ "t": "b"
+ }
+ }
+ }
+ },
+ {
+ "n": "8",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-numberformat": {
+ "v": "hh%3Amm%3Ass",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "time%3Ben",
+ "t": "s"
+ },
+ "o-halign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "9",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-halign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "10",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "11",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "12",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "13",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "14",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "4",
+ "a-c": [
+ {
+ "n": "3",
+ "o-cell": {
+ "o-a": {
+ "o-key": {
+ "v": "true",
+ "t": "b"
+ }
+ }
+ }
+ },
+ {
+ "n": "5",
+ "o-cell": {
+ "o-t": {
+ "o-numberformat": {
+ "v": "d~5C.m~5C.yyyy%20h%3Amm",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "date%3Bde",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "6",
+ "o-cell": {
+ "o-t": {
+ "o-numberformat": {
+ "v": "h%3Amm%3Ass%20AM%2FPM",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "time%3Ben",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "7",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-fontcolor": {
+ "v": "%230f4db5",
+ "t": "s"
+ },
+ "o-fontsize": {
+ "v": "10"
+ }
+ },
+ "o-a": {
+ "o-key": {
+ "v": "true",
+ "t": "b"
+ }
+ }
+ }
+ },
+ {
+ "n": "8",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-numberformat": {
+ "v": "hh%3Amm%3Ass",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "time%3Ben",
+ "t": "s"
+ },
+ "o-halign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "9",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-halign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "10",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "11",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "12",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "13",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "14",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "5",
+ "a-c": [
+ {
+ "n": "3",
+ "o-cell": {
+ "o-a": {
+ "o-key": {
+ "v": "true",
+ "t": "b"
+ }
+ }
+ }
+ },
+ {
+ "n": "5",
+ "o-cell": {
+ "o-t": {
+ "o-numberformat": {
+ "v": "d~5C.m~5C.yyyy%20h%3Amm",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "date%3Bde",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "6",
+ "o-cell": {
+ "o-t": {
+ "o-numberformat": {
+ "v": "h%3Amm%3Ass%20AM%2FPM",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "time%3Ben",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "7",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-fontcolor": {
+ "v": "%230f4db5",
+ "t": "s"
+ },
+ "o-fontsize": {
+ "v": "10"
+ }
+ },
+ "o-a": {
+ "o-key": {
+ "v": "true",
+ "t": "b"
+ }
+ }
+ }
+ },
+ {
+ "n": "8",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-numberformat": {
+ "v": "hh%3Amm%3Ass",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "time%3Ben",
+ "t": "s"
+ },
+ "o-halign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "9",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-halign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "10",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "11",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "12",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "13",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "14",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "6",
+ "a-c": [
+ {
+ "n": "3",
+ "o-cell": {
+ "o-a": {
+ "o-key": {
+ "v": "true",
+ "t": "b"
+ }
+ }
+ }
+ },
+ {
+ "n": "5",
+ "o-cell": {
+ "o-t": {
+ "o-numberformat": {
+ "v": "d~5C.m~5C.yyyy%20h%3Amm",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "date%3Bde",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "6",
+ "o-cell": {
+ "o-t": {
+ "o-numberformat": {
+ "v": "h%3Amm%3Ass%20AM%2FPM",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "time%3Ben",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "7",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-fontcolor": {
+ "v": "%230f4db5",
+ "t": "s"
+ },
+ "o-fontsize": {
+ "v": "10"
+ }
+ },
+ "o-a": {
+ "o-key": {
+ "v": "true",
+ "t": "b"
+ }
+ }
+ }
+ },
+ {
+ "n": "8",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-numberformat": {
+ "v": "hh%3Amm%3Ass",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "time%3Ben",
+ "t": "s"
+ },
+ "o-halign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "9",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-halign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "10",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "11",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "12",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "13",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "14",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "7",
+ "a-c": [
+ {
+ "n": "3",
+ "o-cell": {
+ "o-a": {
+ "o-key": {
+ "v": "true",
+ "t": "b"
+ }
+ }
+ }
+ },
+ {
+ "n": "5",
+ "o-cell": {
+ "o-t": {
+ "o-numberformat": {
+ "v": "d~5C.m~5C.yyyy%20h%3Amm",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "date%3Bde",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "6",
+ "o-cell": {
+ "o-t": {
+ "o-numberformat": {
+ "v": "h%3Amm%3Ass%20AM%2FPM",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "time%3Ben",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "7",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-fontcolor": {
+ "v": "%230f4db5",
+ "t": "s"
+ },
+ "o-fontsize": {
+ "v": "10"
+ }
+ },
+ "o-a": {
+ "o-key": {
+ "v": "true",
+ "t": "b"
+ }
+ }
+ }
+ },
+ {
+ "n": "8",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-numberformat": {
+ "v": "hh%3Amm%3Ass",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "time%3Ben",
+ "t": "s"
+ },
+ "o-halign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "9",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-halign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "10",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "11",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "12",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "13",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "14",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "8",
+ "a-c": [
+ {
+ "n": "3",
+ "o-cell": {
+ "o-a": {
+ "o-key": {
+ "v": "true",
+ "t": "b"
+ }
+ }
+ }
+ },
+ {
+ "n": "5",
+ "o-cell": {
+ "o-t": {
+ "o-numberformat": {
+ "v": "d~5C.m~5C.yyyy%20h%3Amm",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "date%3Bde",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "6",
+ "o-cell": {
+ "o-t": {
+ "o-numberformat": {
+ "v": "h%3Amm%3Ass%20AM%2FPM",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "time%3Ben",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "7",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-fontcolor": {
+ "v": "%230f4db5",
+ "t": "s"
+ },
+ "o-fontsize": {
+ "v": "10"
+ }
+ },
+ "o-a": {
+ "o-key": {
+ "v": "true",
+ "t": "b"
+ }
+ }
+ }
+ },
+ {
+ "n": "8",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-numberformat": {
+ "v": "hh%3Amm%3Ass",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "time%3Ben",
+ "t": "s"
+ },
+ "o-halign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "9",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-halign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "10",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "11",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "12",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "13",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "14",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "9",
+ "a-c": [
+ {
+ "n": "3",
+ "o-cell": {
+ "o-a": {
+ "o-key": {
+ "v": "true",
+ "t": "b"
+ }
+ }
+ }
+ },
+ {
+ "n": "5",
+ "o-cell": {
+ "o-t": {
+ "o-numberformat": {
+ "v": "d~5C.m~5C.yyyy%20h%3Amm",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "date%3Bde",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "6",
+ "o-cell": {
+ "o-t": {
+ "o-numberformat": {
+ "v": "h%3Amm%3Ass%20AM%2FPM",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "time%3Ben",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "7",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-fontcolor": {
+ "v": "%230f4db5",
+ "t": "s"
+ },
+ "o-fontsize": {
+ "v": "10"
+ }
+ },
+ "o-a": {
+ "o-key": {
+ "v": "true",
+ "t": "b"
+ }
+ }
+ }
+ },
+ {
+ "n": "8",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-numberformat": {
+ "v": "hh%3Amm%3Ass",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "time%3Ben",
+ "t": "s"
+ },
+ "o-halign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "9",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-halign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "10",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "11",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "12",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "13",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "14",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "10",
+ "a-c": [
+ {
+ "n": "3",
+ "o-cell": {
+ "o-a": {
+ "o-key": {
+ "v": "true",
+ "t": "b"
+ }
+ }
+ }
+ },
+ {
+ "n": "5",
+ "o-cell": {
+ "o-t": {
+ "o-numberformat": {
+ "v": "d~5C.m~5C.yyyy%20h%3Amm",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "date%3Bde",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "6",
+ "o-cell": {
+ "o-t": {
+ "o-numberformat": {
+ "v": "h%3Amm%3Ass%20AM%2FPM",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "time%3Ben",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "7",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-fontcolor": {
+ "v": "%230f4db5",
+ "t": "s"
+ },
+ "o-fontsize": {
+ "v": "10"
+ }
+ },
+ "o-a": {
+ "o-key": {
+ "v": "true",
+ "t": "b"
+ }
+ }
+ }
+ },
+ {
+ "n": "8",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-numberformat": {
+ "v": "hh%3Amm%3Ass",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "time%3Ben",
+ "t": "s"
+ },
+ "o-halign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "9",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-halign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "10",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "11",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "12",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "13",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "14",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "11",
+ "a-c": [
+ {
+ "n": "3",
+ "o-cell": {
+ "o-a": {
+ "o-key": {
+ "v": "true",
+ "t": "b"
+ }
+ }
+ }
+ },
+ {
+ "n": "5",
+ "o-cell": {
+ "o-t": {
+ "o-numberformat": {
+ "v": "d~5C.m~5C.yyyy%20h%3Amm",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "date%3Bde",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "6",
+ "o-cell": {
+ "o-t": {
+ "o-numberformat": {
+ "v": "h%3Amm%3Ass%20AM%2FPM",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "time%3Ben",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "7",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-fontcolor": {
+ "v": "%230f4db5",
+ "t": "s"
+ },
+ "o-fontsize": {
+ "v": "10"
+ }
+ },
+ "o-a": {
+ "o-key": {
+ "v": "true",
+ "t": "b"
+ }
+ }
+ }
+ },
+ {
+ "n": "8",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-numberformat": {
+ "v": "hh%3Amm%3Ass",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "time%3Ben",
+ "t": "s"
+ },
+ "o-halign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "9",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-halign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "10",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "11",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "12",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "13",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "14",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "12",
+ "a-c": [
+ {
+ "n": "3",
+ "o-cell": {
+ "o-a": {
+ "o-key": {
+ "v": "true",
+ "t": "b"
+ }
+ }
+ }
+ },
+ {
+ "n": "5",
+ "o-cell": {
+ "o-t": {
+ "o-numberformat": {
+ "v": "d~5C.m~5C.yyyy%20h%3Amm",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "date%3Bde",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "6",
+ "o-cell": {
+ "o-t": {
+ "o-numberformat": {
+ "v": "h%3Amm%3Ass%20AM%2FPM",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "time%3Ben",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "7",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-fontcolor": {
+ "v": "%230f4db5",
+ "t": "s"
+ },
+ "o-fontsize": {
+ "v": "10"
+ }
+ },
+ "o-a": {
+ "o-key": {
+ "v": "true",
+ "t": "b"
+ }
+ }
+ }
+ },
+ {
+ "n": "8",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-numberformat": {
+ "v": "hh%3Amm%3Ass",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "time%3Ben",
+ "t": "s"
+ },
+ "o-halign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "9",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-halign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "10",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "11",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "12",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "13",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "14",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "13",
+ "a-c": [
+ {
+ "n": "7",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-numberformat": {
+ "v": "0.00",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "number%3B2%3Bfalse",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "8",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "9",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "10",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "11",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "12",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "13",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ },
+ {
+ "n": "14",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5fef3",
+ "t": "s"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "15",
+ "a-c": [
+ {
+ "n": "1",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "2",
+ "o-cell": {
+ "o-t": {
+ "o-fontstyle": {
+ "v": "1"
+ },
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "7",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ },
+ "o-fontstyle": {
+ "v": "1"
+ },
+ "o-numberformat": {
+ "v": "0.00",
+ "t": "s"
+ },
+ "o-localculture": {
+ "v": "number%3B2%3Bfalse",
+ "t": "s"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "16",
+ "a-c": [
+ {
+ "n": "1",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "2",
+ "o-cell": {
+ "o-t": {
+ "o-fontstyle": {
+ "v": "1"
+ },
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "7",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ },
+ "o-a": {
+ "o-key": {
+ "v": "true",
+ "t": "b"
+ }
+ }
+ }
+ },
+ {
+ "n": "8",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5b5b5",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ },
+ "o-fontstyle": {
+ "v": "1"
+ },
+ "o-fontcolor": {
+ "v": "%234a90e2",
+ "t": "s"
+ }
+ },
+ "o-a": {
+ "o-key": {
+ "v": "true",
+ "t": "b"
+ },
+ "o-leftborderwidth": {
+ "v": "8"
+ },
+ "o-topborderwidth": {
+ "v": "8"
+ }
+ }
+ }
+ },
+ {
+ "n": "9",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5b5b5",
+ "t": "s"
+ }
+ },
+ "o-a": {
+ "o-leftborderwidth": {
+ "v": "8"
+ },
+ "o-topborderwidth": {
+ "v": "8"
+ }
+ }
+ }
+ },
+ {
+ "n": "10",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5b5b5",
+ "t": "s"
+ }
+ },
+ "o-a": {
+ "o-leftborderwidth": {
+ "v": "8"
+ },
+ "o-topborderwidth": {
+ "v": "8"
+ }
+ }
+ }
+ },
+ {
+ "n": "11",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5b5b5",
+ "t": "s"
+ }
+ },
+ "o-a": {
+ "o-leftborderwidth": {
+ "v": "8"
+ },
+ "o-topborderwidth": {
+ "v": "8"
+ }
+ }
+ }
+ },
+ {
+ "n": "12",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5b5b5",
+ "t": "s"
+ }
+ },
+ "o-a": {
+ "o-leftborderwidth": {
+ "v": "8"
+ },
+ "o-topborderwidth": {
+ "v": "8"
+ }
+ }
+ }
+ },
+ {
+ "n": "13",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5b5b5",
+ "t": "s"
+ }
+ },
+ "o-a": {
+ "o-leftborderwidth": {
+ "v": "8"
+ },
+ "o-topborderwidth": {
+ "v": "8"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "17",
+ "a-c": [
+ {
+ "n": "1",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "2",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "6",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "7",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "8",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5b5b5",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ },
+ "o-a": {
+ "o-leftborderwidth": {
+ "v": "8"
+ },
+ "o-topborderwidth": {
+ "v": "8"
+ }
+ }
+ }
+ },
+ {
+ "n": "9",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5b5b5",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ },
+ "o-fontsize": {
+ "v": "24"
+ }
+ },
+ "o-a": {
+ "o-leftborderwidth": {
+ "v": "8"
+ },
+ "o-topborderwidth": {
+ "v": "8"
+ }
+ }
+ }
+ },
+ {
+ "n": "10",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5b5b5",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ },
+ "o-a": {
+ "o-leftborderwidth": {
+ "v": "8"
+ },
+ "o-topborderwidth": {
+ "v": "8"
+ }
+ }
+ }
+ },
+ {
+ "n": "11",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5b5b5",
+ "t": "s"
+ }
+ },
+ "o-a": {
+ "o-leftborderwidth": {
+ "v": "8"
+ },
+ "o-topborderwidth": {
+ "v": "8"
+ }
+ }
+ }
+ },
+ {
+ "n": "12",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5b5b5",
+ "t": "s"
+ }
+ },
+ "o-a": {
+ "o-leftborderwidth": {
+ "v": "8"
+ },
+ "o-topborderwidth": {
+ "v": "8"
+ }
+ }
+ }
+ },
+ {
+ "n": "13",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5b5b5",
+ "t": "s"
+ }
+ },
+ "o-a": {
+ "o-leftborderwidth": {
+ "v": "8"
+ },
+ "o-topborderwidth": {
+ "v": "8"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "18",
+ "a-c": [
+ {
+ "n": "2",
+ "o-cell": {
+ "o-t": {
+ "o-fontstyle": {
+ "v": "1"
+ },
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "7",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "8",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5b5b5",
+ "t": "s"
+ }
+ },
+ "o-a": {
+ "o-leftborderwidth": {
+ "v": "8"
+ },
+ "o-topborderwidth": {
+ "v": "8"
+ }
+ }
+ }
+ },
+ {
+ "n": "9",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5b5b5",
+ "t": "s"
+ }
+ },
+ "o-a": {
+ "o-leftborderwidth": {
+ "v": "8"
+ },
+ "o-topborderwidth": {
+ "v": "8"
+ }
+ }
+ }
+ },
+ {
+ "n": "10",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5b5b5",
+ "t": "s"
+ }
+ },
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ },
+ "o-a": {
+ "o-leftborderwidth": {
+ "v": "8"
+ },
+ "o-topborderwidth": {
+ "v": "8"
+ }
+ }
+ }
+ },
+ {
+ "n": "11",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5b5b5",
+ "t": "s"
+ }
+ },
+ "o-a": {
+ "o-leftborderwidth": {
+ "v": "8"
+ },
+ "o-topborderwidth": {
+ "v": "8"
+ }
+ }
+ }
+ },
+ {
+ "n": "12",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5b5b5",
+ "t": "s"
+ }
+ },
+ "o-a": {
+ "o-leftborderwidth": {
+ "v": "8"
+ },
+ "o-topborderwidth": {
+ "v": "8"
+ }
+ }
+ }
+ },
+ {
+ "n": "13",
+ "o-cell": {
+ "o-f": {
+ "o-fillcolor": {
+ "v": "%23b5b5b5",
+ "t": "s"
+ }
+ },
+ "o-a": {
+ "o-leftborderwidth": {
+ "v": "8"
+ },
+ "o-topborderwidth": {
+ "v": "8"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "19",
+ "a-c": [
+ {
+ "n": "1",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "2",
+ "o-cell": {
+ "o-t": {
+ "o-fontstyle": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "10",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "20",
+ "a-c": [
+ {
+ "n": "1",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "2",
+ "o-cell": {
+ "o-t": {
+ "o-fontstyle": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "10",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "21",
+ "a-c": [
+ {
+ "n": "1",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "2",
+ "o-cell": {
+ "o-t": {
+ "o-fontstyle": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "10",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "22",
+ "a-c": [
+ {
+ "n": "1",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "2",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "3",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "4",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "5",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "6",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "7",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "23",
+ "a-c": [
+ {
+ "n": "1",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "2",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "3",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "4",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "5",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "6",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "7",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "24",
+ "a-c": [
+ {
+ "n": "1",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "2",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "3",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "4",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "5",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "6",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "7",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "25",
+ "a-c": [
+ {
+ "n": "1",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "2",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "3",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "4",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "5",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "6",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "7",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "26",
+ "a-c": [
+ {
+ "n": "1",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "2",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "3",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "4",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "5",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "6",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "7",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ }
+ ]
+ },
+ {
+ "n": "27",
+ "a-c": [
+ {
+ "n": "1",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "2",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "3",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "4",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "5",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "6",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ },
+ {
+ "n": "7",
+ "o-cell": {
+ "o-t": {
+ "o-valign": {
+ "v": "1"
+ }
+ }
+ }
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "o-images": {}
+ },
+ "machineId": "S1-EB3RpyP"
+ }
+ }
+ ],
+ "streams": [
+ {
+ "id": "HJZR0U1xSI",
+ "name": "AASRESTTestProducer",
+ "owner": null,
+ "disabled": false,
+ "className": "ProducerConfiguration",
+ "lastModified": "2020-09-09T12:55:45.437Z",
+ "lastAccessed": "2020-03-06T15:05:58.112Z",
+ "connector": {
+ "_id": "S1lML8ygr8",
+ "id": "S1lML8ygr8",
+ "className": "ConnectorConfiguration",
+ "owner": null,
+ "disabled": false,
+ "lastModified": "2020-09-09T12:34:12.984Z",
+ "lastAccessed": "2020-03-06T15:03:38.128Z",
+ "isRef": true
+ },
+ "samplePayloads": null,
+ "mimeType": "auto"
+ },
+ {
+ "id": "S1lML8ygr8",
+ "name": "AASRESTTestProvider",
+ "owner": null,
+ "disabled": false,
+ "className": "ConnectorConfiguration",
+ "lastModified": "2020-09-09T12:55:45.436Z",
+ "lastAccessed": "2020-03-06T15:03:38.128Z",
+ "provider": {
+ "_id": "@cedalo/stream-rest-client",
+ "id": "@cedalo/stream-rest-client",
+ "className": "ProviderConfiguration",
+ "owner": null,
+ "disabled": null,
+ "lastModified": null,
+ "lastAccessed": null,
+ "isRef": true
+ },
+ "baseUrl": "",
+ "userName": "",
+ "password": ""
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/basyx.streamsheets/docker-compose.yml b/examples/basyx.streamsheets/docker-compose.yml
new file mode 100644
index 0000000..77d723d
--- /dev/null
+++ b/examples/basyx.streamsheets/docker-compose.yml
@@ -0,0 +1,34 @@
+version: '3'
+services:
+
+ registry:
+ image: basyx/registry:0.1.0-SNAPSHOT
+ container_name: dashboard-registry
+ ports:
+ - 4000:4000
+
+ dashboard-aas:
+ image: basyx/dashboard-aas:0.1.0-SNAPSHOT
+ container_name: dashboard-aas
+ environment:
+ - BaSyxDashboardSubmodel_Min=15
+# - BaSyxDashboardSubmodel_Max=30
+ ports:
+ - 6400:6400
+
+ aas-wrapper:
+ image: basyx/aas-wrapper:0.1.0-SNAPSHOT
+ container_name: aas-wrapper
+ ports:
+ - 6500:6500
+
+ streamsheets:
+ image: cedalo/streamsheets:1.5
+ container_name: streamsheets
+ ports:
+ - 8081:8081
+ - 8083:8083
+ - 1883:1883
+ volumes:
+ - ./mosquitto:/etc/mosquitto-default-credentials
+ - ./streamsheets:/var/lib/mongodb
\ No newline at end of file
diff --git a/examples/basyx.streamsheets/readme.txt b/examples/basyx.streamsheets/readme.txt
new file mode 100644
index 0000000..5023bde
--- /dev/null
+++ b/examples/basyx.streamsheets/readme.txt
@@ -0,0 +1,12 @@
+HowTo: First Setup
+------------------
+
+1. Start docker-compose
+- Run "docker-compose up" in the /streamsheets/ folder
+
+2. Open in Browser (admin/admin)
+- http://localhost:8081/
+
+3. Import dashboard from JSON (drag & drop possible)
+
+4. Run Streamsheet
diff --git a/examples/basyx.streamsheets/start.bat b/examples/basyx.streamsheets/start.bat
new file mode 100644
index 0000000..83ed0d0
--- /dev/null
+++ b/examples/basyx.streamsheets/start.bat
@@ -0,0 +1 @@
+docker-compose up
diff --git a/examples/basyx.streamsheets/start.sh b/examples/basyx.streamsheets/start.sh
new file mode 100644
index 0000000..d935e43
--- /dev/null
+++ b/examples/basyx.streamsheets/start.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+docker-compose up
\ No newline at end of file
diff --git a/examples/basyx.streamsheets/stop.bat b/examples/basyx.streamsheets/stop.bat
new file mode 100644
index 0000000..58694d0
--- /dev/null
+++ b/examples/basyx.streamsheets/stop.bat
@@ -0,0 +1 @@
+docker-compose down
\ No newline at end of file
diff --git a/examples/basyx.streamsheets/stop.sh b/examples/basyx.streamsheets/stop.sh
new file mode 100644
index 0000000..f5139e2
--- /dev/null
+++ b/examples/basyx.streamsheets/stop.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+docker-compose down
\ No newline at end of file
diff --git a/examples/mvnw b/examples/mvnw
new file mode 100644
index 0000000..a16b543
--- /dev/null
+++ b/examples/mvnw
@@ -0,0 +1,310 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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
+#
+# https://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.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# M2_HOME - location of maven2's installed home dir
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ export JAVA_HOME="`/usr/libexec/java_home`"
+ else
+ export JAVA_HOME="/Library/Java/Home"
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=`java-config --jre-home`
+ fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+ ## resolve links - $0 may be a link to maven's home
+ PRG="$0"
+
+ # need this for relative symlinks
+ while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG="`dirname "$PRG"`/$link"
+ fi
+ done
+
+ saveddir=`pwd`
+
+ M2_HOME=`dirname "$PRG"`/..
+
+ # make it fully qualified
+ M2_HOME=`cd "$M2_HOME" && pwd`
+
+ cd "$saveddir"
+ # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --unix "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME="`(cd "$M2_HOME"; pwd)`"
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="`which javac`"
+ if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=`which readlink`
+ if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+ if $darwin ; then
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+ else
+ javaExecutable="`readlink -f \"$javaExecutable\"`"
+ fi
+ javaHome="`dirname \"$javaExecutable\"`"
+ javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="`which java`"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=`cd "$wdir/.."; pwd`
+ fi
+ # end of workaround
+ done
+ echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ echo "$(tr -s '\n' ' ' < "$1")"
+ fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found .mvn/wrapper/maven-wrapper.jar"
+ fi
+else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+ fi
+ if [ -n "$MVNW_REPOURL" ]; then
+ jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ else
+ jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ fi
+ while IFS="=" read key value; do
+ case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+ esac
+ done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Downloading from: $jarUrl"
+ fi
+ wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+ if $cygwin; then
+ wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
+ fi
+
+ if command -v wget > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found wget ... using wget"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget "$jarUrl" -O "$wrapperJarPath"
+ else
+ wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Found curl ... using curl"
+ fi
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl -o "$wrapperJarPath" "$jarUrl" -f
+ else
+ curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
+ fi
+
+ else
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo "Falling back to using Java to download"
+ fi
+ javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaClass=`cygpath --path --windows "$javaClass"`
+ fi
+ if [ -e "$javaClass" ]; then
+ if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Compiling MavenWrapperDownloader.java ..."
+ fi
+ # Compiling the Java class
+ ("$JAVA_HOME/bin/javac" "$javaClass")
+ fi
+ if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+ # Running the downloader
+ if [ "$MVNW_VERBOSE" = true ]; then
+ echo " - Running MavenWrapperDownloader.java ..."
+ fi
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+ echo $MAVEN_PROJECTBASEDIR
+fi
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$M2_HOME" ] &&
+ M2_HOME=`cygpath --path --windows "$M2_HOME"`
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/examples/mvnw.cmd b/examples/mvnw.cmd
new file mode 100644
index 0000000..c8d4337
--- /dev/null
+++ b/examples/mvnw.cmd
@@ -0,0 +1,182 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM https://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+
+FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %DOWNLOAD_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%