OpenMUC User Guide
Download PDF Version1. Intro
2. Quick Start
2.1. Install OpenMUC
2.2. Start the Demo
./bin/openmuc start -fg
bin\openmuc.bat
... 17:33:00.011 INFO SimpleDemoApp - home1: current grid power = -4.672 kW 17:33:05.006 INFO SimpleDemoApp - home1: current grid power = -4.666 kW 17:33:10.007 INFO SimpleDemoApp - home1: current grid power = -4.671 kW ...
lb
to list all installed bundles.
START LEVEL 1 ID|State |Level|Name 0|Active | 0|System Bundle ... 7|Active | 1|Logback Core Module 8|Active | 1|OpenMUC App - Simple Demo 9|Active | 1|OpenMUC Core - API 10|Active | 1|OpenMUC Core - Data Manager 11|Active | 1|OpenMUC Core - SPI 12|Active | 1|OpenMUC Data Logger - ASCII 13|Active | 1|OpenMUC Data Logger - SlotsDB 14|Active | 1|OpenMUC Driver - CSV 15|Active | 1|OpenMUC Server - RESTful Web Service 16|Active | 1|OpenMUC WebUI - Base 17|Active | 1|OpenMUC WebUI - Channel Access Tool ...
ctrl+d
or stop 0
. For more information about the start script see chapter OpenMUC Start Script.
2.3. WebUI Walk Through
2.3.1. Add a New Channel
3. Tutorials
3.1. Build a Simple M-Bus Data Logger
sudo apt-get install librxtx-java sudo adduser $USER dialout
- Download OpenMUC and unpack it
- Open openmuc/framework/conf/bundles.conf.gradle and comment the following lines by //
osgibundles group: "org.openmuc.framework", name: "openmuc-app-simpledemo", version: openmucVersion osgibundles group: "org.openmuc.framework", name: "openmuc-driver-csv", version: openmucVersion
- Add following lines to make the M-Bus driver and serial communication available
osgibundles group: "org.openmuc.framework", name: "openmuc-driver-mbus", version: openmucVersion osgibundles group: "org.openmuc", name: "jrxtx", version: "1.0.1"
- To apply changes navigate to openmuc/framework/bin and run
./openmuc update-bundles
- Start OpenMUC
./openmuc start -fg
- Open a browser and point it to localhost:8888 to view the WebUI of OpenMUC. Login with user admin and password admin.
- Click on Channel Configurator > Tab Drivers > Add new driver to configuration
- Enter mbus as ID and click Submit
- Now the M-Bus driver appears under Channel Configurator > Tab Drivers. Click on the search icon
- Enter the serial port the meter is connected to and provide the baud rate if needed (e.g. /dev/ttyS0 or /dev/ttyS0:2400). See M-Bus driver section for more information. If you are using an USB device you can use the dmesg tool on linux to figure out on what port it is connected (e.g. /dev/ttyUSB0).
- Click on Scan for devices. Now OpenMUC scans all M-Bus addresses, which may take a while
- Select the desired device from the list and click Add devices
- Now the device is added. If you do not see the search icon next to the device, press F5 to reload the page and navigate to Channel Configurator > Tab Devices
- Click on the search icon and OpenMUC automatically scans all available channels. Select the desired channels and click Add channels
- Now we need to define a sampling and logging interval for the channels. Click on Channel Configurator > Tab Channels and click on Edit Icon of the desired channel. Write 2000 in the Sampling Interval and Logging Interval field and click Submit
- To show actual values of the channel, navigate to Applications > Channel Access Tool, select your device and click Access selected
- All logged data are stored in /openmuc/framework/data/ascii/
- You can also change the configuration by editing /openmuc/framework/conf/channels.xml
3.2. Develop a Customised Application
- Download and unpack the OpenMUC framework. Open a terminal and navigate to the openmuc folder
- Create a new project based on the simple demo application. Navigate to openmuc/projects/app and copy the simpledemo folder and rename the copy to ems (Energy Management System).
- Edit the build.gradle file inside your ems folder. Rename the project name and description and save the file.
def projectName = "EMS" ... description "OpenMUC Energy Management System."
- Navigate to app/ems/src/main/java/org/openmuc/framework/app/ and rename the folder simpledemo to ems
- Replace the SimpleDemoApp.java inside this ems folder with EmsApp.java.
package org.openmuc.framework.app.ems; import org.openmuc.framework.data.Record; import org.openmuc.framework.dataaccess.Channel; import org.openmuc.framework.dataaccess.DataAccessService; import org.openmuc.framework.dataaccess.RecordListener; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Component(service = {}) public final class EmsApp{ private static final Logger logger = LoggerFactory.getLogger(EmsApp.class); private static final String APP_NAME = "OpenMUC EMS App"; private Channel chPowerGrid; private RecordListener powerListener; @Reference private DataAccessService dataAccessService; @Activate private void activate() { logger.info("Activating {}", APP_NAME); powerListener = new PowerListener(); chPowerGrid = dataAccessService.getChannel("power_grid"); chPowerGrid.addListener(powerListener); } @Deactivate private void deactivate() { logger.info("Deactivating {}", APP_NAME); chPowerGrid.removeListener(powerListener); } } class PowerListener implements RecordListener{ private static final Logger logger = LoggerFactory.getLogger(PowerListener.class); @Override public void newRecord(Record record) { if (record.getValue() != null) { logger.info(">>> grid power: {}", record.getValue().asDouble()); } } }
This is a light version of the simple demo application and basically adds a listener to the power_grid channel and logs the current value. This class can be used for further development of your application. - Now we add our project to the gradle build process. For this purpose open the openmuc/settings.gradle in an editor and append following statement to the include statement
, "openmuc-app-ems"
- Furthermore you need to add following line at the end of settings.gradle
project(":openmuc-app-ems").projectDir = file("projects/app/ems")
- Now we create the Eclipse project files by running the following command in the openmuc main directory
gradle eclipse
- Start your Eclipse IDE and set the GRADLE_USER_HOME classpath variable: Go to Window>Preferences>Java>Build Path>Classpath Variable. Set the variable GRADLE_USER_HOME to the path of the ~/.gradle folder in your home directory (e.g. /home/<user_name>/.gradle/
- Import the Openmuc projects into Eclipse: Go to File>Import>General>Existing Projects into Workspace, select your OpenMUC directory and click on Finish. All projects should be imported without any errors.
- Now add the EMS application to the OpenMUC Framework. Navigate openmuc/framework/conf and and following line to bundles.conf.gradle below the openmuc-app-simpledemo entry:
osgibundles group: "org.openmuc.framework", name: "openmuc-app-ems", version: openmucVersion
- Finally we build the framework and start our application. Navigate to openmuc/framework/bin and run:
./openmuc update-bundles -b
This will build all bundles and copies them to /openmuc/framework/bundles. Our EMS app should be now inside this folder e.g. openmuc-app-ems-<version>.jar - Start the framework with:
./openmuc start -fg
- The log messages of our EMS application are now visible in the terminal e.g:
2018-12-17 19:10:20.015 [...] INFO o.o.framework.app.simpledemo.EmsApp - >>> grid power: -1.779 2018-12-17 19:10:25.006 [...] INFO o.o.framework.app.simpledemo.EmsApp - >>> grid power: -1.761
- Now you know all the steps to build a new application and get it running in OpenMUC. For further development you should have a look at the source code of the SimpleDemoApp.java.
3.3. Develop a Customised WebUI Plugin
Note |
This tutorial describes how we developed the simpledemovisualisation. When creating your own plugin you can just replace the name simpledemovisualisation whenever it comes up in the tutorial. |
- First we have to create a new Project with the Structure
openmuc/projects/webui/simpledemovisualisation
- Now copy the build.gradle file from one of the existing WebUI plugins, for example:
openmuc/projects/webui/channelaccesstool/build.gradle
into this project and change the projectName and projectDescriptiondef projectName = "OpenMUC WebUI - Simple Demo Visualisation" def projectDescription = "Simple Demo Visualisation plug-in for the WebUI of the OpenMUC framework."
- Open openmuc/configuration.gradle and add the following line under distributionProjects = javaProjects.findAll
it.getPath() == ":openmuc-webui-simpledemovisualisation" ||
- Open openmuc/settings.gradle and add the following line under OpenMUC WebUI Bundles of the include section
'openmuc-webui-simpledemovisualisation',
- Furthermore, add the following line to the projects section of the settings.gradle
project(":openmuc-webui-simpledemovisualisation").projectDir = file('projects/webui/simpledemovisualisation')
- Open openmuc/framework/conf/bundles.conf.gradle and add the following line under dependencies
osgibundles group: "org.openmuc.framework", name: "openmuc-webui-simpledemovisualisation", version: openmucVersion
Next we will take a look at how our project should be structured once we are done - First we will take a look at the java file. Recreate the folder structure above and create the java file SimpleDemoVisualisation.java, and then copy this into it
import org.openmuc.framework.webui.spi.WebUiPluginService; import org.osgi.service.component.annotations.Component; @Component(service = WebUiPluginService.class) public final class SimpleDemoVisualisation extends WebUiPluginService { @Override public String getAlias() { return "simpledemovisualisation"; } @Override public String getName() { return "Simple Demo Visualisation"; } }
The two functions getAlias and getName have to be overridden. The alias is used to identify the plugin while the name will be displayed in the WebUI. In order to display an icon above the plugin’s name, the file needs to be called icon and put in the images folder. - Next we will take a look at app.js and app.routes.js. In app.js all we do is creating a module and naming it.
(function(){ angular.module('openmuc.openmuc-visu', []); })();
The more interesting one is app.routes.js because it is responsible for allowing us to get from the main page to the page of our plugin. It also allows us to specify which files have to be loaded.(function(){ var app = angular.module('openmuc'); app.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) { $stateProvider. state('simpledemovisualisation', { url: '/simpledemovisualisation', templateUrl: 'simpledemovisualisation/html/index.html', requireLogin: true }). state('simpledemovisualisation.index', { url: '/', templateUrl: 'simpledemovisualisation/html/graphic.html', controller: 'VisualisationController', requireLogin: true, resolve: { openmuc: function ($ocLazyLoad) { return $ocLazyLoad.load( { name: 'openmuc.simpledemovisualisation', files: ['openmuc/js/channels/channelsService.js', 'openmuc/js/channels/channelDataService.js', 'simpledemovisualisation/css/simpledemovisualisation/main.css', 'simpledemovisualisation/js/visu/VisualisationController.js'] } ) } } }) }]); })();
All files you need have to be added to the list “files” in order for the plugin to work. The first two files we load are necessary to access the defined channels. Then we load in our css file and lastly the javascript file of this plugin. - For the Plugin created in this tutorial we will need an svg that is put into the image folder. The SimpleDemoGraphic.svg used in this tutorial is made up of multiple images, paths as well as text fields. In this case only the text fields are of interest.
- The two html files used in this app are very simple, index.html sets the headline and then calls on graphic.html through ui-view. Ui-view calls upon the route defined in app.routes.js.
<div class="page-header"> <h1>OpenMUC Visualisation</h1> </div> <div ui-view></div>
In graphic.html we create a div element and assign it the class svg-container. We then create an object HTML element inside the div and assign it the class svg-content.<div class="svg-container"> <object id="simpleDemoGraphic" type="image/svg+xml" data="simpledemovisualisation/images/SimpleDemoGraphic.svg" class="svg-content" onload="display_visualisation()"></object> </div>
Further we also assign it an Id, in this case simpleDemoGraphic, specify that it is of the type svg and tell it where our svg is located. This way our svg is now displayed on the page, but in order to change elements of the svg we need a javascript function which is called through onload. - In order to specify how our page should be displayed we use a css file.
html, body { font-family: "Arial"; margin: 0px; padding: 0px; } .svg-container { display: inline-block; position: relative; width: 1108px; height: 760px; border:1px solid black; } .svg-content { display: block; position: absolute; width: 1106px; height: 740px; top: 0; left: 0; }
In this css file we tell the browser how the html elements should look and be positioned. If the declaration starts with a dot it signifies all elements with the specified class being targeted, a hash would signify an element with that Id being targeted and nothing signifies all html elements of that type should be targeted. - By default the svg will have an eight pixel margin on each side, meaning there will be white space between the border and svg. If you dont want that you need to open the svg in a text editor and add a style tag after the svg tag as shown below
<svg xmlns:osb="http://www.openswatchbook.org/uri/2009/osb" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="100%" height="100%" viewBox="0 0 573.61664 357.1875" version="1.1" id="svg8" inkscape:version="0.92.3 (2405546, 2018-03-11)" sodipodi:docname="SimpleDemoGraphic.svg"> <style type="text/css" media="screen" id="style5004"><![CDATA[ body{ margin: 0px; } ]]></style>
We cannot change the css of the svg from our css file so we have to do it inside the svg. - Finally we take a look at the javascript file that will allow us to display data in real time.
(function(){ var injectParams = ['$scope', '$interval', 'ChannelsService']; var VisualisationController = function($scope, $interval, ChannelsService) { var svg_document; display_visualisation = function() {
Here we “import” the angular functions $scope and $interval as well as the class ChannelsService. Next we take a look at the function display_visualisation that is called when the html page loads.svg_document = document.getElementById('simpleDemoGraphic').contentDocument;
Through this line of code we now have access to the svg in javascript. We achieve this by calling document.getElementById with the id of our object element as a parameter. The contentDocument means that the return value is the document object, otherwise the return would have just been the content of the document, in which case we could not use it in the way we need to later.$scope.interval = ""; $interval.cancel($scope.interval); $scope.interval = $interval(function(){ ... }, 500); };
What follows is defined inside this interval, meaning it will be repeated every 500 milliseconds.ChannelsService.getAllChannels().then(async function(channels) { $scope.channels = await channels.records; });
Here we call the function getAllChannels of the class ChannelsService. It makes a get call to the REST server and returns all the channels defined in the channels.xml. The “then” means that whatever is in the round brackets will be executed after getAllChannels’ return value arrives. Inside these round brackets we define an async function with getAllChannels’ return value as a parameter. The list records of the return value contains the requested channels, so we save them in the list $scope.channels. Normally the rest of the code would be executed while getAllChannels waits for a reply, in which case our code would fail as $scope.channels would be undefined, but the await keyword in conjunction with marking the function as async makes it so the code only resumes executing once the await has been resolved.if ($scope.channels != undefined){ $scope.channels.forEach(function(channel){ if (channel.id === "power_heatpump"){ textHeatPump = svg_document.getElementById("textHeatPump"); textHeatPump.textContent = channel.record.value + " kW"; } if (channel.id === "power_electric_vehicle"){ textChargingStation = svg_document.getElementById("textChargingStation"); textChargingStation.textContent = channel.record.value + " kW"; } if (channel.id === "power_photovoltaics"){ textPv = svg_document.getElementById("textPv"); textPv.textContent = channel.record.value + " kW"; } if (channel.id === "power_grid"){ textGrid = svg_document.getElementById("textGrid"); textGrid.textContent = channel.record.value + " kW"; } }); }
First we check if our list is not undefined as it is possible that during the first interval there wont be any data to work with. Now we iterate through our channels list to find the channels we need. Once we found the right channel, we search for the corresponding text field and save the reference to it in a variable. By setting the textContent of the text field we can change what is displayed, in this case the channel’s value is displayed in the text field. Now we set the interval and close the function definition as shown above.$scope.$on('$destroy', function () { $interval.cancel($scope.interval); }); }; VisualisationController.$inject = injectParams; angular.module('openmuc.openmuc-visu').controller('VisualisationController', VisualisationController); })();
After that we tell the function to stop the interval if the scope’s destroy event is triggered and that the in app.js defined module should use this controller.
- If you want to change the css of the svg at runtime you can do so through javascript similarly to the manipulation of the text field above.
textHeatPump.style.fill = "blue";
This would set the text color of the text field to blue
4. Architecture
- The data manager represents the core and center of OpenMUC. Virtually all other OpenMUC modules (e.g. drivers, data loggers, servers, applications and web interface plugins) communicate with it through OSGi services. The data manager gets automatically notified when new drivers or data loggers get installed. OpenMUC applications communicate with devices, access logged data or change the configuration by calling service functions provided by the data manager. It is therefore the data manager that shields the application programmer from the details of the communication and data logging technology. What the data manager does is mostly controlled through a central configuration.
- The channel configuration holds the user defined data channels and its parameters. Data channels are the frameworks representation of data points in connected devices. Amongst others the channel configuration holds the following information:
- communication parameters that the drivers require
- when to sample new data from connected devices
- when to send sampled data to existing data logger(s) for efficient persistent storage. The configuration is stored in the file conf/channels.xml. You may add or modify the configured channels by manually editing the channels.xml file or through the channel configurator web interface.
- A driver is used by the data manager to send/get data to/from a connected device. Thus a driver usually implements a communication protocol. Several communication drivers have already been developed (e.g. IEC 61850, ModbusTCP, KNX). Many drivers use standalone communication libraries (e.g. OpenIEC61850, jMBus) developed by the OpenMUC team. These libraries do not depend on the OpenMUC framework and can therefore be used by any Java application. New communication drivers for OpenMUC can be easily developed by third parties.
- A data logger saves sampled data persistently. The data manager forwards sampled data to all available data loggers if configured to do so. Data loggers are specifically designed to store time series data for short storage and retrieval times. OpenMUC currently includes four data loggers. The ASCII data logger saves data in a human readable text format while SlotsDB saves data in a more efficient binary format. And two loggers for remote system logging with AMQP or MQTT.
- If all you want is sample and log data then you can use the OpenMUC framework as it is and simply configure it to your needs. But if you want to process sampled data or control a device you will want to write your own application. Like all other modules your application will be an OSGi bundle. In your application you can use the DataAccessService and the ConfigService provided by the data manager to access sampled and logged data. You may also issue immediate read or write commands. These are forwarded by the data manager to the driver. The configuration (when to sample and to log) can also be changed during run-time by the application. At all times the application only communicates with the data manager and is therefore not confronted with the complicated details of the communication technology being used.
- If your application is located on a remote system (e.g. a smart phone or an Internet server) then the data and configuration can be accessed through an OpenMUC server. At the moment OpenMUC provides a RESTful web service for this purpose.
- The OpenMUC framework provides a web user interface (WebUI) for tasks such as configuration, visualization of sampled data or exporting logged data. The web interface is modular and provides a plug-in interface. This way developers may write a website that integrates into the main menu of the web interface. The WebUI is mostly for configuration and testing purposes. Most companies will want to create their own individual UI.
- OpenMUC also contains a set of core libraries which provide helper classes that are used by multiple bundles of the framework.
4.1. File Structure of the Distribution
- build/libs-all
- All modules/bundles that make up the OpenMUC framework
- dependencies
- Information on the external dependencies of the OpenMUC framework. Also contains the RXTX library (repacked as a bundle) which is needed by many OpenMUC drivers based on serial communication.
- projects
- All sources of the OpenMUC framework. You can easily change and rebuild OpenMUC using Gradle.
- framework
- A ready to use OpenMUC demo framework that is introduced next.
4.2. Folder framework/
- felix
- The main Apache Felix OSGi jar which is run to start OpenMUC.
- bin
- Run scripts for Linux/Unix and Windows.
- bundle
- Contains all bundles that are started by the Felix OSGi framework. Note that this folder does not contain all available OpenMUC bundles but only a subset for demonstration purposes.
- log
- Log files produced by the running framework.
- conf
- Various configuration files of the framework.
4.2.1. Folder conf/
- bundles.conf.gradle
- Contains a list of all bundles which should be used for the framework.
- channels.xml
- Configuration file of OpenMUC to configure drivers, devices and channels.
- config.properties
- Property file of the Felix OSGi framework.
- logback.xml
- Configuration file to configure log levels for console and log file.
Currently, the logging is configured to create logfiles of at most 100MB, create a new log file every day and keep a maximum of 30 days or 3GB, which ever is reached first.
- system.properties
- Contains general settings for the OpenMUC framework
4.3. Devices and Channels
4.4. Configuration via channels.xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<configuration>
<logger>loggerId</logger>
<driver id="driver_x">
<!-- driver options -->
<device>
<!-- device options -->
<channel>
<!-- channel options -->
</channel>
<channel>
<!-- channel options -->
</channel>
</device>
</driver>
</configuration>
Options | Mandatory | Values | Default | Description |
---|---|---|---|---|
id |
yes |
string |
– |
Id of the driver |
samplingTimeout |
no |
time* |
0 |
Default time waited for a read operation to complete if the device doesn’t set a samplingTimeout on its own. |
connectRetryInterval |
no |
time* |
60s |
Default time waited until a failed connection attempt is repeated. |
disabled |
no |
boolean |
false |
While disabled, no connections to devices on this driver are established at all and all channels of these devices stop being sampled and logged. |
Options | Mandatory | Values | Default | Description |
---|---|---|---|---|
id |
no |
string |
– |
ID of the device. |
deviceAddress |
yes |
string |
– |
Address for the driver to uniquely identify the device. Syntax of this address is up to the driver implementation. |
description |
no |
string |
– |
Description of the device. |
settings |
no |
string |
– |
Additional settings for the driver. Syntax is up to the driver implementation. |
samplingTimeout |
no |
time* |
0 |
Time waited for a read operation to complete. Overwrites samplingTimeout of Driver. |
connectRetryInterval |
no |
time* |
60s |
Time waited until a failed connection attempt is repeated. |
disabled |
no |
boolean |
false |
While disabled, no connection of this device is established and all channels of this device stop being sampled and logged. |
Options | Mandatory | Values | Default | Description |
---|---|---|---|---|
id |
yes |
string |
– |
Globally unique identifier. Used by data logger implementations. The OpenMUC framework automatically generates an id if none is provided. |
description |
no |
string |
– |
Description of the channel. |
channelAddress |
yes |
string |
– |
The channelAddress is driver specific and contains the necessary parameters for the driver to access. |
settings |
no |
string |
– |
Additional settings for the driver. Syntax is up to the driver implementation. |
valueType |
no |
DOUBLE
FLOAT
LONG
INTEGER
SHORT
BYTE
BOOLEAN
BYTE_ARRAY
STRING
|
DOUBLE |
Data type of the channel. Used on data logger. Driver implementation do NOT receive this settings! |
valueType Attribute: length |
no |
integer |
10 |
The attribute length is only used if valueType is BYTE_ARRAY or STRING. Determines the maximum length of the byte array or string. |
scalingFactor |
no |
double |
1 |
Is used to scale a value read by a driver or set by an application. The value read by an driver is multiplied with the scalingFactor and a value set by an application is divided by the scalingFactor. Possible values are e.g.: 1.0 4.94147E-9 -2.4 |
valueOffset |
no |
double |
0 |
Is used to offset a value read by a driver or set by an application. The offset is added to a value read by a driver and subtracted from a value set by an application. |
unit |
no |
string |
– |
Physical unit of this channel. For information only (info can be accessed by an app or driver) |
loggingInterval |
no |
time* |
– |
Time difference until this channel is logged again. Omitting loggingInterval disables logging in intervals. Setting loggingInterval disables loggingEvent. |
loggingTimeOffset |
no |
time* |
0 |
|
loggingEvent |
no |
boolean |
false |
If true, immediately logs latest record on value change. Only supported by some data loggers. Disabled if loggingInterval is set. See data logger description for more information. |
loggingSettings |
no |
string |
– |
Data logger specific log settings. Format: <loggerId_A>[:<param_A>=<value_A>][,…][;<loggerId_B>[:<param_B>=<value_B>]]. See data logger description for more information. |
loggingSettings Attribute: reader |
no |
string |
– |
In case multiple readers are registered in the framework you can use the attribute reader to specify a dedicated logger for reading values e.g. <loggingSettings reader=”asciilogger”>mqttlogger:topic=my/topic</loggingSettings |
listening |
no |
boolean |
false |
Determines if this channel shall passively listen for incoming value changes from the driver. |
samplingInterval |
no |
time* |
– |
Time interval between two attempts to read this channel. -1 or omitting samlingOffset disables sampling on this channel. |
samplingTimeOffset |
no |
time* |
0 |
|
samplingGroup |
no |
string |
– |
For grouping channels. All channels with the same samplingGroup and same samplingInterval are in one group. The purpous of samplingGroups is to improve the drivers performance – if possible. |
disabled |
no |
boolean |
false |
If a channel is disabled, all sampling and logging actions of this channel are stopped. |
Note |
if you don’t use a suffix, then ms is automatically used |
4.5. Sampling, Listening and Logging
- sampling is when the data manager frequently asks a driver to retrieve a channel value.
- listening is when the driver listens on a channel and forwards new values to the data manager.
- logging is when the data manager forwards the current sampled value to the data loggers that are installed. The data loggers then store the data persistently
<channel>
<id>channel1</id>
<channelAddress>dummy/channel/address/1</channelAddress>
<samplingInterval>4s</samplingInterval>
</channel>
<channel>
<id>channel2</id>
<channelAddress>dummy/channel/address/2</channelAddress>
<samplingInterval>4s</samplingInterval>
<loggingInterval>8s</loggingInterval>
</channel>
<channel>
<id>channel3</id>
<channelAddress>dummy/channel/address/3</channelAddress>
<listening>true</listening>
</channel>
<channel>
<id>channel4</id>
<channelAddress>dummy/channel/address/4</channelAddress>
<listening>true</listening>
<loggingInterval>8s</loggingInterval>
</channel>
Important |
When listening is true and additional a sampling interval is defined then the sampling is ignored. |
<channel>
<id>channel4</id>
<channelAddress>dummy/channel/address/5</channelAddress>
<listening>true</listening>
<loggingEvent>true</loggingEvent>
</channel>
5. OpenMUC Start Script
/framework/bin/
.
5.1. Start OpenMUC
./bin/openmuc start -fg
-fg
. On Windows you can run the bin\openmuc.bat
to start OpenMUC. For now we will focus on the Linux script, since Linux is the more common environment for OpenMUC. The start command will basically run the Felix OSGi Framework via java -jar felix/felix.jar
and Felix starts all bundles located in framework/bundle.
5.2. Stop OpenMUC
./bin/openmuc stop
5.3. Restart OpenMUC
./bin/openmuc restart
5.4. Reload OpenMUC Configuration
./bin/openmuc reload
5.5. Update Bundles
/framework/bundle
folder.
./bin/openmuc update-bundles
/framework/bundle
folder use:
./bin/openmuc update-bundles -b
-i
option to update the repository with the latest changes.
./bin/openmuc update-bundles -i
./bin/openmuc update-bundles -b && ./bin/openmuc start -fg
5.6. Remote Shell
bundles.conf.gradle
. Add the following bundles org.apache.felix.shell.remote
and org.apache.felix.gogo.shell
and comment or remove the bundles org.apache.felix.gogo.jline
and jline
osgibundles group: "org.apache.felix", name: "org.apache.felix.shell.remote", version: "<version>" osgibundles group: "org.apache.felix", name: "org.apache.felix.gogo.shell", version: "<version>" //osgibundles group: "org.apache.felix", name: "org.apache.felix.gogo.jline", version: "<version>" //osgibundles group: "org.jline", name: "jline", version: "<version>"
./bin/openmuc remote-shell
telnet 127.0.0.1 6666
ctrl+d
.
5.7. Auto Start at Boot Time
ln -s /path/to/openmuc/bin/openmuc /etc/init.d/openmuc update-rc.d openmuc defaults
6. Drivers
6.1. Install a Driver
6.1.1. Copy driver
6.1.2. Editing bundles configuration
osgibundles group: "org.openmuc.framework", name: "openmuc-driver-csv", version: openmucVersion osgibundles group: "org.openmuc.framework", name: "openmuc-webui-spi", version: openmucVersion osgibundles group: "org.openmuc.framework", name: "openmuc-webui-base", version: openmucVersion
osgibundles group: "org.openmuc.framework", name: "openmuc-driver-csv", version: openmucVersion osgibundles group: "org.openmuc.framework", name: "openmuc-driver-mbus", version: openmucVersion osgibundles group: "org.openmuc", name: "jrxtx", version: "1.0.1" osgibundles group: "org.openmuc.framework", name: "openmuc-webui-spi", version: openmucVersion osgibundles group: "org.openmuc.framework", name: "openmuc-webui-base", version: openmucVersion
./openmuc update-bundles
./openmuc update-bundles -i
6.1.3. Use a Driver with Serial Communication
cp ../dependencies/rxtx/jrxtx-1.0.1.jar ./bundle/
sudo apt-get install librxtx-java
sudo adduser <yourUserName> dialout
6.2. Modbus
TCP (ethernet) | RTU (serial) | RTUTCP (serial over ethernet) | |
---|---|---|---|
ID |
modbus |
||
Device Address |
<ip>[:<port>] |
<serial port> |
<ip>[:<port>] |
Settings |
<type> |
<type>:<encoding>:<baudrate>:<databits>:<parity>:<stopbits>: <echo>:<flowControlIn>:<flowControlOut> |
<type> |
Channel Address |
<UnitId>:<PrimaryTable>:<Address>:<Datatyp> |
Note |
The driver uses the j2mod library which itself uses the rxtx library for serial communication. Therefore the librxtx-java package needs to be installed on the system. Furthermore the user needs to be in the groups dialout and plugdev |
Config | Description/ Values |
---|---|
<type> |
RTU|TCP|RTUTCP |
<encoding> |
SERIAL_ENCODING_RTU |
<baudrate> |
Integer value: e.g.: 2400, 9600, 115200 |
<databits> |
DATABITS_5, DATABITS_6, DATABITS_7, DATABITS_8 |
<parity> |
PARITY_EVEN, PARITY_MARK, PARITY_NONE, PARITY_ODD, PARITY_SPACE |
<stopbits> |
STOPBITS_1, STOPBITS_1_5, STOPBITS_2 |
<echo> |
ECHO_TRUE, ECHO_FALSE |
<flowControlIn> |
FLOWCONTROL_NONE, FLOWCONTROL_RTSCTS_IN, FLOWCONTROL_XONXOFF_IN |
<flowControlOut> |
FLOWCONTROL_NONE, FLOWCONTROL_RTSCTS_OUT, FLOWCONTROL_XONXOFF_OUT |
<settings>
RTU:SERIAL_ENCODING_RTU:38400:DATABITS_8:PARITY_NONE:STOPBITS_1 :ECHO_FALSE:FLOWCONTROL_NONE:FLOWCONTROL_NONE
</settings>
Parameter | Description |
---|---|
UnitId |
In homogenious architecture (when just MODBUS TCP/IP is used) On TCP/IP, the MODBUS server is addressed by its IP address; therefore, the MODBUS Unit Identifier is useless. The value 255 (0xFF) has to be used. In heterogeneous architecture (when using MODBUS TCP/IP and MODBUS serial or MODBUS+) This field is used for routing purpose when addressing a device on a MODBUS+ or MODBUS serial line sub-network. In that case, the “Unit Identifier” carries the MODBUS slave address of the remote device. The MODBUS slave device addresses on serial line are assigned from 1 to 247 (decimal). Address 0 is used as broadcast address. Note: Some MODBUS devices act like a bridge or a gateway and require the UnitId even if they are accessed through TCP/IP. One of those devices is the Janitza UMG. To access data from the Janitza the UnitId has to be 1. |
PrimaryTable |
PrimaryTable defines the which part of the device memory should be accessed. Valid values: COILS, DISCRETE_INPUTS, INPUT_REGISTERS, HOLDING_REGISTERS |
Address |
Address of the channel/register. Decimal integer value – not hex! |
Datatyp |
Valid values: BOOLEAN (1 bit) INT16 (1 register/word, 2 bytes) UINT16 (1 register/word, 2 bytes) INT32 (2 registers/words, 4 bytes) UINT32 (2 registers/words, 4 bytes) LONG (4 registers/words, 8 bytes) FLOAT (2 registers/words, 4 bytes) DOUBLE (4 registers/words, 8 bytes) BYTEARRAY[n] (n = number of REGISTERS not BYTES, 1 register = 2 bytes!) |
Note |
To store a UINT32 value it requires <valueType>LONG</valueType> for the channel. |
Primary Table | BOOLEAN | SHORT | INT | FLOAT | DOUBLE | LONG | BYTEARRAY[n] |
---|---|---|---|---|---|---|---|
COILS |
x |
– |
– |
– |
– |
– |
– |
DISCRETE_INPUTS |
x |
– |
– |
– |
– |
– |
– |
INPUT_REGISTERS |
– |
x |
x |
x |
x |
x |
x |
HOLDING_REGISTERS |
– |
x |
x |
x |
x |
x |
x |
Primary Table | BOOLEAN | SHORT | INT | FLOAT | DOUBLE | LONG | BYTEARRAY[n] |
---|---|---|---|---|---|---|---|
COILS |
x |
– |
– |
– |
– |
– |
– |
DISCRETE_INPUTS |
– |
– |
– |
– |
– |
– |
– |
INPUT_REGISTERS |
– |
– |
– |
– |
– |
– |
– |
HOLDING_REGISTERS |
– |
x |
x |
x |
x |
x |
x |
<channelAddress>255:INPUT_REGISTERS:100:SHORT</channelAddress>
<channelAddress>255:COILS:412:BOOLEAN</channelAddress>
<channelAddress>255:INPUT_REGISTERS:100:BOOLEAN</channelAddress> (BOOLEAN doesn't
go with INPUT_REGISTERS)
<channelAddress>255:COILS:412:LONG</channelAddress> (LONG does not go with COILS)
j2mod Method | Modbus Function Code | Primary Table | Access | Java Data Type |
---|---|---|---|---|
ReadCoilsRequest |
FC 1 Read Coils |
Coils |
RW |
boolean |
ReadInputDiscretesRequest |
FC 2 Read Discrete Inputs |
Discrete Inputs |
R |
boolean |
ReadMultipleRegistersRequest |
FC 3 Read Holding Registers |
Holding Registers |
RW |
short, int, double, long, float, bytearray[] |
ReadInputRegistersRequest |
FC 4 Read Input Registers |
Input Registers |
R |
short, int, double, long, float, bytearray[] |
WriteCoilRequest |
FC 5 Write Single Coil |
Coils |
RW |
boolean |
WriteMultipleCoilsRequest |
FC 15 Write Multiple Coils |
Coils |
RW |
boolean |
WriteMultipleRegistersRequest |
FC 6 Write Single Registers |
Holding Registers |
RW |
short, int, double, long, float, bytearray[] |
WriteMultipleRegistersRequest |
FC 16 Write Multiple Registers |
Holding Registers |
RW |
short, int, double, long, float, bytearray[] |
<channelAddress>255:INPUT_REGISTERS:100:SHORT</channelAddress> will be accessed
via function code 4.
6.2.1. Modbus TCP and Wago
Note |
Till now the driver has been tested with some modules of the Wago 750 Series with the Fieldbus-Coupler 750-342 |
<channelAddress>255:INPUT_REGISTERS:1:SHORT</channelAddress>
<channelAddress>255:DISCRETE_INPUTS:3:BOOLEAN</channelAddress>
<channelAddress>255:HOLDING_REGISTERS:512:SHORT</channelAddress>
<channelAddress>255:COILS:513:BOOLEAN</channelAddress>
<channelAddress>255:COILS:513:BOOLEAN</channelAddress> or
<channelAddress>255:DISCRETE_INPUTS:513:BOOLEAN</channelAddress>
6.3. M-Bus (wired)
ID | mbus |
---|---|
Device Address |
<serial_port>:<mbus_address> or tcp:<host_address>:<port> |
Settings |
[<baudrate>][:timeout][:lr][:ar][:d<delay>] [:tc<tcp_connection_timeout>] |
Channel Address |
[X]<dib>:<vib> |
6.4. M-Bus (wireless)
ID | wmbus |
---|---|
Device Address |
<serial_port>:<secondary_address> |
Settings |
<transceiver> <mode> [<key>] |
Channel Address |
<dib>:<vib> |
6.5. IEC 60870-5-104
ID | iec60870 |
---|---|
Device Address |
[ca=<common_address>] [;p=<port>] [;h=<host_address>] |
Settings |
[mft=<message_fragment_timeout>] [;cfl=<cot_field_length>] [;cafl=<common_address_field_length>] [;ifl=<ioa_field_length>] [;mtnar=<max_time_no_ack_received>] [;mtnas=<max_time_no_ack_sent>] [;mit=<max_idle_time>] [;mupr=<max_unconfirmed_ipdus_received>] [;sct=<stardt_con_timeout>] |
Channel Address |
ca=<common_address>; t=<type_id>; ioa=<ioa> [;dt=<data_type>] [;i=<index>] [;m=<multiple>] |
Data Type | Description |
---|---|
v |
value (default) |
ts |
timestamp |
iv |
in/valid |
nt |
not topical |
sb |
substituted |
bl |
blocked |
ov |
overflow |
ei |
elapsed time invalid |
ca |
counter was adjusted since last reading |
cy |
counter overflow occurred in the |
6.6. IEC 61850
ID | iec61850 |
---|---|
Device Address |
<host>[:<port>] |
Settings |
[-a <authentication parameter>] [-lt <local t-selector>] [-rt <remote t-selector>] |
Channel Address |
<bda reference>:<fc> |
6.7. IEC 62056 part 21
ID | iec62056p21 |
---|---|
Device Address |
<serial_port> |
Settings |
[-d <baud_rate_change_delay>] [-t <timeout>] [-r <number_of_read_retries>] [-bd <initial_baud_rate>] [-a <device_address>] [-fbd] [-rsc <request_message_start_character>] |
Channel Address |
<data_set_id> |
6.8. KNX
ID | knx |
---|---|
Device Address |
knxip://<host_ip>[:<port>] knxip://<device_ip>[:<port>] |
Settings |
[Address=<Individual KNX address (e. g. 2.6.52)>];[SerialNumber=<Serial number>] |
Channel Address |
<Group Adress>:<DPT_ID> |
6.9. eHZ
ID | ehz |
---|---|
Device Address |
sml://<serialPort> or iec://<serialPort> e.g. sml:///dev/ttyUSB0 |
Settings |
|
Channel Address |
<OBIScode> e.g. 10181ff (not 1-0:1.8.1*255) |
6.10. SNMP
ID | snmp |
---|---|
Device Address |
IP/snmpPort |
Settings |
settings string |
Channel Address |
SNMP OID address |
192.168.1.1/161
SNMPVersion | “SNMPVersion” enum contains all available values |
---|---|
USERNAME |
string |
SECURITYNAME |
string |
AUTHENTICATIONPASSPHRASE |
is the same COMMUNITY word in SNMP V2c |
PRIVACYPASSPHRASE |
string |
SNMPVersion=V2c:USERNAME=public:SECURITYNAME=public:AUTHENTICATIONPASSPHRASE=password
1.3.6.1.2.1.1.1.0
6.11. CSV
ID | csv |
---|---|
Device Address |
path to csv file (e.g. /path/to/my.csv) |
Settings |
samplingmode=<samplingmode>[;rewind=<rewind>] |
Channel Address |
column name |
- Samplingmode configures how the csv file is sampled. Currently, three different modes are supported:
- line – starts sampling from the first line of the csv file. Timestamps are ignored and each sampling reads the next line.
- unixtimestamp – csv file must contain a column with the name unixtimestamp, values must be in milliseconds. During sampling the driver searches the closest unixtimestamp which is >= the sampling timestamp. Therefore, the driver keeps returning the same value for sampling timestamps which are before the next unixtimestamp of the csv file.
- hhmmss – csv file must contain a column with the name hhmmss and the time values must be in the format: hhmmss.
- rewind – If true and the last line of the csv file is reached, then the driver will start sampling again from first line. This option can only be used in combination with sampling mode hhmmss or line.
<device id="smarthome">
<description/>
<deviceAddress>./csv-driver/smarthome.csv</deviceAddress>
<settings>samplingmode=hhmmss;rewind=true</settings>
<channel id="power_pv">
<channelAddress>power_photovoltaics</channelAddress>
<unit>W</unit>
<samplingInterval>5s</samplingInterval>
<loggingInterval>5s</loggingInterval>
</channel>
6.12. Aggregator
ID | aggregator |
---|---|
Device Address |
virtual device e.g “aggregatordevice” |
Settings |
|
Channel Address |
<sourceChannelId>:<aggregationType>[:<quality>] |
- AVG: calculates the average of all values of interval (e.g. for average power)
- LAST: takes the last value of interval (e.g. for energy)
- DIFF: calculates difference of first and last value of interval
- PULS_ENERGY,<pulses per Wh>,<max counter>: calculates energy from pulses of interval (e.g. for pulse counter/meter). Example: PULSE_ENERGY,10,65535
<channelid="channelA">
<samplingInterval>10s</samplingInterval>
<loggingInterval>10s</loggingInterval>
</channel>
<driver id="aggregator">
<device id="aggregatordevice">
<channelid="channelB">
<channelAddress>channelA:avg</channelAddress>
<samplingInterval>60s</samplingInterval>
<loggingInterval>60s</loggingInterval>
</channel>
</device>
</driver>
<driver id="aggregator">
<device id="aggregatordevice">
<channelid="channelB">
<channelAddress>channelA:avg</channelAddress>
<samplingInterval>60s</samplingInterval>
<samplingTimeOffset>55s</samplingTimeOffset>
<loggingInterval>60s</loggingInterval>
</channel>
</device>
</driver>
6.13. REST/JSON
ID | rest |
---|---|
Device Address |
http(s)://<address>:<port> |
Settings |
[ct;]<username>:<password> |
Channel Address |
<channelID> |
- host_address: the address of the remote OpenMUC eg. 127.0.0.1
- port: the port of the remote OpenMUC eg. 8888
- ct: this optional flag defines if the driver should check the remote timestamp, before reading the complete record
- username: the username of the remote OpenMUC
- password: the pasword of the remote OpenMUC
- channelID: the ID of the remote OpenMUC
- read channel
- write channel
- scan for all channels
- scan for devices
- reading whole devices instead of single channel
- listening
<driver id="rest">
<device id="example_rest_device">
<deviceAddress>http://192.168.8.18:8888</deviceAddress>
<settings>ct;admin:admin</settings>
<channel id="power_grid_rest">
<channelAddress>power_grid</channelAddress>
<samplingInterval>5s</samplingInterval>
</channel>
</device>
</driver>
6.14. AMQP
ID | amqp |
---|---|
Device Address |
URL of the amqp infrastructure |
Settings |
port=<port>;vhost=<vHost>;user=<user>;password=<pw>;framework=<frameworkID>; parser=<needed Parser-Service>;buffersize=<1-n>;ssl=<true/false>;separator=<e.g. “_”>;exchange=<amqp-exchange> |
Channel Address |
<name of amqp-queue> |
- framework and separator: To add the information about the source of an amqp queue, the concept of subsection Mapping to AMQP-Queues is used. Framework defines the prefix of the amqp queue and seperator the char between framework and channelID.
- buffersize: This parameter makes it possible to optimize the performance at listening and logging huge amounts of records. The driver waits till it collected the configured number of records, before it returns the whole list to the data manager. This decreases the number of needed tasks e.g. for writing to a database.
- read channel
- write channel
- listening
- scan for devices
- scan for all channels
- reading whole devices instead of single channel
<driver id="amqp">
<device id="Smart Meter">
<deviceAddress>myAmqpBroker.de</deviceAddress>
<settings>
port=5671;vhost=myVHost;user=openmuc;password=Password123;framework=SmartMeter;
parser=openmuc;buffersize=1;ssl=true;separator=_;exchange=field1
</settings>
<channel id="power_grid">
<channelAddress>power_grid</channelAddress>
<listening>true</listening>
</channel>
</device>
</driver>
6.15. MQTT
ID | mqtt |
---|---|
Device Address |
URL of the mqtt broker |
Settings |
port=<port>;parser=<needed Parser-Service>[;username=<user>;password=<pw>] [;recordCollectionSize=<1-n>][;ssl=<true/false>][;maxBufferSize=<0-n>;maxFileSize=<0-n>;maxFileCount=<0-n>] [;connectionRetryInterval=<s>][;connectionAliveInterval=<s>][;firstWillTopic=<topic>;firstWillPayload=<payload>] [;lastWillTopic=<topic>;lastWillPayload=<payload>][;lastWillAlways=<true/false>] [;persistenceDirectory=<data/driver/mqtt>] |
Channel Address |
<name of mqtt-topic> |
- port: Port for MQTT communication
- parser: Identifier of needed parser implementation e.g. openmuc
- [username]: Name of your MQTT account
- [password]: Password of your MQTT account
- [recordCollectionSize]: This parameter makes it possible to optimize the performance of listening and logging huge amounts of records. The driver waits until the configured number of records is collected, before returning the list to the data manager. This decreases the number of needed tasks e.g. for writing to a database.
- [ssl]: true enable ssl, false disable ssl
- [maxBufferSize]: Max buffer size in kB. If limit is reached than buffer will be written to file.
- [maxFileSize]: Max file size in kB. If
- [maxFileCount]: Number of files to be created for buffering
- [connectionRetryInterval]: Connection retry interval in s – reconnect after given seconds when connection fails
- [connectionAliveInterval]: Connection alive interval in s – periodically send PING message to broker to detect broken connections
- [firstWillTopic]: Topic on which firstWillPayload will be published on successful connections
- [firstWillPayload]: Payload of the first will message
- [lastWillTopic]: Topic on which lastWillPayload will be published
- [lastWillPayload]: Payload of the last will message
- [lastWillAlways]: true: publish last will payload on every disconnection, including intended disconnects by the client. false publish only on errors/connection interrupts
- [persistenceDirectory]: directory to store data for file buffering e.g. data/driver/mqtt>
- write channel
- listening
- read channel
- scan for devices
- scan for all channels
- reading whole devices instead of single channel
<driver id="mqtt">
<device id="Smart Meter">
<deviceAddress>myMqttBroker.de</deviceAddress>
<settings>
port=1883;username=openmuc;password=Password123
parser=openmuc;bufferSize=2;ssl=true
lastWillTopic=my/topic;lastWillPayload=Offline;lastWillAlways=true
firstWillTopic=my/topic;firstWillPayload=Online
</settings>
<channel id="power_grid">
<channelAddress>SmartMeter/power_grid</channelAddress>
<listening>true</listening>
</channel>
</device>
</driver>
7. Dataloggers
7.1. ASCII Logger
7.1.1. General Information
Parameter | Description | |
---|---|---|
loggerId |
asciilogger |
|
loggingEvent |
not supported |
|
loggingSettings |
not supported |
7.1.2. Configuration
org.openmuc.framework.datalogger.ascii.fillUpFiles = true
org.openmuc.framework.datalogger.ascii.directory = <path>
7.1.3. Structure
- ies format version
- file name
- file info
- timezone relative to gmt (i.e. +1)
- timestep_sec (time between entries in seconds)
- col_no
- col_name
- confidential
- measured
- unit
- category (data type and length)
- comment
7.2. AMQP Logger
7.2.1. General Information
Parameter | Description | |
---|---|---|
loggerId |
amqplogger |
|
loggingEvent |
supported |
|
loggingSettings |
amqplogger:queue=<your.queue> |
7.2.2. Configuration
org.openmuc.framework.datalogger.amqp.host = localhost
org.openmuc.framework.datalogger.amqp.port = 5672
org.openmuc.framework.datalogger.amqp.ssl = false
org.openmuc.framework.datalogger.amqp.vhost = /
org.openmuc.framework.datalogger.amqp.username = guest
org.openmuc.framework.datalogger.amqp.password = guest
7.2.3. Mapping to AMQP-Queues
# Set the unique identifier of this framework (this is also the exchange name)
org.openmuc.framework.datalogger.amqp.framework = openmuc
7.2.4. Serialisation
# Set the parser with which to serialize records
org.openmuc.framework.datalogger.amqp.parser = openmuc
7.3. MQTT Logger
7.3.1. General Information
Parameter | Description | |
---|---|---|
loggerId |
mqttlogger |
|
loggingEvent |
not supported |
|
loggingSettings |
mqttlogger:topic=<your/topic> |
7.3.2. Installation
conf/bundles.conf.gradle
and conf/config.properties
file
bundles.conf.gradle
file.
osgibundles group: "org.openmuc.framework", name: "openmuc-datalogger-mqtt", version: openmucVersion
osgibundles group: "org.openmuc.framework", name: "openmuc-lib-ssl", version: openmucVersion
osgibundles group: "org.openmuc.framework", name: "openmuc-lib-mqtt", version: openmucVersion
osgibundles group: "org.openmuc.framework", name: "openmuc-lib-osgi", version: openmucVersion
//add your project specific bundle here, which provides the ParserService implementation, example with OpenMUC parser:
osgibundles group: "org.openmuc.framework", name: "openmuc-lib-parser-openmuc", version: openmucVersion
config.properties
to provide sun.misc
package.
org.osgi.framework.system.packages.extra=sun.misc
7.3.3. Configuration
# URL of MQTT broker
host=localhost
# port for MQTT communication
port=1883
# (Optional) password of your MQTT account
password=
# (Optional) name of your MQTT account
username=
# identifier of needed parser implementation
parser=openmuc
# directory to store data for file buffering
persistenceDirectory=/data/mqtt/
# file buffering: buffer size in kB
maxBufferSize=1
# file buffering: number of files to be created
maxFileCount=2
#file buffering: file size in kB
maxFileSize=2
# usage of ssl true/false
ssl=false
# if true compose log records of different channels to one mqtt message
multiple=false
# connection retry interval in s – reconnect after given seconds when connection fails
connectionRetryInterval=10
# connection alive interval in s – periodically send PING message to broker to detect broken connections
connectionAliveInterval=10
# (Optional) LWT configuration
# topic on which lastWillPayload will be published
lastWillTopic=
# last will payload
lastWillPayload=
# (Optional) also publish last will payload on client initiated disconnects (true/false)
lastWillAlways=false
# (Optional) "first will" configuration
# topic on which firstWillPayload will be published on successful connections
firstWillTopic=
# first will payload
firstWillPayload=
multiple
can be set true. Otherwise, every record is sent in a single MQTT message.
7.4. SlotsDB Logger
7.4.1. General Information
Parameter | Description | |
---|---|---|
loggerId |
slotsdb |
|
loggingEvent |
not supported |
|
loggingSettings |
not supported |
7.5. SQL Logger
7.5.1. General Information
Parameter | Description | |
---|---|---|
loggerId |
sqllogger |
|
loggingEvent |
supported |
|
loggingSettings |
sqllogger:<empty> |
7.5.2. Database Schema
COLOUMN_NAME | COLOUMN_TYPE | CHARACTER_MAXIMUM_LENGTH |
---|---|---|
channelid |
VARCHAR(30) |
30 |
logginginterval |
VARCHAR(10) |
10 |
listening |
VARCHAR(5) |
5 |
… |
… |
… |
COLOUMN_NAME | DATA_TYPE | CHARACTER_MAXIMUM_LENGTH |
---|---|---|
time |
timestamp with time zone |
|
channelid |
character varying |
30 |
flag |
smallint |
10 |
value |
numeric |
7.5.3. Installation
conf/bundles.conf.gradle
. Different database engines like h2 or postgresql are supported. The needed library bundle depends of the used engine. Add following dependencies to the bundles.conf.gradle
file.
osgibundles group: "org.openmuc.framework", name: "openmuc-datalogger-sql", version: openmucVersion
osgibundles group: "org.openmuc.framework", name: "openmuc-lib-osgi", version: openmucVersion
//add your database engine specific bundle for h2 or postgresql here:
osgibundles group: 'org.postgresql', name: 'postgresql', version: '42.2.14'
osgibundles group: 'com.h2database', name: 'h2', version: '1.4.200'
7.5.4. Configuration
conf/properties/org.openmuc.framework.datalogger.sql.SqlLoggerService.cfg
# (Optional) seconds after a timeout is thrown
socket_timeout=5
# Password for postgresql
psql_pass=<pw_postgres>
# Password for the database user
password=<pw_user>
# (Optional) local time zone
timezone=Europe/Berlin
# (Optional) keep tcp connection alive
tcp_keep_alive=true
# User of the used database
user=<database_user>
# (Optional) SSL needed for the database connection
ssl=false
# URL of the used database
#url=jdbc:h2:retry:file:./data/h2/h2;AUTO_SERVER=TRUE;MODE=MYSQL
url=jdbc:postgresql://127.0.0.1:5432/<database_user>
7.5.5. Migrating database to be compatible with newer H2 version
openmuc migrateh2
java -cp <path to 1.4.200 jar> org.h2.tools.Script -url jdbc:h2:<path to database> -user <username> -password <password> -script <choose a script location path> -options compression zip
java -cp <path to 2.0.206 jar> org.h2.tools.RunScript -url jdbc:h2:<choose path to new database> -user <username> -password <password> -script <script location path> -options compression zip
8. Libraries
8.1. AMQP
- AmqpSettings
- AmqpConnection
- AmqpReader
- AmqpWriter
8.1.1. Connecting to a broker (AmqpSettings/AmqpConnection)
disconnect()
first to clean up any resources.
String host = "localhost";
int port = 5672;
String virtualHost = "/";
String username = "guest";
String password = "guest";
boolean ssl = false;
String exchange = "example";
AmqpSettings settings = new AmqpSettings(
host, port, virtualHost, username, password, ssl, exchange
);
AmqpConnection connection = new AmqpConnection(settings);
// Before stopping the application:
connection.disconnect();
8.1.2. Consuming messages (AmqpReader)
Manually retrieving messages
byte[] read(String queue)
returns a single message retrieved from the given queue or null
if the queue was empty.
AmqpReader reader = new AmqpReader(connection);
byte[] receivedMessage = reader.read("exampleQueue");
if (receivedMessage == null) {
// No message received
} else {
// Handle received message
}
Listening for messages
AmqpReader reader = new AmqpReader(connection);
Collection<String> queues = new ArrayList<>(2);
queues.add("exampleQueue1");
queues.add("exampleQueue2");
reader.listen(queues, (String queue, byte[] message) -> {
if (queue == "exampleQueue1") {
// handle message
} else {
// handle message
}
});
8.1.3. Publishing messages (AmqpWriter)
void write(String routingKey, byte[] message)
with the routing key and the message. The message will be published to the exchange specified in the AmqpConnection.
AmqpWriter writer = new AmqpWriter(connection);
String routingKey = "test.logger";
byte[] message = "Hello World!".getBytes();
writer.write(routingKey, message);
8.2. MQTT
- MqttSettings
- MqttConnection
- MqttReader
- MqttWriter
8.2.1. LWT (Last Will and Testament) and first will
disconnect()
is called and LWT is properly configured (with lastWillAlways=true
).
8.2.2. Connecting to a broker (MqttSettings/MqttConnection)
connect()
is called. It’s important to create needed instances of MqttWriter and/or MqttReader before calling connect()
.
disconnect()
first to clean up any resources.
String host = "localhost";
int port = 1883;
String user = null;
String pw = null;
boolean ssl = false;
long maxBufferSize = 1;
long maxFileSize = 2;
int connRetryInterval = 5;
int connAliveInterval = 10;
String persistenceDirectory = "data/mqtt/app"
MqttSettings settings = new MqttSettings(
host, port, user, pw, ssl, maxBufferSize, maxFileSize, maxFileCount, connRetryInterval,
connAliveInterval, persistenceDirectory
);
// Create MqttReader and/or MqttWriter objects here!
MqttConnection connection = new MqttConnection(settings);
// Before stopping the application:
connection.disconnect();
8.2.3. Subscribing/listening to topics (MqttReader)
MqttReader reader = new MqttReader(connection);
// Note connect() is called after MqttReader instance creation
connection.connect();
Collection<String> topics = new ArrayList<>(2);
topics.add("example/topic/1");
topics.add("example/topic/2");
reader.listen(queues, (String topic, byte[] message) -> {
if (topic == "example/topic/1") {
// handle message
} else {
// handle message
}
});
8.2.4. Publishing messages (MqttWriter)
void write(String topic, byte[] message)
.
MqttWriter writer = new MqttWriter(connection);
connection.connect();
String topic = "test/logger";
byte[] message = "Hello World!".getBytes();
writer.write(topic, message);
8.3. OSGI
8.3.1. OSGi Service Registration
- How to provide your service to the OSGi service registry?
- How to subscribe to a service provided by the OSGi service registry?
RegistrationHandler
@Activate
public void activate(BundleContext context) {
RegistrationHandler registrationHandler = new RegistrationHandler(context);
}
Providing a custom service
String pid = AmqpLogger.class.getName();
registrationHandler.provideInFramework(DataLoggerService.class.getName(), amqpLogger, pid);
Subscribe for a service
registrationHandler.subscribeForService(DataLoggerService.class.getName(), (instance) -> {
if (instance != null)
this.loggerService = (DataLoggerService) instance;
});
Clean up
@Deactivate
public void deactivate() {
registrationHandler.removeAllProvidedServices();
}
8.3.2. OSGi Dynamic Configuration
org.apache.felix.fileinstall-*.jar
) must be added to the framework under framework/conf/bundles.conf.gradle. It is useful to define a directory where the configuration files will be stored. This can be configured in conf/system.properties
e.g.:
##################### Felix FileInstall
felix.fileinstall.dir=properties
felix.fileinstall.poll=5000
properties
. The following subsections help you to implement dynamic configurations for your service.
PropertyHandler
GenericSettings
has to be extended like in this example.
public class Settings extends GenericSettings {
public static final String USERNAME = "username";
public static final String PASSWORD = "password";
public static final String PORT = "port";
public static final String HOST = "host";
public Settings() {
super();
properties.put(USERNAME, new ServiceProperty(USERNAME, "name of your AMQP account", null, true));
properties.put(PASSWORD, new ServiceProperty(PASSWORD, "password of your AMQP account", null, true));
properties.put(PORT, new ServiceProperty(PORT, "port for AMQP communication", null, true));
properties.put(HOST, new ServiceProperty(HOST, "URL of AMQP broker", "localhost", true));
}
}
ServiceProperty
. The instantiation needs the key and description of the property. Furthermore, you can provide a default value and mark the property as optional or mandatory. The OSGi lib will validate the configuration against the mandatory flag and will report a waring if a mandatory property is missing.
The next step is to instantiate the PropertyHandler
with this settings and the pid which corresponds to the class implementing the ManagedService. The following examples is based on the AmqpLogger:
public class AmqpLogger implements DataLoggerService, ManagedService {
...
public AmqpLogger() {
String pid = AmqpLogger.class.getName();
settings = new Settings();
propertyHandler = new PropertyHandler(settings, pid);
}
properties
subdirectory e.g.: org.openmuc.framework.datalogger.amqp.AmqpLogger.cfg
# name of your AMQP account
username=
# password of your AMQP account
password=
# port for AMQP communication
port=
# URL of AMQP broker
host=localhost
Managed Service
public void updated(Dictionary<String, ?> propertyDict)
. Every time the properties in the internal OSGi database for this specific service are updated, for example through the Apache Felix FileInstall, the method is called with a new instance of type Dictionary. The linking of this ManagedService with the configuration file is done by using the same name for the service registration in subsection [provide-ref] and instantiating our PropertyHandler
. Because of this, the name of the configuration file and the full qualified class name are equal.
The given dictionary contains key-value pairs with the properties of the service specific settings class and can be handled like in the following example:
@Override
public void updated(Dictionary<String, ?> propertyDict) throws ConfigurationException {
DictionaryPreprocessor dict = new DictionaryPreprocessor(propertyDict);
if (!dict.wasIntermediateOsgiInitCall()) {
tryProcessConfig(dict);
}
}
private void tryProcessConfig(DictionaryPreprocessor newConfig) {
try {
propertyHandler.processConfig(newConfig);
if (propertyHandler.configChanged()) {
//Properties are updated, trigger a service specific reaction
}
} catch (ServicePropertyException e) {
logger.error("update properties failed", e);
//Do some reaction till properties are valid again
}
}
8.4. Parser-Service
8.4.1. Accessing a specific Parser-Service
String serviceInterfaceName = ParserService.class.getName();
ServiceReference<?> serviceReference = bundleContext.getServiceReference(serviceInterfaceName);
if (serviceReference != null) {
String parserId = (String) serviceReference.getProperty("parserID");
ParserService parser = (ParserService) bundleContext.getService(serviceReference);
}
8.4.2. OpenMUC-Parser
{"timestamp":1587974340000,"flag":"VALID","value":6.67}
8.4.3. Custom Parser
@Activate
public void activate(BundleContext context) {
Dictionary<String, Object> properties = new Hashtable<>();
properties.put("parserID", "<myCustomParser>");
String serviceName = ParserService.class.getName();
registration = context.registerService(serviceName, new MyParserServiceImpl(), properties);
}
8.5. SSL
openssl s_client -connect host:port -showcerts [-proxy host:port]
cert.crt
(beginning with -----BEGIN CERTIFICATE-----
ending with -----END CERTIFICATE-----
. Add them to the store:
keytool -keystore conf/truststore.jks -importcert cert.crt
changeme
. We want to change that:
keytool -keystore conf/truststore.jks -storepasswd
PKCS#12
.
keytool -importkeystore -srckeystore otherstore.jks -destkeystore conf/truststore.jks
conf/keystore.jks
is done analog to the Trust Store.
/conf/system.properties
to reflect the changes:
org.openmuc.framework.truststore = conf/truststore.jks
org.openmuc.framework.keystore = conf/keystore.jks
org.openmuc.framework.truststorepassword = changeme
org.openmuc.framework.keystorepassword = changeme
9. WebUI
9.1. Plugins
- Plotter
- Plugin which provides plotter for visualization of current and historical data
- Channel Access Tool
- Plugin to show current values of selected channels. Provides possibility to set values.
- Channel Configurator
- Plugin for channel configuration e.g. channel name, sampling interval, logging interval
- Media Viewer
- Plugin which allows to embed media files into OpenMUC’s WebUI
- User Configurator
- Plugin for user configuration
9.2. Context Root
org.apache.felix.http.context_path
system property.
org.apache.felix.http.context_path=/muc1
9.3. HTTPS
9.4. Custom Plugins
@Component(service = WebUiPluginService.class)
public final class SamplePlugin extends WebUiPluginService {
@Override
public String getAlias() {
return "sampleplugin";
}
@Override
public String getName() {
return "Sample Plugin";
}
}
samplePlugin/src/main/resources/images
.
9.5. Visualisation
svg_document = document.getElementById('samplePluginGraphic').contentDocument;
sampleText = svg_document.getElementById("sampleTextField");
sampleText.textContent = channel.record.value;
sampleText.style.fill = "blue";
10. REST Server
Tip |
The address to access the web service using the provided demo/framework folder is ‘http://localhost:8888/rest/’ |
10.1. Requirements
- Bundle that provides an org.osgi.service.http.HttpService service. In the demo framework, that service is provided by the org.apache.felix.http.jetty bundle.
10.2. Accessing channels
{
"timestamp" : time_val, /*milliseconds since Unix epoch*/
"flag" : flag_val, /*status flag of the record as string*/
"value" : value_val /*actual value. Omitted if "flag" != "valid"*/
}
[
{
"id" : channel1_id, /*ID of the channel as string*/
"record" : channel1_record /*current record. see Record JSON*/
},
{
"id" : channel2_id,
"record" : channel2_record
}
...
]
10.3. CORS
# Activate CORS functionality for the rest Server org.openmuc.framework.server.restws.enable_cors = true # Origins and methods for CORS , for each origin semicolon separated org.openmuc.framework.server.restws.url_cors = http://localhost:4200; http://localhost:8456 org.openmuc.framework.server.restws.methods_cors = GET, PUT; POST org.openmuc.framework.server.restws.headers_cors = Authorization, Content-Type; Authorization
11. Modbus Server
Primary Table | BOOLEAN | SHORT | INT | FLOAT | DOUBLE | LONG | BYTEARRAY[n] |
---|---|---|---|---|---|---|---|
INPUT_REGISTER |
x |
x |
x |
x |
x |
x |
– |
HOLDING_REGISTERS |
x |
x |
x |
x |
x |
x |
– |
Setting | Mandatory | Values | Default | Description |
---|---|---|---|---|
address |
no |
string |
127.0.0.1 |
IP address to listen on |
poolsize |
no |
int |
3 |
Listener thread pool size, only has affects with TCP and RTUTCP |
port |
no |
int |
502 |
Port to listen on |
unitId |
no |
int |
15 |
UnitId of the slave |
type |
no |
string |
tcp |
Connection type (TCP, RTUTCP or UDP) |
setcap 'cap_net_bind_service=+ep' /path/to/program
11.1. Example
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<configuration>
<driver id="virtual">
<device id="sample_device">
<channel id="sample_channel_1">
<serverMapping id="modbus">HOLDING_REGISTERS:1000:INTEGER</serverMapping>
<valueType>INTEGER</valueType>
</channel>
<channel id="sample_channel_2">
<serverMapping id="modbus">HOLDING_REGISTERS:1002:BOOLEAN</serverMapping>
<valueType>BOOLEAN</valueType>
</channel>
<channel id="sample_channel_3">
<serverMapping id="modbus">INPUT_REGISTERS:1000:DOUBLE</serverMapping>
</channel>
<channel id="sample_channel_4">
<serverMapping id="modbus">INPUT_REGISTERS:1004:LONG</serverMapping>
<valueType>LONG</valueType>
</channel>
</device>
</driver>
</configuration>
org.openmuc.framework.server.modbus.address=127.0.0.1 org.openmuc.framework.server.modbus.port=5502 org.openmuc.framework.server.modbus.unitId=1 org.openmuc.framework.server.modbus.master=false
12. Tools
12.1. Apache Felix Web Console
12.1.1. Dependencies
osgibundles group: "org.apache.felix", name: "org.apache.felix.webconsole", version: "4.5.4" osgibundles group: "commons-io", name: "commons-io", version: "2.6" osgibundles group: "commons-fileupload", name: "commons-fileupload", version: "1.4" osgibundles group: "commons-codec", name: "commons-codec", version: "1.14"
13. Authors
- Dirk Zimmermann
- Marco Mittelsdorf
- Joern Schumann
- Martin Eberle
- Stefan Feuerhahn
- Albrecht Schall
- Michael Zillgith
- Karsten Müller-Bier
- Simon Fey
- Frederic Robra
- Philipp Fels
- Chris Dieter Medam