Universal Plug and Play

UPnP Control Point Implementation Guide

Introduction

This document shows how to build UPnP control point applications using the Applied Informatics Universal Plug and Play framework.

When building a control point, the following steps must be performed to set up the UPnP framework.

  1. Generate the UPnP and RemotingNG infrastructure code.
  2. Set up the SOAP transport and transport factory.
  3. Set up the Poco::UPnP::SSDP::SSDPResponder and search for devices or services.
  4. Set up a HTTP server for receiving GENA event notifications (if required).
  5. Resolve discovered devices and services.
  6. Subscribe to a device's event notifications.
  7. Control the device by sending SOAP requests.

These steps are explained in the following sections.

Building A Control Point Application

The following sections explain the necessary steps to build a control point application.

Generating the UPnP and RemotingNG Infrastructure Code

In order to invoke actions on a device, and to receive event notifications from a device, C++ classes must be generated from the respective UPnP service descriptions. This is done with the UPnPGen and RemoteGenNG tools. Please refer to the UPnP Control And Eventing Tutorial And User Guide for more information. Note that in case of a control point, the RemoteGenNG tool must be invoked with —mode=client or —mode=both in order to create the Proxy and ClientHelper classes.

Setting Up The SOAP Transport

The SOAP Transport is required to send UPnP control requests to devices. Setting up the SOAP Transport is straightforward — all that must be done is registering the Transport Factory for the SOAP Transport with the RemotingNG ORB:

Poco::UPnP::SOAP::TransportFactory::registerFactory();

Setting Up The SSDPResponder

To set up the SSDPResponder, first a multicast socket must be created and configured. Then, a Poco::UPnP::SSDP::SSDPResponder instance is created using the multicast socket. A delegate function must be registered to receive notifications about discovered devices and services (advertisements). The multicast group for the SSDP protocol must be joined, and the SSDPResponder is started.

Poco::Net::NetworkInterface mcastInterface(findActiveNetworkInterface());
Poco::Net::IPAddress ipAddress(mcastInterface.address());
Poco::Net::SocketAddress sa(Poco::Net::IPAddress(), SSDPResponder::MULTICAST_PORT);
Poco::Net::MulticastSocket socket(sa);
socket.setTimeToLive(4);
socket.setInterface(mcastInterface);
SSDPResponder ssdpResponder(_timer, socket);
ssdpResponder.advertisementSeen += Poco::delegate(this, &NetworkLightController::onAdvertisementSeen);
socket.joinGroup(ssdpResponder.groupAddress().host(), mcastInterface);
ssdpResponder.start();

The findActiveNetworkInterface() method searches for a suitable network interface. This is necessary, as the IP address of the network interface being used must be known in order to receive events from UPnP devices. The method looks like this:

Poco::Net::NetworkInterface findActiveNetworkInterface()
{
    Poco::Net::NetworkInterface::NetworkInterfaceList ifs = Poco::Net::NetworkInterface::list();
    for (Poco::Net::NetworkInterface::NetworkInterfaceList::iterator it = ifs.begin(); it != ifs.end(); ++it)
    {
        if (!it->address().isWildcard() && !it->address().isLoopback() && it->supportsIPv4()) return *it;
    }
    throw Poco::IOException("No configured Ethernet interface found");
}

The onAdvertisementSeen() callback method will be discussed later.

Finally, a search request for a specific device type is sent.

ssdpResponder.search("urn:schemas-upnp-org:device:DimmableLight:1");

Setting Up The HTTP server

In order to receive event notifications from devices, a HTTP server must be set up and configured to handle incoming GENA requests. Also, the GENA Listener must be registered with the RemotingNG ORB in order to properly process the incoming event notifications.

Poco::Net::SocketAddress sa(Poco::Net::IPAddress(), SSDPResponder::MULTICAST_PORT);
Poco::Net::SocketAddress httpSA(ipAddress, httpSocket.address().port());
Poco::UPnP::GENA::Listener::Ptr pGENAListener = new Poco::UPnP::GENA::Listener(httpSA.toString(), _timer);
Poco::Net::HTTPServer httpServer(new RequestHandlerFactory(*pGENAListener), httpSocket, new Poco::Net::HTTPServerParams);
httpServer.start();     
Poco::RemotingNG::ORB::instance().registerListener(pGENAListener);

