aioupnp/txupnp/gateway.py

218 lines
7.8 KiB
Python
Raw Normal View History

2018-07-29 04:08:24 +02:00
import logging
from twisted.internet import defer
2018-10-01 15:51:31 +02:00
from txupnp.scpd import SCPDCommand, SCPDRequester
from txupnp.util import get_dict_val_case_insensitive, verify_return_types, BASE_PORT_REGEX, BASE_ADDRESS_REGEX
2018-07-29 04:08:24 +02:00
from txupnp.constants import SPEC_VERSION
2018-10-01 15:51:31 +02:00
from txupnp.commands import SCPDCommands
2018-07-29 04:08:24 +02:00
log = logging.getLogger(__name__)
2018-07-31 22:53:08 +02:00
2018-09-25 20:52:29 +02:00
class CaseInsensitive:
2018-07-31 22:53:08 +02:00
def __init__(self, **kwargs):
not_evaluated = {}
for k, v in kwargs.items():
if k.startswith("_"):
not_evaluated[k] = v
continue
try:
getattr(self, k)
setattr(self, k, v)
except AttributeError as err:
not_evaluated[k] = v
if not_evaluated:
2018-08-02 15:38:00 +02:00
log.debug("%s did not apply kwargs: %s", self.__class__.__name__, not_evaluated)
2018-07-31 22:53:08 +02:00
2018-09-25 20:52:29 +02:00
def _get_attr_name(self, case_insensitive: str) -> str:
2018-07-31 22:53:08 +02:00
for k, v in self.__dict__.items():
if k.lower() == case_insensitive.lower():
return k
def __getattr__(self, item):
if item in self.__dict__:
return self.__dict__[item]
for k, v in self.__class__.__dict__.items():
if k.lower() == item.lower():
if k not in self.__dict__:
self.__dict__[k] = v
return v
raise AttributeError(item)
def __setattr__(self, item, value):
if item in self.__dict__:
self.__dict__[item] = value
return
to_update = None
for k, v in self.__dict__.items():
if k.lower() == item.lower():
to_update = k
break
self.__dict__[to_update or item] = value
2018-09-25 20:52:29 +02:00
def as_dict(self) -> dict:
2018-07-29 23:32:14 +02:00
return {
2018-07-31 22:53:08 +02:00
k: v for k, v in self.__dict__.items() if not k.startswith("_") and not callable(v)
2018-07-29 23:32:14 +02:00
}
2018-07-29 04:08:24 +02:00
2018-07-31 22:53:08 +02:00
class Service(CaseInsensitive):
serviceType = None
serviceId = None
controlURL = None
eventSubURL = None
SCPDURL = None
class Device(CaseInsensitive):
serviceList = None
deviceList = None
deviceType = None
friendlyName = None
manufacturer = None
manufacturerURL = None
modelDescription = None
modelName = None
modelNumber = None
modelURL = None
serialNumber = None
udn = None
upc = None
2018-07-31 22:53:08 +02:00
presentationURL = None
iconList = None
def __init__(self, devices, services, **kwargs):
super(Device, self).__init__(**kwargs)
if self.serviceList and "service" in self.serviceList:
new_services = self.serviceList["service"]
if isinstance(new_services, dict):
new_services = [new_services]
services.extend([Service(**service) for service in new_services])
if self.deviceList:
2018-08-01 00:20:11 +02:00
for kw in self.deviceList.values():
if isinstance(kw, dict):
d = Device(devices, services, **kw)
devices.append(d)
2018-08-01 00:41:20 +02:00
elif isinstance(kw, list):
for _inner_kw in kw:
d = Device(devices, services, **_inner_kw)
2018-08-01 00:20:11 +02:00
devices.append(d)
2018-08-01 00:41:20 +02:00
else:
log.warning("failed to parse device:\n%s", kw)
2018-07-29 23:32:14 +02:00
2018-07-29 04:08:24 +02:00
2018-09-25 20:52:29 +02:00
class Gateway:
2018-10-01 15:51:31 +02:00
def __init__(self, reactor, **kwargs):
2018-07-31 22:53:08 +02:00
flattened = {
k.lower(): v for k, v in kwargs.items()
}
usn = flattened["usn"]
server = flattened["server"]
location = flattened["location"]
st = flattened["st"]
2018-07-29 04:08:24 +02:00
2018-07-31 22:53:08 +02:00
cache_control = flattened.get("cache_control") or flattened.get("cache-control") or ""
date = flattened.get("date", "")
ext = flattened.get("ext", "")
2018-07-29 04:08:24 +02:00
self.usn = usn.encode()
self.ext = ext.encode()
self.server = server.encode()
self.location = location.encode()
self.cache_control = cache_control.encode()
self.date = date.encode()
self.urn = st.encode()
2018-07-31 22:53:08 +02:00
2018-10-01 15:51:31 +02:00
self._xml_response = ""
self._service_descriptors = {}
2018-07-29 04:08:24 +02:00
self.base_address = BASE_ADDRESS_REGEX.findall(self.location)[0]
self.port = int(BASE_PORT_REGEX.findall(self.location)[0])
2018-07-31 22:53:08 +02:00
self.spec_version = None
self.url_base = None
2018-07-29 04:08:24 +02:00
2018-07-31 22:53:08 +02:00
self._device = None
self._devices = []
self._services = []
2018-10-01 15:51:31 +02:00
self._reactor = reactor
self._unsupported_actions = {}
self._registered_commands = {}
self.commands = SCPDCommands()
self.requester = SCPDRequester(self._reactor)
def as_dict(self) -> dict:
2018-07-31 22:53:08 +02:00
r = {
2018-10-01 15:51:31 +02:00
'server': self.server.decode(),
2018-07-31 22:53:08 +02:00
'urlBase': self.url_base,
2018-10-01 15:51:31 +02:00
'location': self.location.decode(),
2018-07-31 22:53:08 +02:00
"specVersion": self.spec_version,
2018-10-01 15:51:31 +02:00
'usn': self.usn.decode(),
'urn': self.urn.decode(),
2018-07-30 23:48:20 +02:00
}
2018-07-31 22:53:08 +02:00
return r
2018-07-29 23:32:14 +02:00
2018-07-29 04:08:24 +02:00
@defer.inlineCallbacks
2018-10-01 15:51:31 +02:00
def discover_commands(self):
response = yield self.requester.scpd_get(self.location.decode().split(self.base_address.decode())[1], self.base_address.decode(), self.port)
self.spec_version = get_dict_val_case_insensitive(response, SPEC_VERSION)
self.url_base = get_dict_val_case_insensitive(response, "urlbase")
if not self.url_base:
self.url_base = self.base_address.decode()
if response:
2018-07-31 22:53:08 +02:00
self._device = Device(
2018-10-01 15:51:31 +02:00
self._devices, self._services, **get_dict_val_case_insensitive(response, "device")
2018-07-31 22:53:08 +02:00
)
else:
self._device = Device(self._devices, self._services)
2018-10-01 15:51:31 +02:00
for service_type in self.services.keys():
service = self.services[service_type]
yield self.register_commands(service)
@defer.inlineCallbacks
def register_commands(self, service: Service):
try:
action_list = yield self.requester.scpd_get_supported_actions(service, self.base_address.decode(), self.port)
except Exception as err:
log.exception("failed to register service %s: %s", service.serviceType, str(err))
return
for name, inputs, outputs in action_list:
try:
command = SCPDCommand(self.requester, self.base_address, self.port,
service.controlURL.encode(),
service.serviceType.encode(), name, inputs, outputs)
current = getattr(self.commands, command.method)
if hasattr(current, "_return_types"):
command._process_result = verify_return_types(*current._return_types)(command._process_result)
setattr(command, "__doc__", current.__doc__)
setattr(self.commands, command.method, command)
self._registered_commands[command.method] = service.serviceType
log.debug("registered %s::%s", service.serviceType, command.method)
except AttributeError:
s = self._unsupported_actions.get(service.serviceType, [])
s.append(name)
self._unsupported_actions[service.serviceType] = s
log.debug("available command for %s does not have a wrapper implemented: %s %s %s",
service.serviceType, name, inputs, outputs)
2018-07-29 04:08:24 +02:00
@property
2018-09-25 20:52:29 +02:00
def services(self) -> dict:
2018-07-29 04:08:24 +02:00
if not self._device:
return {}
2018-07-31 22:53:08 +02:00
return {service.serviceType: service for service in self._services}
2018-07-29 04:08:24 +02:00
@property
2018-09-25 20:52:29 +02:00
def devices(self) -> dict:
2018-07-29 04:08:24 +02:00
if not self._device:
return {}
2018-07-31 22:53:08 +02:00
return {device.udn: device for device in self._devices}
2018-07-29 04:08:24 +02:00
2018-10-01 15:51:31 +02:00
def get_service(self, service_type: str) -> Service:
2018-07-31 22:53:08 +02:00
for service in self._services:
if service.serviceType.lower() == service_type.lower():
2018-07-29 04:08:24 +02:00
return service
2018-10-01 15:51:31 +02:00
def debug_commands(self):
return {
'available': self._registered_commands,
'failed': self._unsupported_actions
}