diff --git a/docs/apidoc/upnp.HTTP.rst b/docs/apidoc/upnp.HTTP.rst
index 8bc03e4..6bc1dcb 100644
--- a/docs/apidoc/upnp.HTTP.rst
+++ b/docs/apidoc/upnp.HTTP.rst
@@ -3,3 +3,5 @@ upnp.HTTP module
.. automodule:: upnp.HTTP
:members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/apidoc/upnp.Objects.rst b/docs/apidoc/upnp.Objects.rst
index f77a53c..fa385c4 100644
--- a/docs/apidoc/upnp.Objects.rst
+++ b/docs/apidoc/upnp.Objects.rst
@@ -3,3 +3,5 @@ upnp.Objects module
.. automodule:: upnp.Objects
:members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/apidoc/upnp.SSDP.rst b/docs/apidoc/upnp.SSDP.rst
index 2a88264..46172c0 100644
--- a/docs/apidoc/upnp.SSDP.rst
+++ b/docs/apidoc/upnp.SSDP.rst
@@ -3,3 +3,5 @@ upnp.SSDP module
.. automodule:: upnp.SSDP
:members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/apidoc/upnp.UPnP.rst b/docs/apidoc/upnp.UPnP.rst
index a687875..465a80c 100644
--- a/docs/apidoc/upnp.UPnP.rst
+++ b/docs/apidoc/upnp.UPnP.rst
@@ -3,3 +3,5 @@ upnp.UPnP module
.. automodule:: upnp.UPnP
:members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/apidoc/upnp.rst b/docs/apidoc/upnp.rst
index c309093..aa42111 100644
--- a/docs/apidoc/upnp.rst
+++ b/docs/apidoc/upnp.rst
@@ -16,3 +16,5 @@ Module contents
.. automodule:: upnp
:members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/make.bat b/docs/make.bat
index 298f9a9..d9605b8 100644
--- a/docs/make.bat
+++ b/docs/make.bat
@@ -27,7 +27,8 @@ if errorlevel 9009 (
exit /b 1
)
-%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
+:html
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -a
goto end
:help
diff --git a/docs/quickstart/index.rst b/docs/quickstart/index.rst
index 92c7bf1..206e815 100644
--- a/docs/quickstart/index.rst
+++ b/docs/quickstart/index.rst
@@ -3,7 +3,6 @@ Quickstart
.. toctree::
:maxdepth: 2
- :hidden:
dict
attribute
@@ -11,18 +10,25 @@ Quickstart
IoT-UPnP require python 3.x. It use the following modules:
-* asyncio: for the main event loop
-* ssdp: base library for SSDP (a component of UPnP)
-* netifaces: Network interfaces discovery (to retrieve IPs)
+
+ * asyncio: for the main event loop
+ * ssdp: base library for SSDP (a component of UPnP)
+ * netifaces: Network interfaces discovery (to retrieve IPs)
They are tree important objects:
.. autoclass:: upnp.Announcer
+ See :class:`upnp.UPnP.Announcer`
+
.. autoclass:: upnp.Device
+ See :class:`upnp.Objects.Device`
+
.. autoclass:: upnp.Service
+ See :class:`upnp.Objects.Service`
+
All objects can be set with theirs attributes or by passing a dict on the contructor.
Goal
diff --git a/upnp/HTTP.py b/upnp/HTTP.py
index 3e4c075..9130b1f 100644
--- a/upnp/HTTP.py
+++ b/upnp/HTTP.py
@@ -2,39 +2,48 @@
import asyncio
from socket import gethostname
-class HttpResponder(asyncio.Protocol):
- def connection_made(self, transport):
- peername = transport.get_extra_info('peername')
- print('Connection from {}'.format(peername))
- self.transport = transport
-
- def connection_lost(self, exc):
- print('Connection lost')
-
- def data_received(self, data):
- message = data.decode()
- print('Data received: {!r}'.format(message))
- print('Close the client socket')
- #self.transport.close()
-
- def eof_received(self):
- print('End data')
-
class HttpRequest:
+ """
+ HTTP Request informations
+ """
def __init__(self, method, path, version, headers):
+ """
+ Initiate a request
+
+ :param method: HTTP request method
+ :type method: str
+ :param path: The requested path
+ :type path: str
+ :param version: HTTP version
+ :type version: str
+ :param headers: Dictionary of request headers
+ :type headers: {str:str}
+ """
self.method = method
self.path = path
self.version = version
self.headers = headers
def pprint(self):
+ """
+ Print human readable request
+ """
print('{} {} {}'.format(self.method, self.path, self.version))
for h in self.headers:
print (' {}: {}'.format(h, self.headers[h]))
print()
class HttpAnswer:
+ """
+ Class to construct an HTTP response
+ """
def __init__(self, request):
+ """
+ Initiate a response
+
+ :param request: Origin request
+ :type request: upnp.HTTP.HttpRequest
+ """
self.request = request
self.statusCode = 200
self.statusText = 'OK'
@@ -46,6 +55,12 @@ def __init__(self, request):
self.data = None
def write(self, writer):
+ """
+ Send the response
+
+ :param writer: Writer to send pakcet
+ :type writer: asyncio.StreamWriter
+ """
writer.write('{} {} {}\r\n'.format(self.version, self.statusCode, self.statusText).encode('latin1'))
for h in self.headers:
writer.write('{}: {}\r\n'.format(h, self.headers[h]).encode('latin1'))
@@ -55,26 +70,57 @@ def write(self, writer):
writer.write(b'\r\n')
def pprint(self):
+ """
+ Show the packet as human readable
+ """
print('{} {} {}'.format(self.version, self.statusCode, self.statusText))
for h in self.headers:
print(' {}: {}'.format(h, self.headers[h]))
print()
def execute(self):
+ """
+ Need to be overrided by subclass. It's the execute process.
+ """
pass
class ServerErrorAnswer(HttpAnswer):
+ """
+ HTTP error response
+ """
def execute(self):
+ """
+ Prepare the response
+ """
self.statusCode = 500
self.statusText = 'Internal Server Error'
self.data = '
Internal Server Error
An internal server error. See logs.
'
class DescriptionAnswer(HttpAnswer):
+ """
+ HTTP success, describe a device (XML)
+ """
+
def __init__(self, request, upnp):
+ """
+ Initiate a device description
+
+ :param request: Origin request
+ :type request: upnp.HTTP.HttpRequest
+ :param upnp: An UPnP configuration to describe
+ :type upnp: upnp.UPnP.Announcer
+ """
super(DescriptionAnswer, self).__init__(request)
self.upnp = upnp
def describeDevice(self, device):
+ """
+ Add a device description to the answer
+
+ :param device: Device to describe
+ :type device: upnp.Objects.Device
+ """
+
self.data += """
{DEVICE.deviceType}
@@ -109,9 +155,22 @@ def describeDevice(self, device):
def describeIcon(self, icon):
+ """
+ Add an icon description to the answer
+
+ :param icon: Icon to describe
+ :type icon: upnp.Objects.Icon
+ """
pass
def describeService(self, service):
+ """
+ Add a service description to the answer
+
+ :param service: Service to describe
+ :type service: upnp.Objects.Service
+ """
+
self.data += """
{SERVICE.serviceType}
@@ -123,6 +182,9 @@ def describeService(self, service):
""".format(SERVICE=service)
def execute(self):
+ """
+ Prepare the description answer
+ """
self.headers['Content-Type'] = 'application/xml; charset=utf-8'
self.URL = 'http://{}'.format(self.request.headers['host'])
@@ -142,11 +204,27 @@ def execute(self):
"""
class ScpdAnswer(HttpAnswer):
+ """
+ HTTP success, Describe a service API
+
+ """
def __init__(self, request, upnp):
+ """
+ Initiate services to describe from the root device
+
+ :param request: Origin request
+ :type request: upnp.HTTP.HttpRequest
+ :param upnp: An UPnP configuration to describe
+ :type upnp: upnp.UPnP.Announcer
+ """
+
super(DescriptionAnswer, self).__init__(request)
self.upnp = upnp
def execute(self):
+ """
+ Prepare the answer
+ """
self.headers['Content-Type'] = 'application/xml; charset=utf-8'
URL = 'http://{}'.format(self.request.headers['host'])
@@ -163,10 +241,29 @@ def execute(self):
""".format(UUID=UUID, URL=URL, CONFIGID=self.upnp.configId)
class HttpServer:
+ """
+ class to handle asyncio events on HTTP service
+ """
+
def __init__(self, config):
+ """
+ Initiate the HTTP server
+
+ :param config: HTTP configuration
+ :type config: upnp.HTTP.HTTP
+ """
self.config = config
def InConnection(self, reader, writer):
+ """
+ A new incomming connection
+
+ :param reader: Request input
+ :type reader: asyncio.StreamReader
+ :param writer: Stream to answer
+ :type writer: asyncio.StreamWriter
+ """
+
header = yield from reader.readline()
cheaders = header.decode('latin1').strip()
method, path, vers = cheaders.split(' ')
@@ -189,6 +286,15 @@ def InConnection(self, reader, writer):
writer.close()
def HttpRouting(self, request):
+ """
+ A simple routing by path for incomming requests
+
+ :param request: The incomming request
+ :type request: upnp.HTTP.HttpRequest
+ :return: The answer to execute
+ :rtype: upnp.HTTP.HttpAnswer
+ """
+
if request.path == '/descr.xml':
ans = DescriptionAnswer(request, self.config.annoncer)
elif request.path == '/scpd.xml':
@@ -198,7 +304,22 @@ def HttpRouting(self, request):
return ans
class HTTP:
+ """
+ The main HTTP server class
+ """
+
def __init__(self, annoncer, port, netbind):
+ """
+ Initiate an HTTP Server
+
+ :param annoncer: The UPnP configuration
+ :type annoncer: upnp.UPnP.Announcer
+ :param port: HTTP port
+ :type port: int
+ :param netbind: Interface address to bind
+ :type netbind: str
+ """
+
self.port = port
self.netbind = netbind
self.server = None
@@ -209,8 +330,19 @@ def __init__(self, annoncer, port, netbind):
self.netbind = None
def initLoop(self, loop):
+ """
+ Add HTTP handlers on the asyncio loop
+
+ :param loop: Loop to use
+ :type loop: asyncio.AbstractEventLoop
+ """
+
self.server = asyncio.start_server(self.http_server.InConnection, port=self.port, host=self.netbind)
self.httploop = loop.run_until_complete(self.server)
def dispose(self):
+ """
+ Close HTTP handling
+ """
+
pass
diff --git a/upnp/Objects.py b/upnp/Objects.py
index 23acc87..851a7a9 100644
--- a/upnp/Objects.py
+++ b/upnp/Objects.py
@@ -20,7 +20,9 @@ class Device(_BaseObj):
def __init__(self, obj=None):
"""
Device object initialisation
+
:param obj: A dict with attributes
+ :type obj: dict or None
"""
self.services = []
self.devices = []
@@ -42,6 +44,7 @@ def __init__(self, obj=None):
def addService(self, service):
"""
Add a service on this device
+
:param service: The service to add
:type service: upnp.Service
"""
@@ -50,6 +53,7 @@ def addService(self, service):
def addDevice(self, device):
"""
Add an embedded device
+
:param device: The embedded device to add
:type device: upnp.Device
"""
diff --git a/upnp/SSDP.py b/upnp/SSDP.py
index 3d3c837..c299723 100644
--- a/upnp/SSDP.py
+++ b/upnp/SSDP.py
@@ -6,11 +6,22 @@
import netifaces
class AnnouncerService(ssdp.SimpleServiceDiscoveryProtocol):
-
+ """
+ Endpoint for UDP packets (used by asyncio)
+ """
def __init__(self):
+ """
+ Initiate the UDP endpoint
+ """
self.annonces = None
def connection_made(self, transport):
+ """
+ Called when the connection is made
+
+ :param transport: The endpoint transport
+ :type transport: asyncio.BaseTransport
+ """
self.transport = transport
sock = transport.get_extra_info("socket")
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
@@ -19,10 +30,19 @@ def connection_made(self, transport):
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
def response_received(self, response, addr):
+ """
+ Not used
+ """
# This will never been called (UDP)
print("Response", addr)
def request_received(self, request, addr):
+ """
+ Handle a new SSDP packet
+
+ :param request: Request informations
+ :param addr: Adress of the peer
+ """
if (request.method == 'M-SEARCH'):
headers = dict()
for (name, value) in request.headers:
@@ -32,7 +52,19 @@ def request_received(self, request, addr):
self.annonces.answer(headers['ST'], addr)
class Notify(ssdp.SSDPRequest):
+ """
+ SSDP Notify packet
+ """
+
def __init__(self, config, device):
+ """
+ Init a Notify packet
+
+ :param config: An instance of SSDP service
+ :type config: upnp.SSDP.SSDP
+ :param device: A device to notify
+ :type device: upnp.Device
+ """
self.transport = config.transport
self.config = config
self.nts = "ssdp:alive"
@@ -43,6 +75,16 @@ def __init__(self, config, device):
super(Notify, self).__init__('NOTIFY')
def send(self, ip, usn = None, transport = None):
+ """
+ Build and send the packet
+
+ :param ip: Destination IP
+ :type ip: str
+ :param usn: USN to notify
+ :type usn: str
+ :param transport: Transport to use
+ :type transport: asyncio.BaseTransport
+ """
if (transport == None):
transport = self.transport
@@ -69,17 +111,49 @@ def send(self, ip, usn = None, transport = None):
self.sendto(transport, (AnnouncerService.MULTICAST_ADDRESS, self.config.port))
def sendto(self, transport, addr):
+ """
+ Rewriting of raw sending method (error at the end of headers).
+
+ :param transport: Transport to use
+ :type transport: asyncio.BaseTransport
+ :param addr: Destination address
+ :type addr: str
+ """
msg = bytes(self) + b'\r\n\r\n'
transport.sendto(msg, addr)
self.config.srv.annonces.count = self.config.srv.annonces.count + 1
class Answer(ssdp.SSDPResponse):
+ """
+ Answer packet for M-SEARCH queries
+ """
+
def __init__(self, config, status_code, reason):
+ """
+ Initiate an SSDP answer
+
+ :param config: SSDP configuration
+ :type config: upnp.SSDP.SSDP
+ :param status_code: Status code (like HTML)
+ :type status_code: int
+ :param reason: Text reason for status code
+ :type reason: str
+ """
super(Answer, self).__init__(status_code, reason)
self.config = config
self.st = ''
def send(self, device, ip, addr):
+ """
+ Send the UDP answer
+
+ :param device: Device to announce
+ :type device: upnp.Objects.Device
+ :param ip: IP of device
+ :type ip: str
+ :param addr: Destination IP
+ :type addr: str
+ """
import datetime
self.headers = [
@@ -96,16 +170,42 @@ def send(self, device, ip, addr):
self.sendto(self.config.transport, addr)
def sendto(self, transport, addr):
+ """
+ Rewriting of raw sending method (error at the end of headers).
+
+ :param transport: Transport to use
+ :type transport: asyncio.BaseTransport
+ :param addr: Destination address
+ :type addr: str
+ """
msg = bytes(self) + b'\r\n\r\n'
transport.sendto(msg, addr)
self.config.srv.annonces.count = self.config.srv.annonces.count + 1
class SSDP_Protocol:
+ """
+ Simple class to send packets
+ """
+
def __init__(self, config):
+ """
+ Create a new instance
+
+ :param config: SSDP configuration
+ :type config: upnp.SSDP.SSDP
+ """
self.config = config
self.count = 1
def notify(self, device, ip):
+ """
+ Send NOTIFY packets for a device (services and embedded devices)
+
+ :param device: Device to announce
+ :type device: upnp.Objects.Device
+ :param ip: IP of the device
+ :type ip: str
+ """
notify = Notify(self.config, device)
notify.send(ip)
@@ -116,6 +216,14 @@ def notify(self, device, ip):
notify.send(ip, service)
def answer(self, st, addr):
+ """
+ Answer to an M-SEARCH query
+
+ :param st: Queried subject
+ :type st: str
+ :param addr: Destination address of answer
+ :type addr: (str, int)
+ """
import locale, datetime
message = Answer(self.config, 200, "OK")
@@ -126,6 +234,15 @@ def answer(self, st, addr):
message.send(device, ip, addr)
def getDevices(self, st):
+ """
+ Get all devices which match ST
+
+ :param st: Queried subject to match
+ :type st: str
+ :return: List of devices that match query
+ :rtype: list(upnp.Objects.Device)
+ """
+
#root device
if st == 'upnp:rootdevice':
return [self.config.annoncer.device]
@@ -136,6 +253,14 @@ def getDevices(self, st):
return devices
def provides(self, usn):
+ """
+ Check if USN is provided by root device
+
+ :param usn: USN to test
+ :type usn: str
+ :return: True if USN is provided
+ :rtype: bool
+ """
if usn == 'upnp:rootdevice':
return True
@@ -144,7 +269,20 @@ def provides(self, usn):
return False
class SSDP:
+ """
+ Public class to handle SSDP protocol
+ """
+
def __init__(self, annoncer, netBind='0.0.0.0'):
+ """
+ Initiate an SSDP endpoint
+
+ :param annoncer: An announcer configuration
+ :type annoncer: upnp.UPnP.Announcer
+ :param netBind: Interface address to bind
+ :type netBind: str
+ """
+
self.annoncer = annoncer
self.port = 1900
self.family = socket.AF_INET
@@ -165,13 +303,25 @@ def __init__(self, annoncer, netBind='0.0.0.0'):
self.interfaces = [netBind]
def initLoop(self, loop):
+ """
+ Initiate an asyncio event loop
+
+ :param loop: An asyncio event loop
+ :type loop: asyncio.AbstractEventLoop
+ """
self.client = loop.create_datagram_endpoint(AnnouncerService, family=self.family, local_addr=(self.netbind, self.port))
self.transport, self.srv = loop.run_until_complete(self.client)
self.srv.annonces = SSDP_Protocol(self)
def notify(self):
+ """
+ Send NOTIFY packets
+ """
for ip in self.interfaces:
self.srv.annonces.notify(self.annoncer.device, ip)
def dispose(self):
+ """
+ Close SSDP handling
+ """
self.transport.close()
diff --git a/upnp/UPnP.py b/upnp/UPnP.py
index f1e4df2..7a3949f 100644
--- a/upnp/UPnP.py
+++ b/upnp/UPnP.py
@@ -6,12 +6,22 @@
from random import randrange
-class Annoncer:
+class Announcer:
"""
Annoncer main class
"""
def __init__(self, device, httpPort=5000, netBind='0.0.0.0'):
+ """
+ Initiate a UPnP Announcer and responder server
+
+ :param device: The root device
+ :type device: upnp.Device
+ :param httpPort: The HTTP port number for description and control
+ :type httpPort: int
+ :param netBind: An interface IP to bind to
+ :type netBind: str
+ """
self.device = None
self.loop = asyncio.get_event_loop()
@@ -24,6 +34,9 @@ def __init__(self, device, httpPort=5000, netBind='0.0.0.0'):
def initLoop(self, loop=None):
"""
Initialise an asyncio loop to handle network packages
+
+ :param loop: An asyncio event loop to initialise
+ :type loop: asyncio.AbstractEventLoop
"""
if loop != None:
self.loop = loop
@@ -32,9 +45,15 @@ def initLoop(self, loop=None):
self.ssdp.initLoop(self.loop)
def notify(self):
+ """
+ Send NOTIFY SSDP packets on the network to announce a new device
+ """
self.ssdp.notify()
def bye(self):
+ """
+ Send BYE SSP packets on the network
+ """
pass
def dispose(self):
@@ -55,3 +74,6 @@ def foreaver(self):
self.dispose()
self.loop.close()
+
+class Annoncer(Announcer):
+ pass
diff --git a/upnp/__init__.py b/upnp/__init__.py
index 40d9893..d30569b 100644
--- a/upnp/__init__.py
+++ b/upnp/__init__.py
@@ -3,5 +3,5 @@
Module to announce a UPnP device on network
"""
-from .UPnP import Annoncer
+from .UPnP import Annoncer, Announcer
from .Objects import Device, Service