Using UPnP enabled devices with Talend - A Belkin WEMO Switch

Universal plug and play (UPnP) devices are ubiquitous these days. More and more homes are filling up with devices that make use of UPnP functionality and this opens lots of doors for Talend users to derive more functionality from connecting these devices. In my home I have the following devices which make use of UPnP protocols....

Samsung Smart TV
Belkin WEMO Switch
Belkin WEMO Sensors
Philips HUE lighting
SONOS Speakers
BT Home Hub Router

Any device that can be connected to your router for internet access is likely to make use of UPnP protocols. There is an easy way to discover UPnP devices using a bit Java to send a discovery message to the multicast address 239.255.255.250 on port 1900 via the UDP protocol and then receive responses and print them out. The following classes will have receive the responses and send the discovery message.

ReceivePackets.java

This class is used to receive and print out the responses to the System.Out. This class needs to be run first.

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.net.SocketTimeoutException;

public class ReceivePackets {

    public static void main(String[] args) {

        ReceivePackets dp = new ReceivePackets();
        try {
            dp.receive();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    public void receive() throws IOException {
        boolean test = true;
        MulticastSocket recSocket = new MulticastSocket(null);
        recSocket.bind(new InetSocketAddress(InetAddress.getByName("0.0.0.0"),
                1900));
        recSocket.setTimeToLive(10);
        recSocket.setSoTimeout(1000);
        recSocket.joinGroup(InetAddress.getByName("239.255.255.250"));
        while (test) { 
            byte[] buf = new byte[2048];
            DatagramPacket input = new DatagramPacket(buf, buf.length);
            try {
                recSocket.receive(input);
                String originaldata = new String(input.getData());

                System.out.println(originaldata);
            } catch (SocketTimeoutException e) {
                System.out.println(e);
            }
        }
        recSocket.disconnect();
        recSocket.close();

    }

}

 

DiscoveryPacket.java

This class sends the discovery packet. This is run after the ReceivePackets class. You need to ensure that your machine's IP address is used where indicated below.....

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;

public class DiscoveryPacket {

    public static void main(String[] args) {

        DiscoveryPacket dp = new DiscoveryPacket();
        try {
            dp.discover();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    public void discover() throws Exception {
        InetSocketAddress socketAddress = new InetSocketAddress(
                "239.255.255.250", 1900);
        MulticastSocket socket = new MulticastSocket(null);

        try {

            socket.bind(new InetSocketAddress("192.168.1.79", 1901)); //<-- Your machine's IP address
            StringBuilder packet = new StringBuilder();
            packet.append("M-SEARCH * HTTP/1.1\r\n");
            packet.append("HOST: 239.255.255.250:1900\r\n");
            packet.append("MAN: \"ssdp:discover\"\r\n");
            packet.append("MX: ").append("5").append("\r\n");
            packet.append("ST: ").append("ssdp:all").append("\r\n")
                    .append("\r\n");
            // packet.append( "ST: " ).append( "urn:Belkin:device:controllee:1"
            // ).append( "\r\n" ).append( "\r\n" )
            byte[] data = packet.toString().getBytes();
            socket.send(new DatagramPacket(data, data.length, socketAddress));

        } catch (IOException e) {
            throw e;
        } finally {
            socket.disconnect();
            socket.close();

        }

    }
}

 

If you use the code above you will get responses similar to the response from my Belkin WEMO Switch below....

NOTIFY * HTTP/1.1
HOST: 239.255.255.250:1900
CACHE-CONTROL: max-age=86400
LOCATION: http://192.168.1.82:49153/setup.xml
OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01
01-NLS: 7af96e40-1aa2-22c1-bcfc-eac9223a69cc
NT: urn:Belkin:service:manufacture:1
NTS: ssdp:alive
SERVER: Unspecified, UPnP/1.0, Unspecified
X-User-Agent: redsonic
USN: uuid:Socket-1_0-324674K1100201::urn:Belkin:service:manufacture:1

The important part you are looking for is the url for the device's UPnP description. This is highlighted above in red. Yours will have a different IP address and possibly a different port number for a Belkin WEMO Switch. Other devices will show different messages, but with a very similar format.

If I open the url above in a web browser, it returns an XML document as below.....

<?xml version="1.0"?>
<root xmlns="urn:Belkin:device-1-0">
  <specVersion>
    <major>1</major>
    <minor>0</minor>
  </specVersion>
  <device>
<deviceType>urn:Belkin:device:controllee:1</deviceType>
<friendlyName>WeMo Switch</friendlyName>
    <manufacturer>Belkin International Inc.</manufacturer>
    <manufacturerURL>http://www.belkin.com</manufacturerURL>
    <modelDescription>Belkin Plugin Socket 1.0</modelDescription>
    <modelName>Socket</modelName>
    <modelNumber>1.0</modelNumber>
    <modelURL>http://www.belkin.com/plugin/</modelURL>
<serialNumber>221234K6789201</serialNumber>
<UDN>uuid:Socket-1_0-22
1234K6780201</UDN>
    <UPC>123456789</UPC>
<macAddress>ED23597C86C</macAddress>
<firmwareVersion>WeMo_WW_2.00.7166.PVT</firmwareVersion>
<iconVersion>0|49153</iconVersion>
<binaryState>0</binaryState>
    <iconList> 
      <icon> 
        <mimetype>jpg</mimetype> 
        <width>100</width> 
        <height>100</height> 
        <depth>100</depth> 
         <url>icon.jpg</url> 
      </icon> 
    </iconList>
    <serviceList>
      <service>
        <serviceType>urn:Belkin:service:WiFiSetup:1</serviceType>
        <serviceId>urn:Belkin:serviceId:WiFiSetup1</serviceId>
        <controlURL>/upnp/control/WiFiSetup1</controlURL>
        <eventSubURL>/upnp/event/WiFiSetup1</eventSubURL>
        <SCPDURL>/setupservice.xml</SCPDURL>
      </service>
      <service>
        <serviceType>urn:Belkin:service:timesync:1</serviceType>
        <serviceId>urn:Belkin:serviceId:timesync1</serviceId>
        <controlURL>/upnp/control/timesync1</controlURL>
        <eventSubURL>/upnp/event/timesync1</eventSubURL>
        <SCPDURL>/timesyncservice.xml</SCPDURL>
      </service>
      <service>
        <serviceType>urn:Belkin:service:basicevent:1</serviceType>
        <serviceId>urn:Belkin:serviceId:basicevent1</serviceId>
        <controlURL>/upnp/control/basicevent1</controlURL>
        <eventSubURL>/upnp/event/basicevent1</eventSubURL>
        <SCPDURL>/eventservice.xml</SCPDURL>
      </service>
      <service>
        <serviceType>urn:Belkin:service:firmwareupdate:1</serviceType>
        <serviceId>urn:Belkin:serviceId:firmwareupdate1</serviceId>
        <controlURL>/upnp/control/firmwareupdate1</controlURL>
        <eventSubURL>/upnp/event/firmwareupdate1</eventSubURL>
        <SCPDURL>/firmwareupdate.xml</SCPDURL>
      </service>
      <service>
        <serviceType>urn:Belkin:service:rules:1</serviceType>
        <serviceId>urn:Belkin:serviceId:rules1</serviceId>
        <controlURL>/upnp/control/rules1</controlURL>
        <eventSubURL>/upnp/event/rules1</eventSubURL>
        <SCPDURL>/rulesservice.xml</SCPDURL>
      </service>
      
      <service>
        <serviceType>urn:Belkin:service:metainfo:1</serviceType>
        <serviceId>urn:Belkin:serviceId:metainfo1</serviceId>
        <controlURL>/upnp/control/metainfo1</controlURL>
        <eventSubURL>/upnp/event/metainfo1</eventSubURL>
        <SCPDURL>/metainfoservice.xml</SCPDURL>
      </service>

      <service>
        <serviceType>urn:Belkin:service:remoteaccess:1</serviceType>
        <serviceId>urn:Belkin:serviceId:remoteaccess1</serviceId>
        <controlURL>/upnp/control/remoteaccess1</controlURL>
        <eventSubURL>/upnp/event/remoteaccess1</eventSubURL>
        <SCPDURL>/remoteaccess.xml</SCPDURL>
      </service>
       
      <service>
        <serviceType>urn:Belkin:service:deviceinfo:1</serviceType>
        <serviceId>urn:Belkin:serviceId:deviceinfo1</serviceId>
        <controlURL>/upnp/control/deviceinfo1</controlURL>
        <eventSubURL>/upnp/event/deviceinfo1</eventSubURL>
        <SCPDURL>/deviceinfoservice.xml</SCPDURL>
      </service>
       
      <service>
        <serviceType>urn:Belkin:service:smartsetup:1</serviceType>
        <serviceId>urn:Belkin:serviceId:smartsetup1</serviceId>
        <controlURL>/upnp/control/smartsetup1</controlURL>
        <eventSubURL>/upnp/event/smartsetup1</eventSubURL>
        <SCPDURL>/smartsetup.xml</SCPDURL>
      </service>
    
      <service>
        <serviceType>urn:Belkin:service:manufacture:1</serviceType>
        <serviceId>urn:Belkin:serviceId:manufacture1</serviceId>
        <controlURL>/upnp/control/manufacture1</controlURL>
        <eventSubURL>/upnp/event/manufacture1</eventSubURL>
        <SCPDURL>/manufacture.xml</SCPDURL>
      </service>

    </serviceList>
   <presentationURL>/pluginpres.html</presentationURL>
</device>
</root>

The XML document above is where we can start to explore the functionality that is available to us. We are interested in the services (<service> elements inside the <serviceList> element). Each of these is described in more detail by following the SCPDURL (Service Control Protocol Description URL) element. This contains a relative path to another XML document that describes that service. In this case, we are looking at the ServiceType "urn:Belkin:service:basicevent:1". This has the URL http://192.168.1.82:49153/eventservice.xml as "/eventservice.xml" is relative to "http://192.168.1.82:49153". If we open this url, it tells us more about what is available to us. This is a good point to note that it is a good idea to explore all of the services before developing with UPnP so that you can get an understanding of what you are working with.

Below is the XML that the URL above points to. We are only interested in 2 of the actions and a couple of the states. As such I have removed the other sections. This document can be very large and there is no point confusing the issue by showing all of it straight away......

<?xml version="1.0"?>
<scpd xmlns="urn:Belkin:service-1-0">

  <specVersion>
    <major>1</major>
    <minor>0</minor>
  </specVersion>
  
  <actionList>
  
    <action>
      <name>SetBinaryState</name>
      <argumentList>
         <argument>
           <retval />
           <name>BinaryState</name>
           <relatedStateVariable>BinaryState</relatedStateVariable>
           <direction>in</direction>
          </argument>
         <argument>
           <retval />
           <name>Duration</name>
           <relatedStateVariable>Duration</relatedStateVariable>
           <direction>in</direction>
          </argument>
         <argument>
           <retval />
           <name>EndAction</name>
           <relatedStateVariable>EndAction</relatedStateVariable>
           <direction>in</direction>
          </argument>
         <argument>
           <retval />
           <name>UDN</name>
           <relatedStateVariable>UDN</relatedStateVariable>
           <direction>in</direction>
          </argument>
      </argumentList>
    </action>

    .................

    <action>
    <name>GetBinaryState</name>
    <argumentList>
    <argument>
    <retval/>
    <name>BinaryState</name>
    <relatedStateVariable>BinaryState</relatedStateVariable>
    <direction>out</direction>
    </argument>
    </argumentList>
    </action>
    

</actionList>

  <serviceStateTable>
  
    <stateVariable sendEvents="yes">
      <name>BinaryState</name>
      <dataType>string</dataType>
      <defaultValue>0</defaultValue>
    </stateVariable>
    

    <stateVariable sendEvents="no">
      <name>Duration</name>
      <dataType>string</dataType>
      <defaultValue>0</defaultValue>
    </stateVariable>

    <stateVariable sendEvents="no">
      <name>EndAction</name>
      <dataType>string</dataType>
      <defaultValue>0</defaultValue>
    </stateVariable>

    <stateVariable sendEvents="no">
      <name>UDN</name>
      <dataType>string</dataType>
      <defaultValue>0</defaultValue>
    </stateVariable>

    ...........

  </serviceStateTable>
  
  </scpd>

 

The XML above gives us information on two actions; GetBinaryState and SetBinaryState. These actions allow us to check the current state of the WEMO Switch (ON/OFF or 1/0) and set the state if we wish to change it. These are the actions that we will be using in this tutorial. It also gives us information on the state variables that are required to interact with the actions. These state variables can be a little hit and miss with some UPnP implementations (SONOS being a good example of this), but with a little trial and error they can be figured out. With the Actions above, only the "BinaryState" variable is important for the "SetBinaryState" action. Using this information we can construct XML messages which will fire these actions when sent using a Talend tSOAP component.

As an example, we will look at the GetBinaryState action. Below is the XML that is needed to be supplied as the SOAP Message of the tSOAP component.
The code in green show elements that are part of the SOAP Envelope namespace.
Within the <Body> element we have the <u:GetBinaryState> element. This element's namespace is the ServiceType "urn:Belkin:service:basicevent:1" mentioned above. This code is coloured red.

<?xml version="1.0" encoding="utf-8"?>
            <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
                <s:Body>

                    <u:GetBinaryState xmlns:u="urn:Belkin:service:basicevent:1">
                        <BinaryState></BinaryState>
                    </u:GetBinaryState>
                </s:Body>
            </s:Envelope>

In the above example, there is no need to include the <BinaryState> element as it was not an "in" variable. However, including it with no value does no harm. When this is sent to the WEMO device, a corresponding message is returned containing the current state of the device. An example of the message showing the device is currently on is shown below....

<u:GetBinaryStateResponse xmlns:u="urn:Belkin:service:basicevent:1">
<BinaryState>1</BinaryState>
</u:GetBinaryStateResponse>

Hopefully this has given you a basic understanding of the WEMO UPnP implementation. It should be good enough to now start the Talend tutorial on how to use the GetBinaryState and SetBinaryState actions to use a Talend Job as a switch.

 

The WemoSwitchStateChange Job

Below we can see a screenshot of this Job. This is a very simple Job that can easily be used as a service or as part of a more complex Job. 

Context Variables

In this example, there are two context variables that are used. These can be seen below....

Change the "ip" and "port" values to be the values that are required for your system.

1) "Get current switch state" (tSOAP)

This component is used to check the current state of the WEMO switch. It configuration can be seen below....

The "Endpoint" value uses the "ip" and "port" context variables. The rest is made up of the value held by the <controlURL> element from the UPnP description XML. It is from the <controlURL> element related to the ServiceType "urn:Belkin:service:basicevent:1". The value needed for the tutorial is inside the box below....

"http://"+context.ip+":"+context.port+"/upnp/control/basicevent1"

The "SOAP Action" value is the ServiceType with the name of the Action being used. The \'s are there to escape the quotes which are needed. The actual value you need for this example is in the box below....

"\"urn:Belkin:service:basicevent:1#GetBinaryState\""

The "SOAP Message" box contains the message that describes the Action and supplies any required variables. In this case there are no variables. The \'s are there to escape the quotes which are needed. The actual value you need for this example is in the box below....

"<?xml version=\"1.0\" encoding=\"utf-8\"?>
            <s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">
                <s:Body>
                    <u:GetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\">
                     </u:GetBinaryState>
                </s:Body>
            </s:Envelope>"

 

2+7) "Display State" and "Display new state" (tLogRow)

 

These components are simply connected to show the state of the data as it passes through the Job. There is no configuration required.

3) "Convert String to Body Doc" (tConvertType)

This component is simply used to convert the "Body" column from the datatype String to Document. The screenshot below shows the Input has the "Body" column type as String and the output has it as Document.

4) "Get alternate state as Integer" (tXMLMap)