The RequestHandlerFactory class must create a Poco::UPnP::GENA::RequestHandler instance for every incoming NOTIFY request. If required by the application, it can also handle other requests (e.g. for web pages, etc.), but for a control point, handling NOTIFY requests is sufficient.

class RequestHandlerFactory: public Poco::Net::HTTPRequestHandlerFactory
{
public:
    RequestHandlerFactory(Poco::UPnP::GENA::Listener& genaListener):
        _genaListener(genaListener)
    {
    }

    Poco::Net::HTTPRequestHandler* createRequestHandler(const Poco::Net::HTTPServerRequest& request)
    {
        if (request.getMethod() == "NOTIFY")
        {
            return new Poco::UPnP::GENA::RequestHandler(_genaListener);
        }
        else return 0;
    }

private:
    Poco::UPnP::GENA::Listener& _genaListener;
};

Resolving Devices And Services

Once a suitable device or service has been discovered, the device description (and optionally, the service descriptions) must be downloaded from the device and proxy objects for the required services must be created.

This is done in the callback method for the advertisementSeen event provided by the SSDPListener.

In the following example, device type advertisements are handled, and if the correct device type (urn:schemas-upnp-org:device:DimmableLight:1) has been discovered, the device description is downloaded from the device. This is done in a separate task, in order to return from the callback method as fast as possible. In order to fire up the download task, the Poco::Util::Timer instance otherwise used by the SSDPResponder is reused.

void onAdvertisementSeen(Advertisement::Ptr& pAdvertisement)
{
    if (pAdvertisement->type() == Advertisement::AD_DEVICE_TYPE)
    {
        if (pAdvertisement->notificationType() == "urn:schemas-upnp-org:device:DimmableLight:1")
        {
            _timer.schedule(new FetchDeviceDescriptionTask(*this, pAdvertisement->location()), Poco::Timestamp());
        }
    }
}

The FetchDeviceDescriptionTask class simply calls the fetchDeviceDescription() method in the application class.

class FetchDeviceDescriptionTask: public Poco::Util::TimerTask
{
public:
    FetchDeviceDescriptionTask(NetworkLightController& nlc, const std::string& location):
        _nlc(nlc),
        _location(location)
    {
    }

    void run()
    {
        _nlc.fetchDeviceDescription(_location);
    }

private:
    NetworkLightController& _nlc;
    std::string _location;
};

The fetchDeviceDescription() method is where all the work is done. First, the XML device description is downloaded from the discovered device. The device description is loaded into a Poco::Util::XMLConfiguration object in order to simplify the extraction of the URLs for control and eventing.

Next, the code iterates over all service elements in the device description and extracts the URLs for the two services needed by the application. The URL is then used to create a RemotingNG Proxy object for invoking the service. In the case of the Dimming service, the Proxy object is also set up for receiving event notifications.

void fetchDeviceDescription(const std::string& location)
{
    Poco::URI uri(location);
    Poco::SharedPtr<std::istream> pStream = Poco::URIStreamOpener::defaultOpener().open(uri);
    Poco::AutoPtr<Poco::Util::XMLConfiguration> pDeviceDescription = new Poco::Util::XMLConfiguration(*pStream);
    Poco::URI baseURL(pDeviceDescription->getString("URLBase"), location);
    int i = 0;
    while (pDeviceDescription->hasProperty(Poco::format("device.serviceList.service[%d].serviceType", i)))
    {
        std::string serviceType = pDeviceDescription->getString(Poco::format("device.serviceList.service[%d].serviceType", i));
        std::string controlURL  = pDeviceDescription->getString(Poco::format("device.serviceList.service[%d].controlURL", i));
        std::string eventSubURL = pDeviceDescription->getString(Poco::format("device.serviceList.service[%d].eventSubURL", i), "");

        if (serviceType == "urn:schemas-upnp-org:service:SwitchPower:1")
        {
            if (!_pSwitchPower)
            {
                Poco::URI switchPowerControlURL(baseURL, controlURL);
                _pSwitchPower = UPnPS::LightingControls1::SwitchPowerClientHelper::find(switchPowerControlURL.toString());
                Poco::URI switchPowerEventSubURL(baseURL, eventSubURL);
                _pSwitchPower.cast<UPnPS::LightingControls1::SwitchPowerProxy>()->remoting__setEventURI(switchPowerEventSubURL);
            }
        }
        else if (serviceType == "urn:schemas-upnp-org:service:Dimming:1")
        {
            if (!_pDimming)
            {
                Poco::URI dimmingControlURL(baseURL, controlURL);
                _pDimming = UPnPS::LightingControls1::DimmingClientHelper::find(dimmingControlURL.toString());
                Poco::URI dimmingEventSubURL(baseURL, eventSubURL);
                _pDimming.cast<UPnPS::LightingControls1::DimmingProxy>()->remoting__setEventURI(dimmingEventSubURL);
            }
        }
        i++;
    }
}

