This document describes a Python UPnP control interface based on libupnpp.

libupnpp does a lot of work to translate the data from well-known services to C++ natural data structures. However, the full C++ API has many quirks which would make it complicated to use with Swig and would need quite a lot of additional glue code (tldr: I’m not skilled enough).

So the current Python interface is a simpler and string-based. The module takes care of discovery and SOAP encoding, but the Python program must deal with some of the data decoding (for example, parsing the XML in AVTransport events).

However, the Python module does have support for parsing Content Directory data (XML DIDL format), which is probably the most common need.

As it is, the interface makes it easy to write quick programs to interface with UPnP devices, querying their state, requesting changes, and receiving events.

The Python interface is compatible with libupnpp versions 0.16 and later.

Discovery

There are two parts in the discovery interface: listing the devices on the network, and accessing a service designated by a device and service name.

The latter is probably more commonly useful and will be described first.

Accessing a specific service

This is provided by a single method, findTypedService

import upnpp

service = upnpp.findTypedService(devname, servicename, fuzzy)

devname defines the device and can be provided either as an UUID or as a case-insensitive friendly name.

servicename can be provided either as an exact service string (e.g. urn:schemas-upnp-org:service:AVTransport:1), or, if fuzzy is True as a case-insensitive substring (e.g. avtransport).

The returned value is None if the device/service is not found.

Once a service is returned, you can call its actions.

Listing the network devices

The listDevices() method returns a list of all the devices found in the network. The first call in a given process will incur a delay of approximately 2 seconds, while UPnP discovery completes. Further calls should be more or less instantaneous.

The complete data structures are described in an annex. We shall just give an overview here. In most cases, you will not need to deal programmatically with the service descriptions, you can just look up the arguments for a given action (in the UPnP documents or the service description), and then use findTypedService() and runaction().

import upnpp

descriptions = upnpp.getDevices()

for devdesc in descriptions:
   print("Device: UDN: [%s] fname [%s]" % (dev.UDN, dev.friendlyName))

Each device object contains a list of services. As returned by getDevices() the service objects only contain the basic elements: service type, and access URLs. A full description of the service, with all variables and action definitions, can be obtained by a call to fetchAndParseDesc().

import upnpp

for srvdesc in dev.services:
   print("  service: %s" % srvdesc.serviceType)
   srvdesc.fetchAndParseDesc(dev.URLBase)
   for var in srvdesc.stateTable:
      print("    VARIABLE %s dataType %s" % (var.name, var.dataType))
   for act in srvdesc.actionList:
      print("    ACTION %s args:" % act.name)
      for arg in act.argList:
         print("      ARGUMENT %s" % arg.name)

Where dev is an element of the list returned by getDevices().

The full script can be found in the listdevs.py sample.

The data returned is sufficient to build dynamic calls to the actions, without any prior knowledge.

Actions

Once connected to a service, its runAction() method allows calling one of its actions. You will normally use a simpler wrapper named runaction()

runaction() takes three arguments:

  • The service object.

  • The action name.

  • The action UPnP arguments list (as strings), in the order prescribed by the service definition.

The function returns a dictionary with the action result variables.

Note that you will need to have a look at the action documentation, or at the service XML definition to determine what the expected arguments are.

See the samples/getmedinfo.py sample script for a working example.

import upnpp

# SetAVTRansportURI arguments: instance #, url, metadata (empty here).
retdata = upnpp.runaction(service, "SetAVTransportURI", ["0", url, ""])

# GetMediaInfo arguments: instance #.
retdata = upnpp.runaction(srv, "GetMediaInfo", ["0"])

if retdata:
   for nm, val in retdata.iteritems():
       print("    %s : %s" % (nm, val))
else:
    # Action failed, do something
    pass

Events

The module allows subscribing to a service’s events.

import time
import upnpp

srv = upnpp.findTypedService(friendlyname, fuzzyservicename, True)

class EventReporter:
   def upnp_event(self, nm, value):
      print("%s -> %s" % (nm, value))

reporter = EventReporter()
# You do need to store the result of installReporter
bridge = upnpp.installReporter(srv, reporter)

while True:
   time.sleep(20000)

See the events.py sample.

Unfortunately, the libupnpp C++ service class has no interface suitable for doing this directly from Python, so a bridge class was defined to provide the translation.

You need to define a class with an upnp_event() method which is the user callback, create an instance, and subscribe to events by calling upnpp.installReporter(), which returns an object which you need to store, until you want to unsubscribe from the service events.

