upnpclient

repository·develop·Indexed 42 days ago

https://github.com/flyte/upnpclient

A Python 3 library for discovering and interacting with uPnP (Universal Plug and Play) devices and their services. It provides tools for SSDP discovery, SOAP action execution, and XML parsing of device descriptions, supporting Python 3.9+.

Tokens
7.3K
Snippets
26
Records
35
Agent score
53%

What's inside upnpclient

  1. Available upnpclient usage examples

    develop

    The following example scripts demonstrate different ways to interact with UPnP devices:

    • discover.py: Discovers UPnP devices on the network and prints device information.
    • server.py: Demonstrates how to connect directly to a specific UPnP device without performing a discovery step.
    • action_call.py: Demonstrates how to find and invoke a specific action on a UPnP device.
    • upnp_dump.py: Discovers all UPnP devices on the network and performs a full dump of their available actions, including parameter details.
  2. Access services and actions on a device

    develop

    A Device object exposes its services via the .services attribute.

    Services are typically accessed as attributes on the Device object using the last part of their service_id. For example, a service with ID urn:upnp-org:serviceId:WANIPConn1 is accessed via d.WANIPConn1.

    If the service name is not a valid Python attribute, use dictionary-style access: d["service_name"].

    # List all services
    print(d.services)
    
    # Access actions via attribute (using the last part of the service_id)
    print(d.WANIPConn1.actions)
    
    # Access service via dictionary-style if attribute access fails
    service = d["WANIPConn1"]
  3. Understand UPnP device behavior and common pitfalls

    develop

    When working with real-world UPnP devices, be aware of the following common behaviors and inconsistencies:

    • Malformed XML: Many devices (e.g., Marantz/DENON) serve XML with leading spaces in URN namespaces or invalid namespace prefixes. The library uses lxml with recover=True to mitigate this.
    • Missing SCPD Files: Devices like Chromecast and Roku may respond to SSDP discovery but return 404 errors when attempting to fetch their SCPD (Service Control Protocol Description) files.
    • Duplicate SSDP Responses: A single device may advertise multiple service types. The discover() method handles this by deduplicating responses based on the location URL.
    • Discovery Timeouts: Some devices (e.g., crashed Sonos speakers) might respond to SSDP discovery but fail to respond to subsequent HTTP requests for their description XML. This can cause the discover() process to take significantly longer than the initial SSDP timeout.
    • Inconsistent Service Names: There is no single canonical name for WAN connection services; they appear as WANIPConn1, WANIPConnection, or WANPPPConnection.1 depending on the router.
    • Service ID Mapping: The service_id follows the format urn:<domain>:serviceId:<name>. The name part (the string following the final colon) is used for attribute-style access on Device objects.
  4. Codebase conventions and requirements

    develop

    If you are contributing to or extending the upnpclient codebase, follow these technical constraints:

    • Python Version: Minimum requirement is Python 3.9. Use datetime.timezone.utc for UTC timestamps (do not use datetime.UTC).
    • XML Parsing: The project relies on lxml. The global XML_PARSER in upnp.py is configured with recover=True to handle malformed device descriptions.
    • Networking:
      • Use urllib.parse directly; do not use requests.compat.
      • SOAP calls are performed using raw requests.post() calls rather than a requests.Session.
    • Package Contents: The gui/, doc/, and examples/ directories are excluded from the published sdist and wheel packages.
  5. Discover uPnP devices

    develop

    Use upnpclient.discover() to find available uPnP devices on your network. This returns a list of Device objects.

    Alternatively, if you already have the URL for a device's description XML, you can instantiate a Device object directly.

    import upnpclient
    
    # Discover devices on the network
    devices = upnpclient.discover()
    
    # Create a device directly from its XML description URL
    d = upnpclient.Device("http://192.168.1.1:5000/rootDesc.xml")
  6. Run upnpclient examples using run.sh

    develop

    The repository includes a run.sh script to execute the provided examples. Note that these examples depend on physical UPnP devices being present on your network and may not work in all environments.

    To run a specific example, pass the script name as an argument to ./run.sh.

    $ ./run.sh discover.py
  7. How to discover and interact with UPnP servers

    develop

    The upnpclient library follows a hierarchical model to interact with UPnP devices:

    1. Discovery: Use upnpclient.discover() (via SSDP) to find Device instances on the network. Alternatively, if you have the XML URL, you can instantiate a Device directly.
    2. Hierarchy: A Device contains multiple Service instances. Each Service contains multiple Action instances.
    3. Inspection: You can inspect Service capabilities and Action arguments (both input argsdef_in and output argsdef_out) to understand what a device can do.
    4. Execution: Use Action.call(arguments) to execute a command on the device via SOAP. The library handles argument verification and type conversion.
    import upnpclient
    
    # 1. Discover servers
    ssdp = upnpclient.SSDP()
    serevers = ssdp.discover()
    
    # 2. Traverse hierarchy
    for server in servers:
        for service in server.services:
            for action in service.actions:
                # 3. Call an action
                result = action.call(arg1='value', arg2=123)
  8. Inspect action arguments and return values

    develop

    You can inspect the expected input arguments and the output structure of an action using argsdef_in and argsdef_out.

    • action.argsdef_in: A list of tuples containing (argument_name, metadata_dict). Metadata includes datatype, name, and allowed_values.
    • action.argsdef_out: A list of tuples containing (result_name, metadata_dict) describing the returned data.
    # Inspect expected input arguments
    print(d.WANIPConn1.AddPortMapping.argsdef_in)
    
    # Inspect expected output structure
    print(d.WANIPConn1.GetStatusInfo.argsdef_out)
    
    # Call the action with arguments
    d.WANIPConn1.AddPortMapping(
        NewRemoteHost='0.0.0.0',
        NewExternalPort=12345,
        NewProtocol='TCP',
        NewInternalPort=12345,
        NewInternalClient='192.168.1.10',
        NewEnabled='1',
        NewPortMappingDescription='Testing',
        NewLeaseDuration=10000
    )
  9. Configure HTTP authentication and headers

    develop

    You can provide requests-compatible authentication or custom HTTP headers for communication with the uPnP device.

    1. At Device creation: Set http_auth and http_headers when instantiating upnpclient.Device. These will be used for all subsequent calls.
    2. Per-call: Pass http_auth and http_headers directly to an action method to override device-level settings.
    3. Resetting: To ignore device-level settings for a specific call, pass None to the authentication or header parameters.
    # Set auth and headers for the entire device
    device = upnpclient.Device(
        "http://192.168.1.1:5000/rootDesc.xml",
        http_auth=('myusername', 'mypassword'),
        http_headers={'Some-Required-Header': 'somevalue'},
    )
    
    # Override auth/headers for a specific action call
    device.Layer3Forwarding1.GetDefaultConnectionService(
        http_auth=('user', 'pass'),
        http_headers={'X-Custom': 'value'}
    )
  10. Call actions on a uPnP service

    develop

    Once you have accessed a service, you can call its actions as methods. Actions return a dictionary containing the results.

    # Call an action and get the result dictionary
    status = d.WANIPConn1.GetStatusInfo()
    print(status)
    # Output example: {'NewConnectionStatus': 'Connected', ...}