Note that the above code won't find services in nested devices. An improved version of this code that also works with nested devices can be found in the IGDClient sample.

Subscribing To Events

In order to receive event notifications via a Proxy object, a delegate must be registered with the respective event member of the Proxy, and the Proxy object must be enabled to receive events by calling the remoting__enableEvents() method, passing a pointer to the Poco::UPnP::GENA::Listener as argument.

_pDimming->loadLevelStatusChanged += Poco::delegate(this, &NetworkLightController::onLoadLevelStatusChanged);
_pDimming->remoting__enableEvents(pGENAListener);

The event callback method is straightforward:

void onLoadLevelStatusChanged(const Poco::UInt8& level)
{
    std::cout << "Load level changed to " << static_cast<unsigned>(level) << std::endl;
}

Controlling The Device

Controlling the device is done by simply invoking the Proxy object's member functions.

_pSwitchPower->setTarget(true);
_pDimming->startRampUp();

Timeouts And Persistent Connections

It is possible to control the HTTP timeout for sending control requests, and to enable or disable HTTP/1.1 persistent connections. This is done via the Proxy's Transport object. The Proxy's Transport object can be obtained by calling the remoting__transport() member function, and casting the result to a Poco::UPnP::SOAP::Transport.

static_cast<Poco::UPnP::SOAP::Transport&>(_pSwitchPower->remoting__transport()).setTimeout(Poco::Timespan(10, 0));

Parsing UPnP XML Service Descriptions

For some control points it may be necessary to process the UPnP service descriptions provided by the device in order to find out which actions are actually implemented by the device. Also, limits for certain action arguments may be specified in the service description that may not be otherwise available.

Support for parsing service descriptions is provided by the ServiceDesc library, specifically the Poco::UPnP::ServiceDesc::ServiceCollection class. In order to parse a service description, a ServiceCollection object must be created for each device. The Poco::UPnP::ServiceDesc::ServiceCollection::loadService() method can then be used to download the service description from the device and process it.

In our sample, downloading and parsing the service description documents can be done in the fetchDeviceDescription() method. We need to extract the SCPDURL for each service from the device description, resolve it against the base URI and pass it to Poco::UPnP::ServiceDesc::ServiceCollection::loadService().

void fetchDeviceDescription(const std::string& location)
{
    Poco::URI uri(location);
    Poco::SharedPtr<std::istream> pStream = Poco::URIStreamOpener::defaultOpener().open(uri);
    Poco::AutoPtr<Poco::Util::XMLConfiguration> pDeviceDescription = new Poco::Util::XMLConfiguration(*pStream);
    Poco::URI baseURL(pDeviceDescription->getString("URLBase", location));
    int i = 0;
    while (pDeviceDescription->hasProperty(Poco::format("device.serviceList.service[%d].serviceType", i)))
    {
        std::string serviceType = pDeviceDescription->getString(Poco::format("device.serviceList.service[%d].serviceType", i));
        std::string serviceId   = pDeviceDescription->getString(Poco::format("device.serviceList.service[%d].serviceId", i));
        std::string controlURL  = pDeviceDescription->getString(Poco::format("device.serviceList.service[%d].controlURL", i));
        std::string eventSubURL = pDeviceDescription->getString(Poco::format("device.serviceList.service[%d].eventSubURL", i), "");
        std::string scpdURL     = pDeviceDescription->getString(Poco::format("device.serviceList.service[%d].SCPDURL", i));

        if (serviceType == "urn:schemas-upnp-org:service:SwitchPower:1")
        {
            if (!_pSwitchPower)
            {
                Poco::URI switchPowerControlURL(baseURL, controlURL);
                _pSwitchPower = UPnPS::LightingControls1::SwitchPowerClientHelper::find(switchPowerControlURL.toString());
                Poco::URI switchPowerEventSubURL(baseURL, eventSubURL);
                _pSwitchPower.cast<UPnPS::LightingControls1::SwitchPowerProxy>()->remoting__setEventURI(switchPowerEventSubURL);
                Poco::URI switchPowerSCPDURL(baseURL, scpdURL);
                _services.loadService(serviceId, serviceType, switchPowerSCPDURL);
            }
        }
        else if (serviceType == "urn:schemas-upnp-org:service:Dimming:1")
        {
            if (!_pDimming)
            {
                Poco::URI dimmingControlURL(baseURL, controlURL);
                _pDimming = UPnPS::LightingControls1::DimmingClientHelper::find(dimmingControlURL.toString());
                Poco::URI dimmingEventSubURL(baseURL, eventSubURL);
                _pDimming.cast<UPnPS::LightingControls1::DimmingProxy>()->remoting__setEventURI(dimmingEventSubURL);
                Poco::URI dimmingSCPDURL(baseURL, scpdURL);
                _services.loadService(serviceId, serviceType, dimmingSCPDURL);
            }
        }
        i++;
    }
}

The ServiceCollection object can then be used to obtain Poco::UPnP::ServiceDesc::Service objects for all loaded services. Supported actions and state variables can be inquired of the Service object.

void listServices()
{
    Poco::UPnP::ServiceDesc::ServiceCollection::ServiceInfos infos = _services.services();
    for (Poco::UPnP::ServiceDesc::ServiceCollection::ServiceInfos::const_iterator it = infos.begin(); it != infos.end(); ++it)
    {
        std::cout << "ServiceId:   " << it->first << std::endl;
        std::cout << "ServiceType: " << it->second->serviceType << std::endl;
        std::cout << "Actions:" << std::endl;
        const Poco::UPnP::ServiceDesc::Service::Actions& actions = it->second->pService->actions();
        for (Poco::UPnP::ServiceDesc::Service::Actions::const_iterator it = actions.begin(); it != actions.end(); ++it)
        {
            std::cout << "\t" << (*it)->name() << std::endl;
        }
    }
}

Putting It All Together

Following is the complete source code for the NetworkLightController sample used in this document.

#include "Poco/Util/Application.h"
#include "Poco/Util/Timer.h"
#include "Poco/Util/TimerTask.h"
#include "Poco/Util/XMLConfiguration.h"
#include "Poco/UPnP/SSDP/SSDPResponder.h"
#include "Poco/UPnP/SSDP/Advertisement.h"
#include "Poco/UPnP/SOAP/TransportFactory.h"
#include "Poco/UPnP/GENA/Listener.h"
#include "Poco/UPnP/GENA/RequestHandler.h"
#include "Poco/UPnP/ServiceDesc/ServiceCollection.h"
#include "Poco/UPnP/URN.h"
#include "Poco/Net/MulticastSocket.h"
#include "Poco/Net/HTTPServer.h"
#include "Poco/Net/HTTPRequestHandler.h"
#include "Poco/Net/HTTPRequestHandlerFactory.h"
#include "Poco/Net/HTTPServerRequest.h"
#include "Poco/Net/HTTPStreamFactory.h"
#include "Poco/URI.h"
#include "Poco/URIStreamOpener.h"
#include "Poco/Delegate.h"
#include "Poco/AutoPtr.h"
#include "Poco/Format.h"
#include "Poco/Mutex.h"
#include "Poco/Event.h"
#include "UPnPS/LightingControls1/SwitchPowerProxy.h"
#include "UPnPS/LightingControls1/SwitchPowerClientHelper.h"
#include "UPnPS/LightingControls1/DimmingProxy.h"
#include "UPnPS/LightingControls1/DimmingClientHelper.h"
#include <iostream>


