Overview
This document describes a Python3 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).
The Python3 API is 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). The package provides some Python3 helper methods, e.g. for for parsing
Content Directory data (XML DIDL format), or OpenHome IdArray data.
Most of the API though, is generated with Swig, and, when in doubt on a data member name, for example, looking at the C\++ include files will probably yield a correct answer.
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 package 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 desc in descriptions:
print(f"Device: UDN: [{desc.UDN}] fname [{desc.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
descriptions = upnpp.getDevices()
desc = descriptions[0]
for srv in desc.services:
print(f" service: {srv.serviceType}")
srv.fetchAndParseDesc(desc.URLBase)
for var in srv.stateTable:
print(f" VARIABLE {var.name} dataType {var.dataType}")
for act in srv.actionList:
print(f" ACTION {act.name} args:")
for arg in act.argList:
print(f" ARGUMENT {arg.name}")
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, the runaction() method allows calling one of its actions.
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
devname = "UpMpd-hm2-UPnP/AV"
service = upnpp.findTypedService(devname , "avtransport", True)
# GetMediaInfo argument: instance #.
retdata = upnpp.runaction(service, "GetMediaInfo", ["0"])
if retdata:
for nm, val in retdata.items():
print(f" {nm} : {val}")
else:
# Action failed, do something
pass
Events
The module allows subscribing to a service’s events.
import sys
import time
import upnpp
devname = "UpMpd-hm2"
fuzzyservicename = "playlist"
srv = upnpp.findTypedService(devname, fuzzyservicename, True)
if not srv:
msg("findTypedService failed")
sys.exit(1)
class EventReporter:
def upnp_event(self, nm, value):
print(f"{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
devname = "UpMpd-hm2-UPnP/AV"
srv = upnpp.findTypedService(devname, "avtransport", True)
retdata = upnpp.runaction(srv, "GetMediaInfo", ["0"])
metadata = retdata["CurrentURIMetaData"]
if metadata:
print("\nParsed metadata:")
dirc = upnpp.UPnPDirContent()
dirc.parse(metadata)
for item in dirc.m_items:
print(f" title: {item.m_title}")
for nm, val in item.m_props.items():
print(f" {nm} : {val}")
for resource in item.m_resources:
print(f"Resource URL: {resource.m_uri}")
for nm, val in resource.m_props.items():
print(f" {nm} : {val}")
UPnPDirContent.parse() accumulates data, so listing an UPnP container could be implemented like the following:
def listdir(srv, id):
dirc = upnpp.UPnPDirContent()
offs = 0
cnt = 100
while True:
data = upnpp.runaction(srv, "Browse", [id, "BrowseChildren", "", f"{offs}", f"{cnt}", ""])
cnt = int(data["NumberReturned"])
if cnt <= 0:
break
dirc.parse(data["Result"])
offs += cnt
return dirc
OpenHome IdArray data
These are provided as a base64-encoded string representing an array of big-endian 32 bits integers. Recent versions of the package have an idArrayToInts() method for decoding the data. For older versions, this is simply:
import base64
import struct
def idArrayToInts(a):
b = base64.b64decode(a)
format = ">" + "I" * (len(b) // 4)
integers = struct.unpack(format, b)
return integers
Building git code and installing
Dependencies:
-
Python development package
-
meson/ninja
-
libupnpp development files.
-
Swig version 4
Building:
git clone https://framagit.org/medoc92/libupnpp-bindings.git libupnpp-bindings
cd libupnpp-bindings
meson setup -Dprefix=/usr build
cd build
ninja && sudo ninja install
There are a number of small example scripts in the 'samples/' directory to try things out.
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.