This component is used to retrieve the "BinaryState" value from the Body Document. The component needs to be configured as below. 

The easiest way to create the "Body" schema is shown below. First, right click on the "root" element and select "Rename". Rename this element to be "u:GetBinaryStateresponse".

Then, right click on the "u:GetBinaryStateresponse" element and select "Create Sub-Element". Give it the name "BinaryState". The schema is now configured.

On the output side of the tXMLMap component, we need to create an output table by clicking on the green plus symbol. Once that is created, a column called "BinaryState" of type Integer is needed. Then the following code needs to be added to the expression of the "BinaryState" output column....

Integer.parseInt([row6.Body:/u:GetBinaryStateResponse/BinaryState])==1 ? 0 : 1     

The above code converts the value held by the XML Document to be an Integer and checks if its value is equal to 1. If it is, then the value for the output "BinaryState" column will be 0, if it is not, the value of the output "BinaryState" column will be 1. This is the switch mechanism logic. If it is currently 1, change it to 0 and vice versa. 

5) "Store alternate state Integer" (tSetGlobalvar)

This component stores the value calculated by the tXMLMap component (and output as "BinaryState") as a global variable called "state". This is used to set the "BinaryState" when the "SetBinaryState" action is used in the next step. The configuration can be seen below....

6) "Set new state" (tSOAP)

