StevenLooman / async_upnp_client

Async UPnP Client for Python
Other
46 stars 37 forks source link
hacktoberfest

Async UPnP Client

Asyncio UPnP Client library for Python/asyncio.

Written initially for use in Home Assistant <https://github.com/home-assistant/home-assistant>_ to drive DLNA DMR-capable devices, but useful for other projects as well.

Status

.. image:: https://github.com/StevenLooman/async_upnp_client/workflows/Build/badge.svg :target: https://github.com/StevenLooman/async_upnp_client/actions/workflows/ci-cd.yml

.. image:: https://img.shields.io/pypi/v/async_upnp_client.svg :target: https://pypi.python.org/pypi/async_upnp_client

.. image:: https://img.shields.io/pypi/format/async_upnp_client.svg :target: https://pypi.python.org/pypi/async_upnp_client

.. image:: https://img.shields.io/pypi/pyversions/async_upnp_client.svg :target: https://pypi.python.org/pypi/async_upnp_client

.. image:: https://img.shields.io/pypi/l/async_upnp_client.svg :target: https://pypi.python.org/pypi/async_upnp_client

General set up

The UPnP Device Architecture <https://openconnectivity.org/upnp-specs/UPnP-arch-DeviceArchitecture-v2.0-20200417.pdf>_ document contains several sections describing different parts of the UPnP standard. These chapters/sections can mostly be mapped to the following modules:

There are several 'profiles' which a device can implement to provide a standard interface to talk to. Some of these profiles are added to this library. The following profiles are currently available:

For examples on how to use async_upnp_client, see examples/ .

Note that this library is most likely does not fully implement all functionality from the UPnP Device Architecture document and/or contains errors/bugs/mis-interpretations.

Contributing

See CONTRIBUTING.rst.

Development

Development is done on the development branch.

pre-commit is used to run several checks before committing. You can install pre-commit and the git-hook by doing::

$ pip install pre-commit
$ pre-commit --install

The Open Connectivity Foundation <https://openconnectivity.org/> provides a bundle with all UPnP Specifications <https://openconnectivity.org/developer/specifications/upnp-resources/upnp/>.

Changes

Changes are recorded using Towncier <https://towncrier.readthedocs.io/>_. Once a new release is created, towncrier is used to create the file CHANGES.rst.

To create a new change run:

$ towncrier create <pr-number>.<change type>

A change type can be one of:

A new file is then created in the changes directory. Add a short description of the change to that file.

Releasing

Steps for releasing:

Profiling

To do profiling it is recommended to install pytest-profiling <https://pypi.org/project/pytest-profiling>_. Then run a test with profiling enabled, and write the results to a graph::

# Run tests with profiling and svg-output enabled. This will generate prof/*.prof files, and a svg file.
$ pytest --profile-svg -k test_case_insensitive_dict_profile
...

# Open generated SVG file.
$ xdg-open prof/combined.svg

Alternatively, you can generate a profiling data file, use pyprof2calltree <https://github.com/pwaller/pyprof2calltree/> to convert the data and open kcachegrind <http://kcachegrind.sourceforge.net/html/Home.html>. For example::

# Run tests with profiling enabled, this will generate prof/*.prof files.
$ pytest --profile -k test_case_insensitive_dict_profile
...

$ pyprof2calltree -i prof/combined.prof -k
launching kcachegrind

upnp-client

A command line interface is provided via the upnp-client script. This script can be used to:

The output of the script is a single line of JSON for each action-call or subscription-event. See the programs help for more information.

An example of calling an action::

$ upnp-client --pprint call-action http://192.168.178.10:49152/description.xml RC/GetVolume InstanceID=0 Channel=Master
{
    "timestamp": 1531482271.5603056,
    "service_id": "urn:upnp-org:serviceId:RenderingControl",
    "service_type": "urn:schemas-upnp-org:service:RenderingControl:1",
    "action": "GetVolume",
    "in_parameters": {
        "InstanceID": 0,
        "Channel": "Master"
    },
    "out_parameters": {
        "CurrentVolume": 70
    }
}

An example of subscribing to all services, note that the program stays running until you stop it (ctrl-c)::

$ upnp-client --pprint subscribe http://192.168.178.10:49152/description.xml \*
{
    "timestamp": 1531482518.3663802,
    "service_id": "urn:upnp-org:serviceId:RenderingControl",
    "service_type": "urn:schemas-upnp-org:service:RenderingControl:1",
    "state_variables": {
        "LastChange": "<Event xmlns=\"urn:schemas-upnp-org:metadata-1-0/AVT_RCS\">\n<InstanceID val=\"0\">\n<Mute channel=\"Master\" val=\"0\"/>\n<Volume channel=\"Master\" val=\"70\"/>\n</InstanceID>\n</Event>\n"
    }
}
{
    "timestamp": 1531482518.366804,
    "service_id": "urn:upnp-org:serviceId:RenderingControl",
    "service_type": "urn:schemas-upnp-org:service:RenderingControl:1",
    "state_variables": {
        "Mute": false,
        "Volume": 70
    }
}
...