Calling installReporter() from an EventReporter method and storing the result in the object has the consequence that the EventReporter object (and the bridge object) will not be automatically deleted because the bridge holds a reference to the user object. If you want to do this, you need to explicitly delete the bridge object for unsubscribing. See the events.py sample for examples of the two approaches and more explanation.

This is quite unnatural, and I’d be glad to take hints from a Swig/Python master on the subject… However, it works.

Data parsers

Content Directory records

UPnP accepts and outputs track metadata in an XML format named DIDL lite.

The Python wrapper gives access to the functions from the cdirobject.hxx libupnpp module, which can translate from the XML format.

The main class is upnpp.UPnPDirContent, which performs the parsing, and has vector members for items and containers entries.

An example follows, taken from the getmedinfo.py sample, accessing the current metadata from a GetMediaInfo command. For this command, if CurrentURIMetaData is set, it is the metadata for the currently playing track, and there will be a single item, from which we extract the title and properties, then the details from the resource entry (which describe the actual format details).

Refer to the comments in the libupnpp libupnpp/control/cdircontent.hxx source file for more details on the data structures, which are just reflected in the Python objects.

Also have a look at the browse.py sample, which performs recursive browsing on a Media Server ContentDirectory service.

import upnpp
srv = upnpp.findTypedService(devname, "avtransport", True)
args = upnpp.VectorString()
retdata = upnpp.MapStringString()
args.append("0")
runaction(srv, "GetMediaInfo", args, retdata)

metadata = retdata["CurrentURIMetaData"]
if metadata:
   print("\nParsed metadata:")

   dirc = upnpp.UPnPDirContent()
   dirc.parse(metadata)

   if dirc.m_items.size():
      dirobj = dirc.m_items[0]
      print("  title: %s "% dirobj.m_title)
      for nm, val in dirobj.m_props.iteritems():
         print("  %s : %s" % (nm, val))

      resources = dirobj.m_resources
      if len(resources):
         print("Resource object details:")
         for nm, val in resources[0].m_props.iteritems():
            print("  %s : %s" % (nm, val))

Building git code and installing

Dependencies:

  • Python development package

  • autotools (autoconf/automake/libtool)

  • libupnpp 0.16.0 or later

  • Swig (at least 2.0).

git clone https://framagit.org/medoc92/libupnpp-bindings.git libupnpp-bindings
cd libupnpp-bindings
sh autogen.sh
configure --prefix=/usr
make
sudo make install

There are a number of small example scripts in the samples/ directory to try things out.

The default build with be for the python command (usually Python 2.x). You can set the PYTHON_VERSION variable when running configure to change this:

configure --prefix=/usr PYTHON_VERSION=3

Annex: full description of the listDevices data

Device description object

The elements of the device list returned by listDevices() have the following attributes:

deviceType

Device Type: e.g. urn:schemas-upnp-org:device:MediaServer:1

friendlyName

User-configurable name (usually), e.g. Lounge-streamer

UDN

Unique Device Number. This is the same as the deviceID in the discovery message. e.g. uuid:a7bdcd12-e6c1-4c7e-b588-3bbc959eda8d

URLBase

Base for all relative URLs. e.g. http://192.168.4.4:49152/

manufacturer

Manufacturer: e.g. D-Link, PacketVideo

modelName

Model name: e.g. MediaTomb, DNS-327L XMLText::Raw description text

services

list of the device services.

Service description object

Each member of the device service list has the following attributes:

serviceType

Service Type e.g. urn:schemas-upnp-org:service:ConnectionManager:1.

serviceId

Service Id inside device: e.g. urn:upnp-org:serviceId:ConnectionManager.

SCPDURL

Service description URL.

controlURL

Service control URL.

eventSubURL

Service event URL.

After calling the fetchAndParseDesc() method, the two additional members are populated:

actionList

the service actions.

stateTable

the service variables.

Variables

Attributes for the elements of the state variables lists:

name

the variable name.

sendEvents

True if changes to the value cause events.

dataType

the variable data type (e.g. int, string, ui4, see the UPnP docs).

hasValueRange

True if the possible values defined by a value range (see further).

minimum

if hasValueRange is True: the minimum value.

maximum

if hasValueRange is True: the maximum value.

step

if hasValueRange is True: the step for values between min and max.

Actions

Attributes for the elements of the actions lists:

name

the action name.

argList

the action arguments list.

Arguments

Attributes for the elements of the arguments lists:

name

the argument name.

todevice

True if data goes to the device, False if it is returned by the call to the action.

relatedVariable

the name of a state variable which defines the argument type.