using Poco::Util::Application;
using Poco::UPnP::SSDP::SSDPResponder;
using Poco::UPnP::SSDP::Advertisement;


class RequestHandlerFactory: public Poco::Net::HTTPRequestHandlerFactory
{
public:
    RequestHandlerFactory(Poco::UPnP::GENA::Listener& genaListener):
        _genaListener(genaListener),
        _logger(Poco::Logger::get("RequestHandlerFactory"))
    {
    }

    Poco::Net::HTTPRequestHandler* createRequestHandler(const Poco::Net::HTTPServerRequest& request)
    {
        const std::string& path = request.getURI();
        const std::string& method = request.getMethod();
        if (_logger.information())
        {
            _logger.information(method + " " + path + " (" + request.clientAddress().toString() + ")");
        }

        if (method == "NOTIFY")
        {
            return new Poco::UPnP::GENA::RequestHandler(_genaListener);
        }
        else return 0;
    }

private:
    Poco::UPnP::GENA::Listener& _genaListener;
    Poco::Logger& _logger;
};


class NetworkLightController: public Application
{
public:
    NetworkLightController()
    {
    }

    ~NetworkLightController()
    {
    }

protected:
    class FetchDeviceDescriptionTask: public Poco::Util::TimerTask
    {
    public:
        FetchDeviceDescriptionTask(NetworkLightController& nlc, const std::string& location):
            _nlc(nlc),
            _location(location)
        {
        }

        void run()
        {
            _nlc.fetchDeviceDescription(_location);
        }

    private:
        NetworkLightController& _nlc;
        std::string _location;
    };

    void fetchDeviceDescription(const std::string& location)
    {
        logger().information("Retrieving device description...");

        Poco::URI uri(location);
        Poco::SharedPtr<std::istream> pStream = Poco::URIStreamOpener::defaultOpener().open(uri);
        Poco::AutoPtr<Poco::Util::XMLConfiguration> pDeviceDescription = new Poco::Util::XMLConfiguration(*pStream);

        Poco::FastMutex::ScopedLock lock(_mutex);

        Poco::URI baseURL(pDeviceDescription->getString("URLBase", location));
        int i = 0;
        while (pDeviceDescription->hasProperty(Poco::format("device.serviceList.service[%d].serviceType", i)))
        {
            std::string serviceType = pDeviceDescription->getString(Poco::format("device.serviceList.service[%d].serviceType", i));
            std::string serviceId   = pDeviceDescription->getString(Poco::format("device.serviceList.service[%d].serviceId", i));
            std::string controlURL  = pDeviceDescription->getString(Poco::format("device.serviceList.service[%d].controlURL", i));
            std::string eventSubURL = pDeviceDescription->getString(Poco::format("device.serviceList.service[%d].eventSubURL", i), "");
            std::string scpdURL     = pDeviceDescription->getString(Poco::format("device.serviceList.service[%d].SCPDURL", i));

            logger().information(Poco::format("Found: %s.", serviceType));              
            if (serviceType == "urn:schemas-upnp-org:service:SwitchPower:1")
            {
                if (!_pSwitchPower)
                {
                    Poco::URI switchPowerControlURL(baseURL, controlURL);
                    _pSwitchPower = UPnPS::LightingControls1::SwitchPowerClientHelper::find(switchPowerControlURL.toString());
                    Poco::URI switchPowerEventSubURL(baseURL, eventSubURL);
                    _pSwitchPower.cast<UPnPS::LightingControls1::SwitchPowerProxy>()->remoting__setEventURI(switchPowerEventSubURL);
                    _switchPowerFound.set();
                    Poco::URI switchPowerSCPDURL(baseURL, scpdURL);
                    _services.loadService(serviceId, serviceType, switchPowerSCPDURL);
                }
            }
            else if (serviceType == "urn:schemas-upnp-org:service:Dimming:1")
            {
                if (!_pDimming)
                {
                    Poco::URI dimmingControlURL(baseURL, controlURL);
                    _pDimming = UPnPS::LightingControls1::DimmingClientHelper::find(dimmingControlURL.toString());
                    Poco::URI dimmingEventSubURL(baseURL, eventSubURL);
                    _pDimming.cast<UPnPS::LightingControls1::DimmingProxy>()->remoting__setEventURI(dimmingEventSubURL);
                    _dimmingFound.set();
                    Poco::URI dimmingSCPDURL(baseURL, scpdURL);
                    _services.loadService(serviceId, serviceType, dimmingSCPDURL);
                }
            }
            i++;
        }
    }