You can subscribe to list of services by providing these names or abbreviated names, such as::

$ upnp-client --pprint subscribe http://192.168.178.10:49152/description.xml RC AVTransport

An example of searching for devices::

$ upnp-client --pprint search
{
    "Cache-Control": "max-age=3600",
    "Date": "Sat, 27 Oct 2018 10:43:42 GMT",
    "EXT": "",
    "Location": "http://192.168.178.1:49152/description.xml",
    "OPT": "\"http://schemas.upnp.org/upnp/1/0/\"; ns=01",
    "01-NLS": "906ad736-cfc4-11e8-9c22-8bb67c653324",
    "Server": "Linux/4.14.26+, UPnP/1.0, Portable SDK for UPnP devices/1.6.20.jfd5",
    "X-User-Agent": "redsonic",
    "ST": "upnp:rootdevice",
    "USN": "uuid:e3a17dd5-9d85-3131-3c34-b827eb498d72::upnp:rootdevice",
    "_timestamp": "2018-10-27 12:43:09.125408",
    "_host": "192.168.178.1",
    "_port": 49152
    "_udn": "uuid:e3a17dd5-9d85-3131-3c34-b827eb498d72",
    "_source": "search"
}

An example of listening for advertisements, note that the program stays running until you stop it (ctrl-c)::

$ upnp-client --pprint advertisements
{
    "Host": "239.255.255.250:1900",
    "Cache-Control": "max-age=30",
    "Location": "http://192.168.178.1:1900/WFADevice.xml",
    "NTS": "ssdp:alive",
    "Server": "POSIX, UPnP/1.0 UPnP Stack/2013.4.3.0",
    "NT": "urn:schemas-wifialliance-org:device:WFADevice:1",
    "USN": "uuid:99cb221c-1f15-c620-dc29-395f415623c6::urn:schemas-wifialliance-org:device:WFADevice:1",
    "_timestamp": "2018-12-23 11:22:47.154293",
    "_host": "192.168.178.1",
    "_port": 1900
    "_udn": "uuid:99cb221c-1f15-c620-dc29-395f415623c6",
    "_source": "advertisement"
}

IPv6 support

IPv6 is supported for the UPnP client functionality as well as the SSDP functionality. Please do note that multicast over IPv6 does require a scope_id/interface ID. The scope_id is used to specify which interface should be used.

There are several ways to get the scope_id. Via Python this can be done via the ifaddr <https://github.com/pydron/ifaddr>_ library. From the (Linux) command line the scope_id can be found via the ip command::

$ ip address
...
6: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:15:5d:38:97:cf brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.2/24 brd 192.168.1.255 scope global eth0
        valid_lft forever preferred_lft forever
    inet6 fe80::215:5dff:fe38:97cf/64 scope link
        valid_lft forever preferred_lft forever

In this case, the interface index is 6 (start of the line) and thus the scope_id is 6.

Or on Windows using the ipconfig command::

C:\> ipconfig /all
...
Ethernet adapter Ethernet:
    ...
    Link-local IPv6 Address . . . . . : fe80::e530:c739:24d7:c8c7%8(Preferred)
...

The scope_id is 8 in this example, as shown after the % character at the end of the IPv6 address.

Or on macOS using the ifconfig command::

% ifconfig
...
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
      options=50b<RXCSUM,TXCSUM,VLAN_HWTAGGING,AV,CHANNEL_IO>
      ether 38:c9:86:30:fe:be
      inet6 fe80::215:5dff:fe38:97cf%en0 prefixlen 64 secured scopeid 0x4
...

The scope_id is 4 in this example, as shown by scopeid 0x4. Note that this is a hexadecimal value.

Be aware that Python <3.9 does not support the IPv6Address.scope_id attribute. As such, a AddressTupleVXType is used to specify the source- and target-addresses. In case of IPv4, AddressTupleV4Type is a 2-tuple with address, port. AddressTupleV6Type is used for IPv6 and is a 4-tuple with address, port, flowinfo, scope_id. More information can be found in the Python socket module documentation.

All functionality regarding SSDP uses AddressTupleVXType the specify addresses.

For consistency, the AiohttpNotifyServer also uses a tuple the specify the source (the address and port the notify server listens on.)