2012/12/10

Learning how to control an existing KNX (Konnex) Infrastructure

Now you may wonder why would want to control a KNX infrastructure (sensors, actuators, etc)? Specially nowadays when Arduino and Raspberriers boards are invading us. Don't get me wrong, I'm an Arduino and RPI enthusiast and use them whenever possible, but is also true that there are many places where you find legacy systems all built with KNX technology. Other times, this is a requirement, so you're forced to use KNX no matter what. Whatever the reason, whether you want to control some of the KNX devices you may have at home/work, or simply because you're curious, keep reading and I'll show you how to write a simple Java program to interact with a KNX network.

 The not so known KNX protocol

As part of a project I'm working on right now, had to integrate a Konnex network of sensors and actuators with a middleware developed in my company, some of it by myself. Thus, had to create a Java client to connect and control KNX devices in an existing KNX network we have at work.

The KNX protocol (usually called Konnex) is the nowadays standard in sensor and actuators network for "intelligent" buildings, mostly across Europe (as far as I know). Bad news is that if you try to get hands on the protocol specification, you're out of luck, unless you can spare a grand (€) on it (serious).

The KNX specification, hard to find as the Necronomicon

Now for normal folks like me, this is unthinkable and besides, the whole idea of paying to get a protocol specification enrages me, goes against my thinking about how protocols and standards should be published. Standards and protocols should always be open and accessible, in my opinion. Wanna make money out of it? Fine, but do it with something that adds value, not charging X amount for downloading a document.

Anyway, luckily enough KNX is not supposed to bind to any underlying hardware communication infrastructure and as such, there is an implementation of the protocol that allows KNX messages to go through an IP network, using UDP. This implementation is known as EIBnet/IP. Looking for an implementation of this, I found the excellent work done by Martin Kogler, who has developed a BCU (Bus Coupling Units) SDK that will allow us to encapsulate KNX messages in UDP packages to be sent over an IP network. The SDK itself does much more than this, but in our case, we'll just focus on the task at hand. I'll leave it up to you if you want to go in the specifics about the SDK. 

   
Server side

Obviously, you must have some KNX sensors and actuators in an already existing KNX network to try this. Besides, you'll need a USB adapter from KNX network. Basically it is a hardware piece that connects to the KNX bus on one side and outputs KNX messaages through a USB standard cable. Is not precisly cheap hardware, but nothing is in the KNX world. The one I'm using is from JUNG, and looks like this:

JUNG USB Datenschnittstelle
That's about the hardware. Let's go back to the software part, more precisely, the BCU SDK. In order to use the SDK, you can download some pre-built packages for Debian or RedHat or you can compile those yourself. I'd highly recommend that you build the sources, and this is what I'll explain here, but you can always opt for the first choice and see if it works out-of-the-box for you. If you choose to compile, the SKD requires to have pthsem compiled and installed in /usr/lib. Not really sure what pthsem does, but it looks that it has to do with the GNU pth library and some semaphore functionality built on top. Steps are:

  • Download the sources for phtsem here.
  • Extract, compile and deploy:
$ tar zxvf pthsem_2.0.8.tar.gz
$ cd pthsem-2.0.8
$ ./configure --disable-shared
$ make
$ sudo make install 
 
  • Now download sources for bcu skd.
  • Do as before:
$ tar zxvf bcusdk_0.0.5.tar.gz
$ cd bcusdk-0.0.5
$ ./configure --without-pth-test --enable-onlyeibd 
     --enable-ft12 --enable-pei16 --enable-eibnetip 
     --enable-eibnetiptunnel  --enable-eibnetipserver 
     --enable-usb
$ make
$ sudo make install 

If you are wondering what the flags are for, just type configure --help to know about them. Let me state that those are the needed ones in the current scenario (controlling a KNX network via a USB adapter).

Now having those two compiled and deployed, we can plug the USB cable, but there are still some things to do first. First, we must create a user group that has permissions to read and write at /dev/bus/usb. You can alwasy start EIB server as root, but this is not recommeded. So to do this, create a new file in /etc/udev/rules.d/ named 80-knx.rules with the following contents:

SUBSYSTEM=="usb", ATTR{idVendor}=="135e", ATTR{idProduct}=="0023", ACTION=="add", GROUP="knx", MODE="0664"

You may have to change the idVendor and idProduct attributes to match the ones from your USB adapter. You can check this with lsusb command. In my case, output is the following, you can see in the last line that the idVendor:idProduct of my USB adapter (135e:0023):

$ lsusb
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 002: ID 0424:9512 Standard Microsystems Corp.
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp.
Bus 001 Device 004: ID 135e:0023

  • Create a user group, name it however you desire:

$ sudo addgroup knx
  • Now include the users that will start the EIB server to this group. Change user for whatever you users are


$ sudo usermod -a -G knx usuario

In many Linux distros (including my one), the USB filesystem is not mounted. Thus, you'll have to do this manually, or modify /etc/fstab to include the usbfs, although this last option never worked for me, always get an error message at reboot.

sudo mount --bind /dev/bus /proc/bus

Almost ready on the server part! Time to plug-in your USB cable and do some testing.

Yeah, that one
Once plugged, you can use the findknxusb command to check if you can find your knx USB adapter. In my case, I'm getting my JUNG adapter.

$ findknxusb
Possible addresses for KNX USB devices:
device: 1:4:1:0:0 (ALBRECHT JUNG GMBH & CO. KG:KNX-USB Data Int)

Now you can try to get messages from a KNX network by starting the eib server:


$ eibd -i usb: &

And the use vbusmonitor1 command with default IP address used by eibd command to print incoming messages from the KNX network.


$ vbusmonitor1 ip:127.0.0.1

If you see something, you're almost there! Now try to send some KNX messages to the network. In my case, among other things I have automatic blinds at address 2/5/1, so I can pull them up/down using state = 1 or vice-versa. Try this with command groupswrite.


$ groupswrite ip:127.0.0.1 2/5/1 1 -> Pull up blinds

While this is good for testing, I recommend starting the EIB server so it serves as a EIBnet/IP server. To do this, you have to start the daemon this way:

$ eibd -S -T usb: &

Doing this everytime your machine reboots is a pain, thus, I created a couple bash scripts to do this for you. First one is called starteib:

#!/bin/bash
echo "Mounting konnex usb in /proc/bus"

CMD=`sudo -k mount --bind /dev/bus /proc/bus 2>&1`
if [ $? != 0 ]; then
{
    echo "Could not mount /dev/bus in /proc/bus. Aborting"
    echo $CMD
    exit 1
} fi

echo "Mounting finished correctly!"

echo "Starting EIB server..."

eibd -S -T usb: &

Simply mount the USB filesystem, with root momentary privileges (-k) and then start the eibd daemon. Use -k so you don't start the eibd as root (remember, this is not recommended). Second one will simply stop the eibd:


#!/bin/bash
echo "Stopping eib server..."

CMD=`killall eibd 2>&1`
if [ $? != 0 ]; then
{
    echo "Could not stop eibd. Aborting"
    echo $CMD
    exit 1
} fi

echo "Stopped eib server correctly"

Finished with the server side, time to focus on the client.

Client side

The BCU SDK provides several client implementations, but after trying some of them I decided to use the KNX client implementation known as Calimero.


It works in my computer, I swear!

Calimero is a really nice client library for KNX networks that use the EIBnet/IP protocol. You can use it to connect to a server created with the BCU SDK, read messages from that network, and also send messages back to the network, in order to actually "do something", besides reading values. That said, let's see what we need to get Calimero up and running:

  • Download latest build here
  • Create a simple Java project in your favorite IDE
  • Add calimero JAR file to your library path
  • Start coding!
Make sure you've started the eibd server from BCU SDK before trying to connect with Calimero. Here is a simple program that will connect to an existing eibnet server.

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;

import tuwien.auto.calimero.exception.KNXException;
import tuwien.auto.calimero.link.KNXNetworkLink;
import tuwien.auto.calimero.link.KNXNetworkLinkIP;
import tuwien.auto.calimero.link.medium.TPSettings;



public class CalimeroTest {

 /**
  * The object used to interact with the KNX network
  */
 private KNXNetworkLink knxLink = null;
 
 
 /**
  * Connects to the KNX network though the EIBD server, thus the EIBD server
  * must be up&running first
  * @param serverIP The IP where the EIBD server should be listening
  * @param serverPort The Port where the EIBD server is listening
  * @return TRUE if connection successful
  */
 public boolean connectToEIBD(String serverIP, int serverPort) {
  
  try {
   InetSocketAddress local = new InetSocketAddress(InetAddress.getLocalHost(), 0);
   InetSocketAddress remote = new InetSocketAddress(serverIP, serverPort);
   knxLink = new KNXNetworkLinkIP(KNXNetworkLinkIP.TUNNEL, local, remote, false, TPSettings.TP1);
   
   return true;
  }
  catch (KNXException e){
   System.out.println("There was a problem trying to create a KNXNetworkLink object or creating a ProcessCommunicator object. More info " + e.getStackTrace());
   return false;
  } 
  catch (UnknownHostException e) {
   System.out.println("There was a problem trying to obtain the local host address, aborting.");
   return false;
  }  
  
 }
 