This component is used to change the state of the WEMO switch. It configuration can be seen below....

The "Endpoint" value uses the "ip" and "port" context variables. The rest is made up of the value held by the <controlURL> element from the UPnP description XML. It is from the <controlURL> element related to the ServiceType "urn:Belkin:service:basicevent:1". The value needed for the tutorial is inside the box below....

"http://"+context.ip+":"+context.port+"/upnp/control/basicevent1"

The "SOAP Action" value is the ServiceType with the name of the Action being used. The \'s are there to escape the quotes which are needed. The actual value you need for this example is in the box below....

"\"urn:Belkin:service:basicevent:1#SetBinaryState\""

The "SOAP Message" box contains the message that describes the Action and supplies any required variables. In this case there is a single variable "<BinaryState>". This is set using the value held by the "state" global variable saved in step 5. The \'s are there to escape the quotes which are needed. The actual value you need for this example is in the box below....

"<?xml version=\"1.0\" encoding=\"utf-8\"?>
            <s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">
                <s:Body>
                    <u:SetBinaryState xmlns:u=\"urn:Belkin:service:basicevent:1\">
            <BinaryState>"+((Integer)(globalMap.get("state")))+"</BinaryState>
                    </u:SetBinaryState>
                </s:Body>
            </s:Envelope>"

 

 

Running the Job

To run this Job simply click on the "Run" button on the "Run" tab. If it has been configured correctly, you will see XML similar to below showing in the Run Window....

Starting job WemoSwitchStateChange at 19:20 06/01/2015.

[statistics] connecting to socket on port 3365
[statistics] connected
|<u:GetBinaryStateResponse xmlns:u="urn:Belkin:service:basicevent:1">
<BinaryState>0</BinaryState>
</u:GetBinaryStateResponse>|
|<u:SetBinaryStateResponse xmlns:u="urn:Belkin:service:basicevent:1">
<BinaryState>1</BinaryState>
</u:SetBinaryStateResponse>|
[statistics] disconnected
Job WemoSwitchStateChange ended at 19:20 06/01/2015. [exit code=0]

The first XML string shows the current state (0) and the second XML string shows the new state (1).

A copy of the completed tutorial can be found here. It was built using Talend 5.5.1 but can be imported into subsequent versions. It cannot be imported into earlier versions, so you will either need to upgrade or recreate it following the tutorial. You will need to set the Context variables according to your system before running it.

Talend Version: 
Type of content: