### Install upnpclient Source: https://github.com/flyte/upnpclient/blob/develop/README.md Install the library using pip. Requires Python 3.9+. ```bash pip install upnpclient ``` -------------------------------- ### Discover UPnP Devices Source: https://github.com/flyte/upnpclient/blob/develop/README.md Discover UPnP devices on the network and get a list of Device objects. This is the primary method for finding available devices. ```python import upnpclient devices = upnpclient.discover() print(devices) ``` -------------------------------- ### UPnP Error Handling Hierarchy Source: https://context7.com/flyte/upnpclient/llms.txt Illustrates the exception hierarchy raised by the library, including UPNPError, ValidationError, SOAPError, and InvalidActionException, with examples for each. ```APIDOC ## Error handling — `UPNPError`, `ValidationError`, `SOAPError`, `InvalidActionException`, `UnexpectedResponse` The library raises a clear exception hierarchy. `UPNPError` is the base class. `ValidationError` carries a `reasons` dict keyed by argument name. `SOAPError` is raised when the UPnP device returns a SOAP fault and carries `(error_code, error_description)` from the device. ```python import upnpclient from upnpclient import UPNPError, ValidationError, InvalidActionException from upnpclient.soap import SOAPError d = upnpclient.Device("http://192.168.1.1:5000/rootDesc.xml") # 1. Missing required argument → UPNPError try: d.WANIPConn1.AddPortMapping(NewRemoteHost="0.0.0.0") except UPNPError as e: print("UPNPError:", e) # UPNPError: Missing required param 'NewExternalPort' # 2. Type/range/allowed-value violation → ValidationError try: d.WANIPConn1.AddPortMapping( NewRemoteHost="0.0.0.0", NewExternalPort=70000, # exceeds ui2 max NewProtocol="ICMP", # not in allowed values NewInternalPort=8080, NewInternalClient="192.168.1.50", NewEnabled="1", NewPortMappingDescription="Test", NewLeaseDuration=3600, ) except ValidationError as e: for arg, reasons in e.reasons.items(): print(f" {arg}: {reasons}") # NewExternalPort: {"'ui2' datatype must be a number in the range 0 to 65535"} # NewProtocol: {"Value 'ICMP' not in allowed values list"} # 3. Device-level error (e.g. conflicting port mapping) → SOAPError try: d.WANIPConn1.GetGenericPortMappingEntry(NewPortMappingIndex=9999) except SOAPError as e: code, description = e.args print(f"SOAP error {code}: {description}") # SOAP error 714: No such entry in array # 4. Action not found → InvalidActionException try: d.WANIPConn1("BogusAction") except InvalidActionException as e: print(e) # Action with name 'BogusAction' does not exist. ``` ``` -------------------------------- ### Handle UPnP Errors with Exception Hierarchy Source: https://context7.com/flyte/upnpclient/llms.txt The library provides a clear exception hierarchy starting from `UPNPError`. Specific exceptions like `ValidationError`, `SOAPError`, and `InvalidActionException` provide detailed error information. Use these to gracefully handle issues such as missing arguments, data violations, device-level faults, or non-existent actions. ```python import upnpclient from upnpclient import UPNPError, ValidationError, InvalidActionException from upnpclient.soap import SOAPError d = upnpclient.Device("http://192.168.1.1:5000/rootDesc.xml") # 1. Missing required argument → UPNPError try: d.WANIPConn1.AddPortMapping(NewRemoteHost="0.0.0.0") except UPNPError as e: print("UPNPError:", e) # UPNPError: Missing required param 'NewExternalPort' # 2. Type/range/allowed-value violation → ValidationError try: d.WANIPConn1.AddPortMapping( NewRemoteHost="0.0.0.0", NewExternalPort=70000, # exceeds ui2 max NewProtocol="ICMP", # not in allowed values NewInternalPort=8080, NewInternalClient="192.168.1.50", NewEnabled="1", NewPortMappingDescription="Test", NewLeaseDuration=3600, ) except ValidationError as e: for arg, reasons in e.reasons.items(): print(f" {arg}: {reasons}") # NewExternalPort: {"'ui2' datatype must be a number in the range 0 to 65535"} # NewProtocol: {"Value 'ICMP' not in allowed values list"} # 3. Device-level error (e.g. conflicting port mapping) → SOAPError try: d.WANIPConn1.GetGenericPortMappingEntry(NewPortMappingIndex=9999) except SOAPError as e: code, description = e.args print(f"SOAP error {code}: {description}") # SOAP error 714: No such entry in array # 4. Action not found → InvalidActionException try: d.WANIPConn1("BogusAction") except InvalidActionException as e: print(e) # Action with name 'BogusAction' does not exist. ``` -------------------------------- ### Listing Services and Actions Source: https://github.com/flyte/upnpclient/blob/develop/README.md This demonstrates how to list the available services on a `Device` object and then list the actions available for a specific service. ```APIDOC ## Listing Services and Actions ### Description Access and list the services and actions exposed by a UPnP device. ### Method ```python # List services print(d.services) # Access a service by its ID (e.g., WANIPConn1) wan_service = d.WANIPConn1 # List actions for the service print(wan_service.actions) ``` ### Response Example ```json { "example": "[, , ]" } ``` ``` -------------------------------- ### Build Project with uv Source: https://github.com/flyte/upnpclient/blob/develop/CLAUDE.md Execute 'uv build' to create the sdist and wheel distributions for the project. ```bash uv build # Build sdist + wheel ``` -------------------------------- ### Create Device Object from URL Source: https://github.com/flyte/upnpclient/blob/develop/README.md Instantiate a Device object directly if the URL of the device's description XML is known. ```python d = upnpclient.Device("http://192.168.1.1:5000/rootDesc.xml") ``` -------------------------------- ### Device and Service Shorthand Invocation Source: https://context7.com/flyte/upnpclient/llms.txt Demonstrates how to call actions directly on Device and Service objects using a shorthand method, including handling invalid actions. ```APIDOC ## `Device.__call__(action_name, **kwargs)` / `Service.__call__(action_name, **kwargs)` — shorthand invocation Both `Device` and `Service` implement `__call__` via `CallActionMixin`, letting you call an action by name in a single expression. Raises `InvalidActionException` if the action does not exist on that object. ```python import upnpclient from upnpclient import InvalidActionException d = upnpclient.Device("http://192.168.1.1:5000/rootDesc.xml") # Call directly on device — searches all services result = d("GetExternalIPAddress") print(result) # {'NewExternalIPAddress': '203.0.113.42'} # Call directly on service — restricted to that service result = d.WANIPConn1("GetStatusInfo") print(result) # {'NewConnectionStatus': 'Connected', 'NewLastConnectionError': 'ERROR_NONE', 'NewUptime': 57600} # Dictionary-style chaining result = d["WANIPConn1"]["GetStatusInfo"]() print(result) # {'NewConnectionStatus': 'Connected', ...} # InvalidActionException when action doesn't exist try: d("NoSuchAction") except InvalidActionException as e: print(e) # Action with name 'NoSuchAction' does not exist. ``` ``` -------------------------------- ### Calling Actions Source: https://github.com/flyte/upnpclient/blob/develop/README.md Shows how to call actions on a service, including actions with and without arguments. ```APIDOC ## Calling Actions ### Description Invoke actions provided by a UPnP service. ### Method ```python # Call an action without arguments status_info = d.WANIPConn1.GetStatusInfo() print(status_info) # Call an action with arguments response = 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 ) print(response) ``` ### Response Example ```json { "example": "{'NewConnectionStatus': 'Connected', 'NewLastConnectionError': 'ERROR_NONE', 'NewUptime': 14851479}" } ``` ``` -------------------------------- ### Service Subscription and Management Source: https://context7.com/flyte/upnpclient/llms.txt Demonstrates how to subscribe to UPnP events, renew subscriptions, and cancel them using the `subscribe`, `renew_subscription`, and `cancel_subscription` methods. ```APIDOC ## `Service.subscribe(callback_url, timeout=None)` / `renew_subscription` / `cancel_subscription` Subscribes to UPnP eventing on a service so the device pushes state-change notifications to your HTTP endpoint. Returns `(sid, timeout_seconds)` where `sid` is the subscription ID used to renew or cancel. Raises `UnexpectedResponse` if the device returns a malformed subscription response. ```python import upnpclient from upnpclient import UnexpectedResponse d = upnpclient.Device("http://192.168.1.1:5000/rootDesc.xml") svc = d.WANIPConn1 # Subscribe — device will POST events to your callback URL try: sid, timeout = svc.subscribe( "http://192.168.1.100:8080/upnp-callback", timeout=1800, # request 30-minute subscription ) print(f"Subscribed: SID={sid}, timeout={timeout}s") # Subscribed: SID=uuid:abcdef12-3456-7890-abcd-ef1234567890, timeout=1800 # Renew before it expires new_timeout = svc.renew_subscription(sid, timeout=1800) print(f"Renewed: new timeout={new_timeout}s") # Cancel when done svc.cancel_subscription(sid) print("Unsubscribed") except UnexpectedResponse as e: print("Device returned an unexpected subscription response:", e) ``` ``` -------------------------------- ### Configure HTTP Authentication and Headers on Device Creation Source: https://github.com/flyte/upnpclient/blob/develop/README.md Set authentication credentials and custom HTTP headers when creating a Device object. These will be used for all subsequent HTTP calls to that device. ```python device = upnpclient.Device( "http://192.168.1.1:5000/rootDesc.xml", http_auth=('myusername', 'mypassword'), http_headers={'Some-Required-Header': 'somevalue'}, ) ``` -------------------------------- ### Lint Code with ruff Source: https://github.com/flyte/upnpclient/blob/develop/CLAUDE.md Run 'uv run ruff check upnpclient' to perform linting on the upnpclient code. ```bash uv run ruff check upnpclient # Lint ``` -------------------------------- ### Creating a Device Object Directly Source: https://github.com/flyte/upnpclient/blob/develop/README.md If the URL for a device's description XML is known, a `Device` object can be created directly. ```APIDOC ## Creating Device Directly ### Description Instantiate a `Device` object when the description XML URL is known. ### Method ```python d = upnpclient.Device("http://192.168.1.1:5000/rootDesc.xml") ``` ``` -------------------------------- ### Connect Directly to a UPnP Device Source: https://context7.com/flyte/upnpclient/llms.txt Establishes a direct connection to a UPnP device by fetching its root description XML from a given URL. This bypasses the discovery process. It supports basic HTTP authentication and custom headers for secured devices. Raises `requests.exceptions.HTTPError` if the URL is unreachable. ```python import upnpclient # Direct connection — no discovery needed d = upnpclient.Device("http://192.168.1.1:5000/rootDesc.xml") print(d) # print(d.friendly_name) # OpenWRT router print(d.manufacturer) # OpenWrt print(d.model_name) # Linux/3.x.x, UPnP/1.0, MiniUPnPd/1.9 print(d.serial_number) # 000000000000 print(d.udn) # uuid:1234abcd-5678-efgh-ijkl-000000000001 # With HTTP Basic Auth (requests-compatible auth object) d_auth = upnpclient.Device( "http://192.168.1.1:5000/rootDesc.xml", http_auth=("admin", "password"), http_headers={"X-Custom-Header": "value"}, ) # Enumerate all services on the device for svc in d.services: print(svc.service_id, "->", svc.service_type) # urn:upnp-org:serviceId:Layer3Forwarding1 -> urn:schemas-upnp-org:service:Layer3Forwarding:1 # urn:upnp-org:serviceId:WANCommonIFC1 -> urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1 # urn:upnp-org:serviceId:WANIPConn1 -> urn:schemas-upnp-org:service:WANIPConnection:1 ``` -------------------------------- ### Inspecting Action Arguments Source: https://github.com/flyte/upnpclient/blob/develop/README.md Provides details on how to inspect the input and output arguments expected by a specific action. ```APIDOC ## Inspecting Action Arguments ### Description Examine the expected input and output arguments for a given action. ### Method ```python # Inspect input arguments input_args = d.WANIPConn1.AddPortMapping.argsdef_in print(input_args) # Inspect output arguments output_args = d.WANIPConn1.GetStatusInfo.argsdef_out print(output_args) ``` ### Response Example ```json { "example": "[('NewRemoteHost', {'allowed_values': set(), 'datatype': 'string', 'name': 'RemoteHost'}), ('NewExternalPort', {'allowed_values': set(), 'datatype': 'ui2', 'name': 'ExternalPort'}), ...]" } ``` ``` -------------------------------- ### Listing All Actions on a Service Source: https://context7.com/flyte/upnpclient/llms.txt Iterate through all available actions for a specific service on a UPnP device and print their names. ```APIDOC ## List all available actions on a service for action in d.WANIPConn1.actions: print(action.name) # GetStatusInfo # GetNATRSIPStatus # GetExternalIPAddress # AddPortMapping # DeletePortMapping # GetGenericPortMappingEntry # GetSpecificPortMappingEntry ``` -------------------------------- ### HTTP Authentication and Headers Source: https://github.com/flyte/upnpclient/blob/develop/README.md Details how to provide HTTP authentication credentials and custom headers for device communication, both at the device level and per-call. ```APIDOC ## HTTP Auth/Headers ### Description Configure HTTP authentication and headers for device interactions. ### Method ```python # Set on Device creation device_with_auth = upnpclient.Device( "http://192.168.1.1:5000/rootDesc.xml", http_auth=('myusername', 'mypassword'), http_headers={'Some-Required-Header': 'somevalue'}, ) # Set per-call (overrides device level) device.Layer3Forwarding1.GetDefaultConnectionService( http_auth=('myusername', 'mypassword'), http_headers={'Some-Required-Header': 'somevalue'}, ) # Override/disable per-call device.Layer3Forwarding1.GetDefaultConnectionService( http_auth=None, http_headers=None, ) ``` ``` -------------------------------- ### Discovering UPnP Devices Source: https://github.com/flyte/upnpclient/blob/develop/README.md This snippet shows how to discover UPnP devices on the network using the `discover` function. It returns a list of `Device` objects. ```APIDOC ## Discovering Devices ### Description Discover UPnP devices on the network. ### Method ```python import upnpclient devices = upnpclient.discover() ``` ### Response Example ```json { "example": "[, , ]" } ``` ``` -------------------------------- ### List all actions across all services Source: https://context7.com/flyte/upnpclient/llms.txt Retrieves a flat list of all actions available on a UPnP device. Useful for quick enumeration or searching actions without manually iterating through services. ```python import upnpclient d = upnpclient.Device("http://192.168.1.1:5000/rootDesc.xml") all_actions = d.actions print(len(all_actions)) # e.g. 14 for action in all_actions: print(f"{action.service.name}.{action.name}") ``` -------------------------------- ### Listing All Actions on a Device Source: https://context7.com/flyte/upnpclient/llms.txt Access a flat list of all actions across all services on a UPnP device for easy enumeration or searching. ```APIDOC ## `Device.actions` — flat list of all actions across all services A convenience property that returns every `Action` from every `Service` on the device as a single flat list. Useful for quick enumeration or searching without iterating services manually. ```python import upnpclient d = upnpclient.Device("http://192.168.1.1:5000/rootDesc.xml") all_actions = d.actions print(len(all_actions)) # e.g. 14 for action in all_actions: print(f"{action.service.name}.{action.name}") # Layer3Forwarding1.SetDefaultConnectionService # Layer3Forwarding1.GetDefaultConnectionService # WANCommonIFC1.GetCommonLinkProperties # WANIPConn1.GetStatusInfo # WANIPConn1.GetExternalIPAddress # WANIPConn1.AddPortMapping # ... ``` ``` -------------------------------- ### Inspect action argument definitions Source: https://context7.com/flyte/upnpclient/llms.txt Examines the input and output argument definitions for a UPnP action. Useful for understanding required arguments, their data types, and allowed values before calling an action. ```python import upnpclient d = upnpclient.Device("http://192.168.1.1:5000/rootDesc.xml") action = d.WANIPConn1.AddPortMapping # Inspect required input arguments for arg_name, statevar in action.argsdef_in: allowed = ", ".join(statevar["allowed_values"]) or "*" print(f" IN {arg_name:35s} ({statevar['datatype']:12s}) allowed={allowed}") ``` -------------------------------- ### List all actions on a service Source: https://context7.com/flyte/upnpclient/llms.txt Iterates through actions of a specific service and prints their names. Useful for understanding the capabilities of a particular service. ```python for action in d.WANIPConn1.actions: print(action.name) ``` -------------------------------- ### Run Tests with uv Source: https://github.com/flyte/upnpclient/blob/develop/CLAUDE.md Use 'uv run pytest' to execute tests. For comprehensive testing across Python versions and linting, use 'uv run --with tox-uv tox'. ```bash uv run pytest # Run tests ``` ```bash uv run --with tox-uv tox # Run tests on Python 3.9-3.13 + lint + coverage ``` -------------------------------- ### Inspecting Action Argument Definitions Source: https://context7.com/flyte/upnpclient/llms.txt Examine the input and output argument definitions for an action to understand expected data types, allowed values, and argument names. ```APIDOC ## `Action(service, url, ...).argsdef_in` / `argsdef_out` These lists describe the input and output argument contracts of an action. Each entry is a tuple of `(arg_name, statevar_dict)` where `statevar_dict` contains `datatype`, `allowed_values` (a set, empty means unrestricted), and `name`. Inspect these before calling an action to determine what to pass and what to expect back. ```python import upnpclient d = upnpclient.Device("http://192.168.1.1:5000/rootDesc.xml") action = d.WANIPConn1.AddPortMapping # Inspect required input arguments for arg_name, statevar in action.argsdef_in: allowed = ", ".join(statevar["allowed_values"]) or "*" print(f" IN {arg_name:35s} ({statevar['datatype']:12s}) allowed={allowed}") # IN NewRemoteHost (string ) allowed=* # IN NewExternalPort (ui2 ) allowed=* # IN NewProtocol (string ) allowed=TCP, UDP # IN NewInternalPort (ui2 ) allowed=* ``` ``` -------------------------------- ### Configure HTTP Authentication and Headers on Per-Call Basis Source: https://github.com/flyte/upnpclient/blob/develop/README.md Override or set HTTP authentication and headers for individual action calls. Setting them to None will use the device-level settings or defaults. ```python device.Layer3Forwarding1.GetDefaultConnectionService( http_auth=('myusername', 'mypassword'), http_headers={'Some-Required-Header': 'somevalue'}, ) ``` -------------------------------- ### Connect to a Specific Device Source: https://context7.com/flyte/upnpclient/llms.txt Connects directly to a UPnP device by fetching and parsing its root description XML from a given URL. Supports authentication and custom headers. ```APIDOC ## `upnpclient.Device(location, device_name=None, ignore_urlbase=False, http_auth=None, http_headers=None)` Connects directly to a UPnP device by fetching and parsing its root description XML from `location`. Populates `.friendly_name`, `.manufacturer`, `.model_name`, `.model_number`, `.serial_number`, `.udn`, `.device_type`, and `.services`. Raises `requests.exceptions.HTTPError` if the URL is unreachable. Use this when you already know a device's description URL and want to skip discovery. ```python import upnpclient # Direct connection — no discovery needed d = upnpclient.Device("http://192.168.1.1:5000/rootDesc.xml") print(d) # print(d.friendly_name) # OpenWRT router print(d.manufacturer) # OpenWrt print(d.model_name) # Linux/3.x.x, UPnP/1.0, MiniUPnPd/1.9 print(d.serial_number) # 000000000000 print(d.udn) # uuid:1234abcd-5678-efgh-ijkl-000000000001 # With HTTP Basic Auth (requests-compatible auth object) d_auth = upnpclient.Device( "http://192.168.1.1:5000/rootDesc.xml", http_auth=("admin", "password"), http_headers={"X-Custom-Header": "value"}, ) # Enumerate all services on the device for svc in d.services: print(svc.service_id, "->", svc.service_type) # urn:upnp-org:serviceId:Layer3Forwarding1 -> urn:schemas-upnp-org:service:Layer3Forwarding:1 # urn:upnp-org:serviceId:WANCommonIFC1 -> urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1 # urn:upnp-org:serviceId:WANIPConn1 -> urn:schemas-upnp-org:service:WANIPConnection:1 ``` ``` -------------------------------- ### Inspect Action Output Arguments Source: https://github.com/flyte/upnpclient/blob/develop/README.md Understand the output arguments returned by an action using the 'argsdef_out' attribute. This details the names and types of returned values. ```python print(d.WANIPConn1.GetStatusInfo.argsdef_out) ``` -------------------------------- ### Call Action with Arguments Source: https://github.com/flyte/upnpclient/blob/develop/README.md Invoke an action by providing the required arguments as keyword arguments, as defined by 'argsdef_in'. ```python 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 ) ``` -------------------------------- ### Calling an Action Source: https://context7.com/flyte/upnpclient/llms.txt Invoke a UPnP action by calling the Action object directly. Pass required arguments as keyword arguments. Handles argument validation and returns results as a dictionary. ```APIDOC ## `Action.__call__(**kwargs)` — calling an action Actions are callable. Pass required arguments as keyword arguments. The library validates each argument against the UPnP SCPD type constraints before dispatching a SOAP call. On success, returns a dict of output values marshalled to native Python types. Raises `UPNPError` for missing required arguments, `ValidationError` for type/range violations, and `SOAPError` for UPnP-level device errors. ```python import upnpclient from upnpclient import UPNPError, ValidationError d = upnpclient.Device("http://192.168.1.1:5000/rootDesc.xml") # Zero-argument call status = d.WANIPConn1.GetStatusInfo() print(status) # {'NewConnectionStatus': 'Connected', # 'NewLastConnectionError': 'ERROR_NONE', # 'NewUptime': 14851479} # <-- already an int, not a string # Call with arguments — add a NAT port mapping result = d.WANIPConn1.AddPortMapping( NewRemoteHost="0.0.0.0", NewExternalPort=8080, NewProtocol="TCP", NewInternalPort=8080, NewInternalClient="192.168.1.50", NewEnabled="1", NewPortMappingDescription="My App", NewLeaseDuration=3600, ) print(result) # {} (no output args for AddPortMapping) # Missing required argument → UPNPError try: d.WANIPConn1.AddPortMapping(NewRemoteHost="0.0.0.0") except UPNPError as e: print(e) # Missing required param 'NewExternalPort' # Invalid value → ValidationError try: d.WANIPConn1.AddPortMapping( NewRemoteHost="0.0.0.0", NewExternalPort=99999, # ui2 max is 65535 NewProtocol="TCP", NewInternalPort=8080, NewInternalClient="192.168.1.50", NewEnabled="1", NewPortMappingDescription="My App", NewLeaseDuration=3600, ) except ValidationError as e: print(e.reasons) # {'NewExternalPort': "'ui2' datatype must be a number in the range 0 to 65535"} # Per-call auth override d.WANIPConn1.GetExternalIPAddress( http_auth=("user", "pass"), http_headers={"X-Forwarded-For": "10.0.0.1"}, ) ``` ``` -------------------------------- ### Call a UPnP action Source: https://context7.com/flyte/upnpclient/llms.txt Executes a UPnP action by calling the Action object. Arguments are passed as keyword arguments and validated against UPnP SCPD constraints. Returns a dictionary of output values on success, or raises UPNPError, ValidationError, or SOAPError on failure. ```python import upnpclient from upnpclient import UPNPError, ValidationError d = upnpclient.Device("http://192.168.1.1:5000/rootDesc.xml") # Zero-argument call status = d.WANIPConn1.GetStatusInfo() print(status) # {'NewConnectionStatus': 'Connected', # 'NewLastConnectionError': 'ERROR_NONE', # 'NewUptime': 14851479} # <-- already an int, not a string # Call with arguments — add a NAT port mapping result = d.WANIPConn1.AddPortMapping( NewRemoteHost="0.0.0.0", NewExternalPort=8080, NewProtocol="TCP", NewInternalPort=8080, NewInternalClient="192.168.1.50", NewEnabled="1", NewPortMappingDescription="My App", NewLeaseDuration=3600, ) print(result) # {} (no output args for AddPortMapping) # Missing required argument → UPNPError try: d.WANIPConn1.AddPortMapping(NewRemoteHost="0.0.0.0") except UPNPError as e: print(e) # Missing required param 'NewExternalPort' # Invalid value → ValidationError try: d.WANIPConn1.AddPortMapping( NewRemoteHost="0.0.0.0", NewExternalPort=99999, # ui2 max is 65535 NewProtocol="TCP", NewInternalPort=8080, NewInternalClient="192.168.1.50", NewEnabled="1", NewPortMappingDescription="My App", NewLeaseDuration=3600, ) except ValidationError as e: print(e.reasons) # {'NewExternalPort': "'ui2' datatype must be a number in the range 0 to 65535"} # Per-call auth override d.WANIPConn1.GetExternalIPAddress( http_auth=("user", "pass"), http_headers={"X-Forwarded-For": "10.0.0.1"}, ) ``` -------------------------------- ### Call Service Actions by Name Source: https://github.com/flyte/upnpclient/blob/develop/README.md Invoke actions on a service. If the service name is not a valid Python attribute, use dictionary-style access. ```python d.WANIPConn1.GetStatusInfo() ``` ```python d["WANIPConn1"]["GetStatusInfo"]() ``` -------------------------------- ### Argument Validation Source: https://context7.com/flyte/upnpclient/llms.txt Explains how to use the static `Action.validate_arg` method to validate argument values against UPnP state variable definitions, checking for type and allowed value constraints. ```APIDOC ## `Action.validate_arg(arg, argdef)` — static argument validator Validates a single argument value against a UPnP statevar definition. Returns `(is_valid: bool, reasons: set)`. Reasons is an empty set on success, or contains human-readable failure messages. Used internally before every SOAP call but also available directly for pre-flight checks. ```python from upnpclient.upnp import Action # Validate a ui2 integer in range valid, reasons = Action.validate_arg("8080", {"datatype": "ui2", "allowed_values": set()}) print(valid, reasons) # True set() # Out-of-range ui2 valid, reasons = Action.validate_arg("99999", {"datatype": "ui2", "allowed_values": set()}) print(valid, reasons) # False {"'ui2' datatype must be a number in the range 0 to 65535"} # String with allowed values valid, reasons = Action.validate_arg( "HDMI", {"datatype": "string", "allowed_values": {"TCP", "UDP"}} ) print(valid, reasons) # False {"Value 'HDMI' not in allowed values list"} # Boolean valid, reasons = Action.validate_arg("yes", {"datatype": "boolean", "allowed_values": set()}) print(valid, reasons) # True set() # UUID valid, reasons = Action.validate_arg( "not-a-uuid", {"datatype": "uuid", "allowed_values": set()} ) print(valid, reasons) # False {"'uuid' datatype must contain a valid UUID"} ``` ``` -------------------------------- ### Inspect Action Input Arguments Source: https://github.com/flyte/upnpclient/blob/develop/README.md Examine the input arguments required for an action using the 'argsdef_in' attribute. This shows argument names, data types, and allowed values. ```python print(d.WANIPConn1.AddPortMapping.argsdef_in) ``` -------------------------------- ### Find an action by name Source: https://context7.com/flyte/upnpclient/llms.txt Searches for an action by its name across all services on a device or within a specific service. Returns the Action instance or None if not found. Useful when the action name is known but its service is not. ```python import upnpclient d = upnpclient.Device("http://192.168.1.1:5000/rootDesc.xml") # Find across all services on the device action = d.find_action("GetExternalIPAddress") if action is not None: result = action() print(result) # {'NewExternalIPAddress': '203.0.113.42'} else: print("Action not found on this device") # Find within a specific service action2 = d.WANIPConn1.find_action("AddPortMapping") print(action2) # # Returns None (not raises) when missing missing = d.find_action("NonExistentAction") print(missing) # None ``` -------------------------------- ### Finding an Action by Name Source: https://context7.com/flyte/upnpclient/llms.txt Search for a specific action by its name across all services on a device or within a single service. Returns the Action object or None if not found. ```APIDOC ## `Device.find_action(action_name)` / `Service.find_action(action_name)` Searches all services (on `Device`) or the current service's action map (on `Service`) for an action by name. Returns the `Action` instance if found, or `None` if not. Useful when you know the action name but not which service it belongs to. ```python import upnpclient d = upnpclient.Device("http://192.168.1.1:5000/rootDesc.xml") # Find across all services on the device action = d.find_action("GetExternalIPAddress") if action is not None: result = action() print(result) # {'NewExternalIPAddress': '203.0.113.42'} else: print("Action not found on this device") # Find within a specific service action2 = d.WANIPConn1.find_action("AddPortMapping") print(action2) # # Returns None (not raises) when missing missing = d.find_action("NonExistentAction") print(missing) # None ``` ``` -------------------------------- ### Discover UPnP Devices Source: https://context7.com/flyte/upnpclient/llms.txt Broadcasts SSDP M-SEARCH packets to find UPnP devices on the local network. The timeout parameter can be adjusted to wait longer for responses. Non-responsive devices are ignored. ```python import upnpclient # Discover all UPnP devices on the local network (default 5-second window) devices = upnpclient.discover() # [, , ] # Increase timeout for slower networks devices = upnpclient.discover(timeout=10) for d in devices: print(d.friendly_name, "@", d.location) print(" Manufacturer:", d.manufacturer) print(" Model: ", d.model_name) print(" UDN: ", d.udn) # OpenWRT router @ http://192.168.1.1:5000/rootDesc.xml # Manufacturer: OpenWrt # Model: Linux/3.x.x, UPnP/1.0, MiniUPnPd/1.9 # UDN: uuid:1234abcd-5678-efgh-ijkl-000000000001 ``` -------------------------------- ### Accessing Device Services Source: https://context7.com/flyte/upnpclient/llms.txt Provides methods to access the services exposed by a UPnP device, either as a list or through attribute/dictionary-style access using service IDs. ```APIDOC ## `Device.services` / `Device.` / `Device[key]` `Device.services` is the list of `Service` objects the device exposes. Services are also accessible as attributes or dictionary keys using the trailing segment of their `service_id` (e.g., `urn:upnp-org:serviceId:WANIPConn1` → `d.WANIPConn1`). Attribute-style access is convenient in interactive sessions; dictionary-style access handles names that are not valid Python identifiers. ```python import upnpclient d = upnpclient.Device("http://192.168.1.1:5000/rootDesc.xml") # List all services print(d.services) # [, # , # ] # Attribute access — uses the last segment of the service_id svc = d.WANIPConn1 print(svc) # print(svc.name) # WANIPConn1 print(svc.service_type) # urn:schemas-upnp-org:service:WANIPConnection:1 # Dictionary-style access (useful for names like "wandsllc:pvc_Internet") svc2 = d["WANIPConn1"] ``` ``` -------------------------------- ### List Device Services Source: https://github.com/flyte/upnpclient/blob/develop/README.md Access the 'services' attribute of a Device object to see available services. Services are identified by their 'service_id'. ```python print(d.services) ``` -------------------------------- ### Subscribe, Renew, and Cancel UPnP Event Subscriptions Source: https://context7.com/flyte/upnpclient/llms.txt Subscribe to UPnP events to receive state-change notifications. Use the returned SID to renew subscriptions before they expire or cancel them when no longer needed. Handles `UnexpectedResponse` exceptions. ```python import upnpclient from upnpclient import UnexpectedResponse d = upnpclient.Device("http://192.168.1.1:5000/rootDesc.xml") svc = d.WANIPConn1 # Subscribe — device will POST events to your callback URL try: sid, timeout = svc.subscribe( "http://192.168.1.100:8080/upnp-callback", timeout=1800, # request 30-minute subscription ) print(f"Subscribed: SID={sid}, timeout={timeout}s") # Subscribed: SID=uuid:abcdef12-3456-7890-abcd-ef1234567890, timeout=1800 # Renew before it expires new_timeout = svc.renew_subscription(sid, timeout=1800) print(f"Renewed: new timeout={new_timeout}s") # Cancel when done svc.cancel_subscription(sid) print("Unsubscribed") except UnexpectedResponse as e: print("Device returned an unexpected subscription response:", e) ``` -------------------------------- ### Call UPnP Actions Directly on Device or Service Source: https://context7.com/flyte/upnpclient/llms.txt Use the `__call__` method as a shorthand to invoke actions on a UPnP device or service. Raises `InvalidActionException` if the action does not exist. Supports direct calls on the device (searches all services) or on a specific service. ```python import upnpclient from upnpclient import InvalidActionException d = upnpclient.Device("http://192.168.1.1:5000/rootDesc.xml") # Call directly on device — searches all services result = d("GetExternalIPAddress") print(result) # {'NewExternalIPAddress': '203.0.113.42'} # Call directly on service — restricted to that service result = d.WANIPConn1("GetStatusInfo") print(result) # {'NewConnectionStatus': 'Connected', 'NewLastConnectionError': 'ERROR_NONE', 'NewUptime': 57600} # Dictionary-style chaining result = d["WANIPConn1"]["GetStatusInfo"]() print(result) # {'NewConnectionStatus': 'Connected', ...} # InvalidActionException when action doesn't exist try: d("NoSuchAction") except InvalidActionException as e: print(e) # Action with name 'NoSuchAction' does not exist. ``` -------------------------------- ### Access Service Actions Source: https://github.com/flyte/upnpclient/blob/develop/README.md Services can be accessed using the last part of their 'service_id' as an attribute. The 'actions' attribute lists available actions for that service. ```python print(d.WANIPConn1.actions) ``` -------------------------------- ### Discover UPnP Devices Source: https://context7.com/flyte/upnpclient/llms.txt Broadcasts SSDP M-SEARCH packets to find UPnP devices on the local network. Allows specifying a timeout for responses. ```APIDOC ## `upnpclient.discover(timeout=5)` Broadcasts SSDP M-SEARCH packets on all local IPv4 interfaces and returns a list of `Device` instances for every responding UPnP device. Non-responsive or invalid devices are silently skipped. The `timeout` parameter controls how many seconds to wait for responses. ```python import upnpclient # Discover all UPnP devices on the local network (default 5-second window) devices = upnpclient.discover() # [, , ] # Increase timeout for slower networks devices = upnpclient.discover(timeout=10) for d in devices: print(d.friendly_name, "@", d.location) print(" Manufacturer:", d.manufacturer) print(" Model: ", d.model_name) print(" UDN: ", d.udn) # OpenWRT router @ http://192.168.1.1:5000/rootDesc.xml # Manufacturer: OpenWrt # Model: Linux/3.x.x, UPnP/1.0, MiniUPnPd/1.9 # UDN: uuid:1234abcd-5678-efgh-ijkl-000000000001 ``` ``` -------------------------------- ### Access Device Services Source: https://context7.com/flyte/upnpclient/llms.txt Provides access to the `Service` objects exposed by a `Device`. Services can be accessed via the `Device.services` list, attribute access using the service ID's last segment, or dictionary-style access for names that are not valid Python identifiers. ```python import upnpclient d = upnpclient.Device("http://192.168.1.1:5000/rootDesc.xml") # List all services print(d.services) # [, # , # ] # Attribute access — uses the last segment of the service_id svc = d.WANIPConn1 print(svc) # print(svc.name) # WANIPConn1 print(svc.service_type) # urn:schemas-upnp-org:service:WANIPConnection:1 # Dictionary-style access (useful for names like "wandsllc:pvc_Internet") svc2 = d["WANIPConn1"] ``` -------------------------------- ### Marshal UPnP Data Types to Python Values Source: https://context7.com/flyte/upnpclient/llms.txt Convert raw XML string values from SOAP responses into appropriate Python types based on UPnP datatype names. This function is called automatically for action return values but can also be used standalone. ```python from upnpclient.marshal import marshal_value # Integers print(marshal_value("ui4", "14851479")) # (True, 14851479) print(marshal_value("i2", "-32000")) # (True, -32000) # Boolean print(marshal_value("boolean", "1")) # (True, True) print(marshal_value("boolean", "false")) # (True, False) # Decimal print(marshal_value("r8", "3.14")) # (True, Decimal('3.14')) # Date / datetime print(marshal_value("date", "2024-03-15")) # (True, datetime.date(2024, 3, 15)) print(marshal_value("dateTime", "2024-03-15T10:30:00")) # (True, datetime.datetime(2024, 3, 15, 10, 30)) # UUID print(marshal_value("uuid", "550e8400-e29b-41d4-a716-446655440000")) # (True, UUID('550e8400-e29b-41d4-a716-446655440000')) # URI from urllib.parse import ParseResult print(marshal_value("uri", "http://example.com/path")) # (True, ParseResult(scheme='http', netloc='example.com', path='/path', ...)) # Passthrough for unknown types print(marshal_value("unknown_type", "raw string")) # (False, 'raw string') ``` -------------------------------- ### UPnP Type Marshalling Source: https://context7.com/flyte/upnpclient/llms.txt Details the `marshal_value` function for converting raw XML string values from SOAP responses into appropriate Python types based on UPnP datatypes. ```APIDOC ## `marshal_value(datatype, value)` — UPnP type marshalling Converts a raw XML string value from a SOAP response into the appropriate Python type, given a UPnP datatype name. Returns `(was_marshalled: bool, python_value)`. Called automatically on all action return values; also available for standalone use. ```python from upnpclient.marshal import marshal_value # Integers print(marshal_value("ui4", "14851479")) # (True, 14851479) print(marshal_value("i2", "-32000")) # (True, -32000) # Boolean print(marshal_value("boolean", "1")) # (True, True) print(marshal_value("boolean", "false")) # (True, False) # Decimal print(marshal_value("r8", "3.14")) # (True, Decimal('3.14')) # Date / datetime print(marshal_value("date", "2024-03-15")) # (True, datetime.date(2024, 3, 15)) print(marshal_value("dateTime", "2024-03-15T10:30:00")) # (True, datetime.datetime(2024, 3, 15, 10, 30)) # UUID print(marshal_value("uuid", "550e8400-e29b-41d4-a716-446655440000")) # (True, UUID('550e8400-e29b-41d4-a716-446655440000')) # URI from urllib.parse import ParseResult print(marshal_value("uri", "http://example.com/path")) # (True, ParseResult(scheme='http', netloc='example.com', path='/path', ...)) # Passthrough for unknown types print(marshal_value("unknown_type", "raw string")) # (False, 'raw string') ``` ```