 /**
  * @param args
  */
 public static void main(String[] args) {
  CalimeroTest test = new CalimeroTest();
  String address = "127.0.0.1";
  int port = 3671;
  if (test.connectToEIBD(address, port)){
   System.out.println("Connected! Yeeha!");
  }

 }

}

As you can see, connecting to an existing eibd server is quite easy. Now if you want to get messages from the KNX netowrk, the way to go with Calimero is creating a listener class that has call-back methods triggered each time a KNX message is sent through the network. This is the class that will listen to incoming KNX messages:

import tuwien.auto.calimero.DetachEvent;
import tuwien.auto.calimero.process.ProcessEvent;
import tuwien.auto.calimero.process.ProcessListener;


/**
 * Class used to capture KNX events from the EIBD server
 * 
 * */
class KNXListener implements ProcessListener {
  
 public KNXListener() {
 }

 @Override
 /**
  * Callback method whenever something is written in the KNX network
  * @param Contains the information about the event occured
  */
 public void groupWrite(ProcessEvent event) {
  
  String destAddr = event.getDestination().toString();
   
  //Message value (state)
  byte[] asdu = event.getASDU();
  //Check if state was correct
  int state = ((asdu != null) && (asdu.length > 0)) ? asdu[0] : -1;
   
  System.out.println("Group address is " + destAddr + "and state is " + state);  
 }
  
 @Override
 public void detached(DetachEvent arg0) {
   
  System.out.println("The KNXNetworkLink has been disconnected from the Process Monitor");  
 }
}

Each time a message arrives the groupWrite method is called, you can check what group address was causing the message and check the state, which is normally binary (0-1), but not always. Let's see how to send messages, modify the listener class from before, supposing there is a group address with value "1/1/1" and we want to send state 1, just think of it as light switch we want to swich on:

import tuwien.auto.calimero.DetachEvent;
import tuwien.auto.calimero.GroupAddress;
import tuwien.auto.calimero.exception.KNXFormatException;
import tuwien.auto.calimero.exception.KNXTimeoutException;
import tuwien.auto.calimero.link.KNXLinkClosedException;
import tuwien.auto.calimero.process.ProcessEvent;
import tuwien.auto.calimero.process.ProcessListener;


/**
 * Class used to capture KNX events from the EIBD server
 * 
 * */
class KNXListener implements ProcessListener {
 
 /**
  * Reference to the CalimeroTest class
  */
 CalimeroTest tester; 
 
 public KNXListener(CalimeroTest t) {
  tester = t;
 }

 @Override
 /**
  * Callback method whenever something is written in the KNX network
  * @param Contains the information about the event occured
  */
 public void groupWrite(ProcessEvent event) {
  
  String destAddr = event.getDestination().toString();
   
  //Message value (state)
  byte[] asdu = event.getASDU();
  //Check if state was correct
  int state = ((asdu != null) && (asdu.length > 0)) ? asdu[0] : -1;
   
  System.out.println("Group address is " + destAddr + "and state is " + state);
 
  String address = new String("1/1/1");
  sendDataToDevice(address, true);
  
 }
  
 @Override
 public void detached(DetachEvent arg0) {
   
  System.out.println("The KNXNetworkLink has been disconnected from the Process Monitor");  
 }
 
 /**
  * Method used to send a command to a KNX device
  * @param deviceAddress  The group address of the device
  * @param value    The value to send to the device TRUE or FALSE
  * @return     TRUE if everything went fine
  */
 public boolean sendDataToDevice(String deviceAddress, boolean value){
  //try to send data to a device
  try {
   System.out.println("Trying to send message to device with address " +  deviceAddress + "...");
   GroupAddress gp = new GroupAddress(deviceAddress);
   tester.getPc().write(gp, value);
   System.out.println("Sent message to device with address " +  deviceAddress + " successfully");
   return true;
  } 
  catch (KNXFormatException e) {
   System.out.println("Could not obtain GroupAddress from string provided. Aborting send");
   return false;
  } 
  catch (KNXTimeoutException e) {
   System.out.println("Timeout ocurred while trying to send a write to the KNX network. Aborting send.");
   return false;
  } 
  catch (KNXLinkClosedException e) {
   System.out.println("The link was closed while trying to send a command. Aborting send");
   return false;
  }
  
 }
}

Obviously, the CalimeroTest class also needs to be modified accordingly:

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;

import tuwien.auto.calimero.exception.KNXException;
import tuwien.auto.calimero.link.KNXNetworkLink;
import tuwien.auto.calimero.link.KNXNetworkLinkIP;
import tuwien.auto.calimero.link.medium.TPSettings;
import tuwien.auto.calimero.process.ProcessCommunicator;
import tuwien.auto.calimero.process.ProcessCommunicatorImpl;



public class CalimeroTest {

 /**
  * The object used to interact with the KNX network
  */
 private KNXNetworkLink knxLink = null;
 
 /**
  * The object used to read and write from the KNX network
  */
 private ProcessCommunicator pc = null;
 
 /**
  * A listener class used to capture KNX events
  */
 private KNXListener listener = null;
 
 
 /**
  * Connects to the KNX network though the EIBD server, thus the EIBD server
  * must be up&running first
  * @param serverIP The IP where the EIBD server should be listening
  * @param serverPort The Port where the EIBD server is listening
  * @return TRUE if connection successful
  */
 public boolean connectToEIBD(String serverIP, int serverPort) {
  
  try {
   listener = new KNXListener(this);
   InetSocketAddress local = new InetSocketAddress(InetAddress.getLocalHost(), 0);
   InetSocketAddress remote = new InetSocketAddress(serverIP, serverPort);
   knxLink = new KNXNetworkLinkIP(KNXNetworkLinkIP.TUNNEL, local, remote, false, TPSettings.TP1);
   setPc(new ProcessCommunicatorImpl(knxLink));
   getPc().addProcessListener(listener);
   return true;
  }
  catch (KNXException e){
   System.out.println("There was a problem trying to create a KNXNetworkLink object or creating a ProcessCommunicator object. More info " + e.getStackTrace());
   return false;
  } 
  catch (UnknownHostException e) {
   System.out.println("There was a problem trying to obtain the local host address, aborting.");
   return false;
  }  
  
 }
 
 /**
  * @param args
  */
 public static void main(String[] args) {
  CalimeroTest test = new CalimeroTest();
  String address = "127.0.0.1";
  int port = 3671;
  if (test.connectToEIBD(address, port)){
   System.out.println("Connected! Yeeha!");
  }

 }

 public ProcessCommunicator getPc() {
  return pc;
 }

 public void setPc(ProcessCommunicator pc) {
  this.pc = pc;
 }

}

And that's it! You now know how to connect, read and write messages using Calimero. Piece of cake. In case you're feeling lazy, you can download the calimero test project here.

Hope you find this useful, you can have a lot of fun experimenting  what can be done with a KNX network. Enjoy!

7 comments:

  1. Hola Alejandro, que gran tutorial has elaborado!!! Estoy intentando instalar eibd en la Raspberry, de hecho no se como conseguí que me funcionase, pero volví a empezar de nuevo y ahora ya no lo consigo hacer funcionar. Podrías hecharme una mano, estoy desesperado.

    Muchas gracias

    Podríamos contactar por mail???

    Saludos Javier

    ReplyDelete
  2. Hola Jabi,

    Claro, mandame un mail e intentare ayudarte

    ReplyDelete
  3. Hello Alejandro,
    Does Calimero API support direct communication via serial, usb, etc. to the KNX bus? I think you used BCU SDK to establish an IP link to the client project to test IP access of Calimero. If Calimero supports direct USB interface to Knx bus, so we won't need BCU server right?

    ReplyDelete
  4. Hello Zafer,

    I'm afraid not, but you can check the Calimero docs for more info. I think you do need yes or yes the BCU server to act as a gateway. If you don't like Calimero, the BCU SDK site has some examples on creating clients and testing the server too.

    Hope this helps,
    Alejandro

    ReplyDelete
  5. Hola Alejandro, una excelente post, implementando la partes servidor, para enlazar con usb realizas la siguiente instruccion:
    $ eibd -S -T USB: &
    No debirias de utilizar la direccion del usb que nos muestra en el metodo Findknxusb en vez de utilizar el ampersan?, o el ampersan hace de comodin?
    En pruebas que he realizado el & me da un error de sintaxis.

    Saludos

    ReplyDelete
  6. Hola Sergio,

    El & lo unico que hace es mandar el proceso al background, de modo que no se te quede la sesion de la terminal "bloqueada" por este proceso, con lo que si haces CTRL-C, lo matas y tienes que volver a empezar. La verdad que no te puedo ayudar con esto ahora mismo, hace tiempo que formatee y no he vuelto a reproducir el entorno, con lo que no te podria dar una pronta respuesta.

    ReplyDelete
  7. Now you can buy for 0$ the KNX specifications : www.knx.org after that myKNX and create a account go on the shop buy for free the specifications go in my product for download them!

    ReplyDelete