Source code for coherence.upnp.core.device

# Licensed under the MIT license
# http://opensource.org/licenses/mit-license.php

# Copyright (C) 2006 Fluendo, S.A. (www.fluendo.com).
# Copyright 2006, Frank Scholz <coherence@beebits.net>

import time

from lxml import etree
from twisted.internet import defer

import coherence.extern.louie as louie
from coherence import log
from coherence.upnp.core import utils
from coherence.upnp.core.service import Service
from . import xml_constants

ns = xml_constants.UPNP_DEVICE_NS


[docs]class Device(log.LogAble): logCategory = 'device' def __init__(self, parent=None, udn=None): log.LogAble.__init__(self) self.parent = parent self.udn = udn self.services = [] # self.uid = self.usn[:-len(self.st)-2] self.friendly_name = "" self.device_type = "" self.upnp_version = "n/a" self.friendly_device_type = "[unknown]" self.device_type_version = 0 self.detection_completed = False self.client = None self.icons = [] self.devices = [] louie.connect(self.receiver, 'Coherence.UPnP.Service.detection_completed', self) louie.connect(self.service_detection_failed, 'Coherence.UPnP.Service.detection_failed', self) def __repr__(self): return "embedded device %r %r, parent %r" % ( self.friendly_name, self.device_type, self.parent) # def __del__(self): # #print "Device removal completed" # pass
[docs] def as_dict(self): d = {'device_type': self.get_device_type(), 'friendly_name': self.get_friendly_name(), 'udn': self.get_id(), 'services': [x.as_dict() for x in self.services]} icons = [] for icon in self.icons: icons.append({"mimetype": icon['mimetype'], "url": icon['url'], "height": icon['height'], "width": icon['width'], "depth": icon['depth']}) d['icons'] = icons return d
[docs] def remove(self, *args): self.info('removal of {} {}'.format( self.friendly_name, self.udn)) while len(self.devices) > 0: device = self.devices.pop() self.debug('try to remove {}'.format(device)) device.remove() while len(self.services) > 0: service = self.services.pop() self.debug("try to remove %r", service) service.remove() if self.client is not None: louie.send( 'Coherence.UPnP.Device.remove_client', None, self.udn, self.client) self.client = None # del self return True
[docs] def receiver(self, *args, **kwargs): if self.detection_completed: return for s in self.services: if not s.detection_completed: return if self.udn is None: return self.detection_completed = True if self.parent is not None: self.info( 'embedded device {} {} initialized, parent {}'.format( self.friendly_name, self.device_type, self.parent)) louie.send( 'Coherence.UPnP.Device.detection_completed', None, device=self) if self.parent is not None: louie.send( 'Coherence.UPnP.Device.detection_completed', self.parent, device=self) else: louie.send( 'Coherence.UPnP.Device.detection_completed', self, device=self)
[docs] def service_detection_failed(self, device): self.remove()
[docs] def get_id(self): return self.udn
[docs] def get_uuid(self): return self.udn[5:]
[docs] def get_embedded_devices(self): return self.devices
[docs] def get_embedded_device_by_type(self, type): r = [] for device in self.devices: if type == device.friendly_device_type: r.append(device) return r
[docs] def get_services(self): return self.services
[docs] def get_service_by_type(self, type): if not isinstance(type, (tuple, list)): type = [type, ] for service in self.services: _, _, _, service_class, version = service.service_type.split(':') if service_class in type: return service
[docs] def add_service(self, service): self.debug('add_service {}'.format(service)) self.services.append(service)
# :fixme: This fails as Service.get_usn() is not implemented.
[docs] def remove_service_with_usn(self, service_usn): for service in self.services: if service.get_usn() == service_usn: self.services.remove(service) service.remove() break
[docs] def add_device(self, device): self.debug("Device add_device %r", device) self.devices.append(device)
[docs] def get_friendly_name(self): return self.friendly_name
[docs] def get_device_type(self): return self.device_type
[docs] def get_friendly_device_type(self): return self.friendly_device_type
[docs] def get_markup_name(self): try: return self._markup_name except AttributeError: self._markup_name = "%s:%s %s" % \ (self.friendly_device_type, self.device_type_version, self.friendly_name) return self._markup_name
[docs] def get_device_type_version(self): return self.device_type_version
[docs] def set_client(self, client): self.client = client
[docs] def get_client(self): return self.client
[docs] def renew_service_subscriptions(self): """ iterate over device's services and renew subscriptions """ self.info( 'renew service subscriptions for {}'.format(self.friendly_name)) now = time.time() for service in self.services: self.info('check service {} {} {} {}'.format(service.id, service.get_sid(), service.get_timeout(), now)) if service.get_sid() is not None: if service.get_timeout() < now: self.debug('wow, we lost an event subscription for {} {}, ' 'maybe we need to rethink the loop time and ' 'timeout calculation?'.format( self.friendly_name, service.get_id())) if service.get_timeout() < now + 30: service.renew_subscription() for device in self.devices: device.renew_service_subscriptions()
[docs] def unsubscribe_service_subscriptions(self): """ iterate over device's services and unsubscribe subscriptions """ sl = [] for service in self.get_services(): if service.get_sid() is not None: sl.append(service.unsubscribe()) dl = defer.DeferredList(sl) return dl
[docs] def parse_device(self, d): self.info('parse_device {}'.format(d)) self.device_type = d.findtext('./{%s}deviceType' % ns) self.friendly_device_type, self.device_type_version = \ self.device_type.split(':')[-2:] self.friendly_name = d.findtext('./{%s}friendlyName' % ns) self.udn = d.findtext('./{%s}UDN' % ns) self.info("found udn %r %r", self.udn, self.friendly_name) try: self.manufacturer = d.findtext('./{%s}manufacturer' % ns) except Exception: pass try: self.manufacturer_url = d.findtext('./{%s}manufacturerURL' % ns) except Exception: pass try: self.model_name = d.findtext('./{%s}modelName' % ns) except Exception: pass try: self.model_description = d.findtext('./{%s}modelDescription' % ns) except Exception: pass try: self.model_number = d.findtext('./{%s}modelNumber' % ns) except Exception: pass try: self.model_url = d.findtext('./{%s}modelURL' % ns) except Exception: pass try: self.serial_number = d.findtext('./{%s}serialNumber' % ns) except Exception: pass try: self.upc = d.findtext('./{%s}UPC' % ns) except Exception: pass try: self.presentation_url = d.findtext('./{%s}presentationURL' % ns) except Exception: pass try: for dlna_doc in d.findall( './{urn:schemas-dlna-org:device-1-0}X_DLNADOC'): try: self.dlna_dc.append(dlna_doc.text) except AttributeError: self.dlna_dc = [] self.dlna_dc.append(dlna_doc.text) except Exception: pass try: for dlna_cap in d.findall( './{urn:schemas-dlna-org:device-1-0}X_DLNACAP'): for cap in dlna_cap.text.split(','): try: self.dlna_cap.append(cap) except AttributeError: self.dlna_cap = [] self.dlna_cap.append(cap) except Exception: pass icon_list = d.find('./{%s}iconList' % ns) if icon_list is not None: from urllib.parse import urlparse url_base = "%s://%s" % urlparse(self.get_location())[:2] for icon in icon_list.findall('./{%s}icon' % ns): try: i = {} i['mimetype'] = icon.find('./{%s}mimetype' % ns).text i['width'] = icon.find('./{%s}width' % ns).text i['height'] = icon.find('./{%s}height' % ns).text i['depth'] = icon.find('./{%s}depth' % ns).text i['realurl'] = icon.find('./{%s}url' % ns).text i['url'] = self.make_fullyqualified( i['realurl']).decode('utf-8') self.icons.append(i) self.debug('adding icon %r for %r' % ( i, self.friendly_name)) except Exception as e: import traceback self.debug(traceback.format_exc()) self.warning( 'device %r seems to have an invalid icon description, ' 'ignoring that icon [error: %r]' % ( self.friendly_name, e)) serviceList = d.find('./{%s}serviceList' % ns) if serviceList is not None: for service in serviceList.findall('./{%s}service' % ns): serviceType = service.findtext('{%s}serviceType' % ns) serviceId = service.findtext('{%s}serviceId' % ns) controlUrl = service.findtext('{%s}controlURL' % ns) eventSubUrl = service.findtext('{%s}eventSubURL' % ns) presentationUrl = service.findtext('{%s}presentationURL' % ns) scpdUrl = service.findtext('{%s}SCPDURL' % ns) """ check if values are somehow reasonable """ if len(scpdUrl) == 0: self.warning('service has no uri for its description') continue if len(eventSubUrl) == 0: self.warning('service has no uri for eventing') continue if len(controlUrl) == 0: self.warning('service has no uri for controling') continue try: self.add_service( Service(serviceType, serviceId, self.get_location(), controlUrl, eventSubUrl, presentationUrl, scpdUrl, self)) except Exception as e: self.error( 'Error on adding service: {} [ERROR: {}]'.format( service, e)) # now look for all sub devices embedded_devices = d.find('./{%s}deviceList' % ns) if embedded_devices is not None: for d in embedded_devices.findall('./{%s}device' % ns): embedded_device = Device(self) self.add_device(embedded_device) embedded_device.parse_device(d) self.receiver()
[docs] def get_location(self): return self.parent.get_location()
[docs] def get_usn(self): return self.parent.get_usn()
[docs] def get_upnp_version(self): return self.parent.get_upnp_version()
[docs] def get_urlbase(self): return self.parent.get_urlbase()
[docs] def get_presentation_url(self): try: return self.make_fullyqualified(self.presentation_url) except Exception: return ''
[docs] def get_parent_id(self): try: return self.parent.get_id() except Exception: return ''
[docs] def make_fullyqualified(self, url): return self.parent.make_fullyqualified(url)
[docs] def as_tuples(self): r = [] def append(name, attribute): try: if isinstance(attribute, tuple): if callable(attribute[0]): v1 = attribute[0]() else: v1 = getattr(self, attribute[0]) if v1 in [None, 'None']: return if callable(attribute[1]): v2 = attribute[1]() else: v2 = getattr(self, attribute[1]) if v2 in [None, 'None']: return r.append((name, (v1, v2))) return elif callable(attribute): v = attribute() else: v = getattr(self, attribute) if v not in [None, 'None']: r.append((name, v)) except Exception as e: self.error('Device.as_tuples: %r' % e) import traceback self.debug(traceback.format_exc()) try: r.append(('Location', (self.get_location(), self.get_location()))) except Exception: pass try: append('URL base', self.get_urlbase) except Exception: pass try: r.append(('UDN', self.get_id())) except Exception: pass try: r.append(('Type', self.device_type)) except Exception: pass try: r.append(('UPnP Version', self.upnp_version)) except Exception: pass try: r.append(('DLNA Device Class', ','.join(self.dlna_dc))) except Exception: pass try: r.append(('DLNA Device Capability', ','.join(self.dlna_cap))) except Exception: pass try: r.append(('Friendly Name', self.friendly_name)) except Exception: pass try: append('Manufacturer', 'manufacturer') except Exception: pass try: append('Manufacturer URL', ('manufacturer_url', 'manufacturer_url')) except Exception: pass try: append('Model Description', 'model_description') except Exception: pass try: append('Model Name', 'model_name') except Exception: pass try: append('Model Number', 'model_number') except Exception: pass try: append('Model URL', ('model_url', 'model_url')) except Exception: pass try: append('Serial Number', 'serial_number') except Exception: pass try: append('UPC', 'upc') except Exception: pass try: append('Presentation URL', ('presentation_url', lambda: self.make_fullyqualified( getattr(self, 'presentation_url')))) except Exception: pass for icon in self.icons: r.append(('Icon', (icon['realurl'], self.make_fullyqualified(icon['realurl']), {'Mimetype': icon['mimetype'], 'Width': icon['width'], 'Height': icon['height'], 'Depth': icon['depth']}))) return r
[docs]class RootDevice(Device): def __init__(self, infos): self.usn = infos['USN'] self.udn = infos.get('UDN', None) self.server = infos['SERVER'] self.st = infos['ST'] self.location = infos['LOCATION'] self.manifestation = infos['MANIFESTATION'] self.host = infos['HOST'] self.root_detection_completed = False Device.__init__(self, None) louie.connect( self.device_detect, 'Coherence.UPnP.Device.detection_completed', self) # we need to handle root device completion # these events could be ourself or our children. self.parse_description() self.debug('RootDevice initialized: %r' % self.location) def __repr__(self): return "rootdevice %r %r %r %r, manifestation %r" % \ (self.friendly_name, self.udn, self.st, self.host, self.manifestation)
[docs] def remove(self, *args): result = Device.remove(self, *args) louie.send('Coherence.UPnP.RootDevice.removed', self, usn=self.get_usn()) return result
[docs] def get_usn(self): return self.usn
[docs] def get_st(self): return self.st
[docs] def get_location(self): return self.location if isinstance(self.location, bytes) else \ self.location.encode('ascii') if self.location else None
[docs] def get_upnp_version(self): return self.upnp_version
[docs] def get_urlbase(self): return self.urlbase if isinstance(self.urlbase, bytes) else \ self.urlbase.encode('ascii') if self.urlbase else None
[docs] def get_host(self): return self.host
[docs] def is_local(self): if self.manifestation == 'local': return True return False
[docs] def is_remote(self): if self.manifestation != 'local': return True return False
[docs] def device_detect(self, *args, **kwargs): self.debug("device_detect %r", kwargs) self.debug("root_detection_completed %r", self.root_detection_completed) if self.root_detection_completed: return # our self is not complete yet self.debug("detection_completed %r", self.detection_completed) if not self.detection_completed: return # now check child devices. self.debug("self.devices %r", self.devices) for d in self.devices: self.debug("check device %r %r", d.detection_completed, d) if not d.detection_completed: return # now must be done, so notify root done self.root_detection_completed = True self.info("rootdevice %r %r %r initialized, manifestation %r", self.friendly_name, self.st, self.host, self.manifestation) louie.send('Coherence.UPnP.RootDevice.detection_completed', None, device=self)
[docs] def add_device(self, device): self.debug("RootDevice add_device %r", device) self.devices.append(device)
[docs] def get_devices(self): self.debug("RootDevice get_devices: %s", self.devices) return self.devices
[docs] def parse_description(self): def gotPage(x): self.debug("got device description from %r", self.location) self.debug("data is %r", x) data, headers = x xml_data = None try: xml_data = etree.fromstring(data) except Exception: self.warning("Invalid device description received from %r", self.location) import traceback self.debug(traceback.format_exc()) if xml_data is not None: tree = xml_data major = tree.findtext('./{%s}specVersion/{%s}major' % (ns, ns)) minor = tree.findtext('./{%s}specVersion/{%s}minor' % (ns, ns)) try: self.upnp_version = '.'.join((major, minor)) except Exception: self.upnp_version = 'n/a' try: self.urlbase = tree.findtext('./{%s}URLBase' % ns) except Exception: import traceback self.debug(traceback.format_exc()) d = tree.find('./{%s}device' % ns) if d is not None: self.parse_device(d) # root device self.debug("device parsed successfully %r", self.location) def gotError(failure, url): self.warning("error getting device description from %r", url) self.info(failure) try: utils.getPage( self.location).addCallbacks( gotPage, gotError, None, None, [self.location], None) except Exception as e: self.error('Error on parsing device description: {}'.format(e))
[docs] def make_fullyqualified(self, url): '''Be aware that this function returns a byte string''' self.info('make_fullyqualified: {} [{}]'.format(url, type(url))) if isinstance(url, str): url = url.encode('ascii') if url.startswith(b'http://'): return url from urllib.parse import urljoin base = self.get_urlbase() if isinstance(base, str): base = base.encode('ascii') if base is not None: if base[-1] != b'/': base += b'/' r = urljoin(base, url) else: loc = self.get_location() if isinstance(loc, str): loc = loc.encode('ascii') r = urljoin(loc, url) return r