    void onAdvertisementSeen(Advertisement::Ptr& pAdvertisement)
    {
        if (pAdvertisement->type() == Advertisement::AD_DEVICE_TYPE)
        {
            if (pAdvertisement->notificationType() == "urn:schemas-upnp-org:device:DimmableLight:1")
            {
                std::string location = pAdvertisement->location();
                logger().information(Poco::format("Found DimmableLight device at %s.", location));

                Poco::FastMutex::ScopedLock lock(_mutex);
                if (!_pSwitchPower || !_pDimming)
                {
                    _timer.schedule(new FetchDeviceDescriptionTask(*this, location), Poco::Timestamp());
                }
            }
        }
    }

    void onStatusChanged(const bool& status)
    {
        logger().information(Poco::format("Status changed to %s.", std::string(status ? "ON" : "OFF")));
    }

    void onLoadLevelStatusChanged(const Poco::UInt8& level)
    {
        logger().information(Poco::format("Load level changed to %u.", static_cast<unsigned>(level)));
    }

    Poco::Net::NetworkInterface findActiveNetworkInterface()
    {
        Poco::Net::NetworkInterface::NetworkInterfaceList ifs = Poco::Net::NetworkInterface::list();
        for (Poco::Net::NetworkInterface::NetworkInterfaceList::iterator it = ifs.begin(); it != ifs.end(); ++it)
        {
            if (!it->address().isWildcard() && !it->address().isLoopback() && it->supportsIPv4()) return *it;
        }
        throw Poco::IOException("No configured Ethernet interface found");
    }

    void listServices()
    {
        Poco::UPnP::ServiceDesc::ServiceCollection::ServiceInfos infos = _services.services();
        for (Poco::UPnP::ServiceDesc::ServiceCollection::ServiceInfos::const_iterator it = infos.begin(); it != infos.end(); ++it)
        {
            std::cout << "ServiceId:   " << it->first << std::endl;
            std::cout << "ServiceType: " << it->second->serviceType << std::endl;
            std::cout << "Actions:" << std::endl;
            const Poco::UPnP::ServiceDesc::Service::Actions& actions = it->second->pService->actions();
            for (Poco::UPnP::ServiceDesc::Service::Actions::const_iterator it = actions.begin(); it != actions.end(); ++it)
            {
                std::cout << "\t" << (*it)->name() << std::endl;
            }
        }
    }

    void printHelp()
    {
        std::cout 
            << "The following commands are available:\n" 
            << "\t 0: Turn the light OFF.\n"
            << "\t 1: Turn the light ON.\n"
            << "\t +: Step up brightness.\n"
            << "\t -: Step down brightness.\n"
            << "\t >: Ramp up brightness.\n"
            << "\t <: Ramp down brightness.\n"
            << "\t S: List available services.\n"
            << "\t Q: Quit." << std::endl;
    }

