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