    int main(const std::vector<std::string>& args)
    {
        // Register SOAP transport
        Poco::UPnP::SOAP::TransportFactory::registerFactory();

        // Register HTTPStreamFactory
        Poco::Net::HTTPStreamFactory::registerFactory();

        // Find suitable multicast interface
        Poco::Net::NetworkInterface mcastInterface(findActiveNetworkInterface());
        Poco::Net::IPAddress ipAddress(mcastInterface.address());
        logger().information(Poco::format("Using multicast network interface %s (%s).", mcastInterface.name(), ipAddress.toString()));

        // Set up multicast socket and SSDPResponder
        Poco::Net::SocketAddress sa(Poco::Net::IPAddress(), SSDPResponder::MULTICAST_PORT);
        Poco::Net::MulticastSocket socket(sa);
        socket.setTimeToLive(4);
        socket.setInterface(mcastInterface);
        SSDPResponder ssdpResponder(_timer, socket);
        ssdpResponder.advertisementSeen += Poco::delegate(this, &NetworkLightController::onAdvertisementSeen);
        socket.joinGroup(ssdpResponder.groupAddress().host(), mcastInterface);
        ssdpResponder.start();

        // Search for DimmableLight devices
        ssdpResponder.search("urn:schemas-upnp-org:device:DimmableLight:1");

        // Set up HTTP server for GENA and GENA Listener
        Poco::Net::ServerSocket httpSocket(Poco::Net::SocketAddress(ipAddress, 0));
        Poco::Net::SocketAddress httpSA(ipAddress, httpSocket.address().port());
        Poco::UPnP::GENA::Listener::Ptr pGENAListener = new Poco::UPnP::GENA::Listener(httpSA.toString(), _timer);
        Poco::Net::HTTPServer httpServer(new RequestHandlerFactory(*pGENAListener), httpSocket, new Poco::Net::HTTPServerParams);
        httpServer.start();     
        Poco::RemotingNG::ORB::instance().registerListener(pGENAListener);

        // Wait until services have been found
        logger().information("Waiting for services...");
        _switchPowerFound.wait();
        _dimmingFound.wait();

        // Subscribe to events
        logger().information("Subscribing for events...");
        _pSwitchPower->statusChanged += Poco::delegate(this, &NetworkLightController::onStatusChanged);
        _pSwitchPower->remoting__enableEvents(pGENAListener);
        _pDimming->loadLevelStatusChanged += Poco::delegate(this, &NetworkLightController::onLoadLevelStatusChanged);
        _pDimming->remoting__enableEvents(pGENAListener);

        bool status = false;
        _pSwitchPower->getStatus(status);
        logger().information(std::string("The light is ") + (status ? "ON" : "OFF") + ".");

        bool quit = false;
        while (!quit)
        {
            std::cout << "Enter command (0, 1, +, -, >, <, S, Q, H for Help): " << std::flush;
            char cmd;
            std::cin >> cmd;
            try
            {
                switch (cmd)
                {
                case '0':
                    _pSwitchPower->setTarget(false);
                    break;
                case '1':
                    _pSwitchPower->setTarget(true);
                    break;
                case '+':
                    _pDimming->stepUp();
                    break;
                case '-':
                    _pDimming->stepDown();
                    break;
                case '>':
                    _pDimming->startRampUp();
                    break;
                case '<':
                    _pDimming->startRampDown();
                    break;
                case 'h':
                case 'H':
                    printHelp();
                    break;
                case 's':
                case 'S':
                    listServices();
                    break;
                case 'q':
                case 'Q':
                    quit = true;
                    break;
                default:
                    std::cout << "Unknown command: " << cmd << std::endl;
                    break;
                }
            }
            catch (Poco::Exception& exc)
            {
                logger().log(exc);
            }
        }

        // Shut down 
        _pSwitchPower->statusChanged -= Poco::delegate(this, &NetworkLightController::onStatusChanged);
        _pDimming->loadLevelStatusChanged += Poco::delegate(this, &NetworkLightController::onLoadLevelStatusChanged);
        httpServer.stop();

        ssdpResponder.stop();
        socket.leaveGroup(ssdpResponder.groupAddress().host(), mcastInterface);
        ssdpResponder.advertisementSeen -= Poco::delegate(this, &NetworkLightController::onAdvertisementSeen);

        Poco::RemotingNG::ORB::instance().shutdown();

        return Application::EXIT_OK;
    }

private:
    Poco::Util::Timer _timer;
    std::string _usn;
    UPnPS::LightingControls1::ISwitchPower::Ptr _pSwitchPower;
    UPnPS::LightingControls1::IDimming::Ptr     _pDimming;
    Poco::Event _switchPowerFound;
    Poco::Event _dimmingFound;
    Poco::UPnP::ServiceDesc::ServiceCollection _services;
    Poco::FastMutex _mutex;

    friend class FetchDeviceDescriptionTask;
};


POCO_APP_MAIN(NetworkLightController)