1 # Written by Ingar Arntzen, Norut
2 # see LICENSE.txt for license information
5 This module implements a UPnP Service.
7 This involves a base class intended for development of new services.
8 The baseclass hides complexity related to producing UPnP Service
9 description in both XML and HTML format. It also hides complexity
10 related to the placement of a service within a device hierarchy.
15 import BaseLib.UPnP.common.upnpmarshal as upnpmarshal
17 class ActionError (exceptions.Exception):
18 """Error associated with invoking actions on a UPnP Server. """
21 ##############################################
23 ##############################################
25 _SERVICE_DESCRIPTION_FMT = """<?xml version="1.0"?>
26 <scpd xmlns="urn:schemas-upnp-org:service-1-0">
34 %s</serviceStateTable>
37 _ACTION_FMT = """<action>
45 _ARGUMENT_FMT = """<argument>
47 <relatedStateVariable>%s</relatedStateVariable>
48 <direction>%s</direction>
51 _EVENTED_VARIABLE_FMT = """<stateVariable sendEvents="yes">
53 <dataType>%s</dataType>
54 <defaultValue>%s</defaultValue>
58 _ARG_VARIABLE_FMT = """<stateVariable sendEvents="no">
60 <dataType>%s</dataType>
64 def _service_description_toxml(service):
65 """This function produces the UPnP XML service description."""
69 for evar in service.get_evented_variables():
70 data_type = upnpmarshal.dumps_data_type(evar.the_type)
71 default_value = upnpmarshal.dumps(evar.default_value)
72 args = (evar.the_name, data_type, default_value)
73 svs_str += _EVENTED_VARIABLE_FMT % args
78 # One state variable per type (event variables of arguments)
79 unique_variables = {} # type : variable name
80 for evar in service.get_evented_variables():
81 if not unique_variables.has_key(evar.the_type):
82 unique_variables[evar.the_type] = evar.the_name
85 for action in service.get_actions():
87 for arg in action.in_arg_list + action.out_arg_list:
89 # Check if argument can be related to event variable
90 if unique_variables.has_key(arg.the_type):
91 related_variable_name = unique_variables[arg.the_type]
94 related_variable_name = "A_ARG_TYPE_%d" % arg_counter
95 unique_variables[arg.the_type] = related_variable_name
97 data_type = upnpmarshal.dumps_data_type(arg.the_type)
98 svs_str += _ARG_VARIABLE_FMT % (related_variable_name,
102 direction = 'in' if isinstance(arg, _InArg) else 'out'
103 args_str += _ARGUMENT_FMT % (arg.the_name,
104 related_variable_name, direction)
106 actions_str += _ACTION_FMT % (action.name, args_str)
108 return _SERVICE_DESCRIPTION_FMT % (actions_str, svs_str)
112 ##############################################
114 ##############################################
119 This implements a base class for all UPnP Services.
121 New services should extend this class.
122 The base class hides complexity related to production
123 of XML service descriptions as well as HTTP descriptions.
124 The base class also hides complexity related to placement
125 in the UPnP device hierarchy.
128 def __init__(self, service_id, service_type, service_version=1):
129 self.service_manager = None
131 self._actions = {} # actionName : Action
132 self._events = {} # eventName : Event
133 self._subs = {} # callbackURL : Subscriptions
136 self.service_type = service_type
137 self.service_version = service_version
138 self.service_id = service_id
141 self.description_path = ""
142 self.control_path = ""
146 def set_service_manager(self, service_manager):
147 """Initialise UPnP service with reference to service manager."""
148 self.service_manager = service_manager
149 self.base_url = self.service_manager.get_base_url()
150 self.description_path = "services/%s/description.xml" % self.service_id
151 self.control_path = "services/%s/control" % self.service_id
152 self.event_path = "services/%s/events" % self.service_id
154 self._logger = self.service_manager.get_logger()
157 """Check if service is valid."""
158 return (self.service_type != None and self.service_id != None
159 and self.base_url != None and self.service_manager != None)
161 def get_short_service_id(self):
162 """Return short service id."""
163 return self.service_id
165 def get_service_id(self):
166 """Return full service id."""
167 return "urn:upnp-org:serviceId:%s" % self.service_id
169 def get_service_type(self):
170 """Return service type."""
171 fmt = "urn:schemas-upnp-org:service:%s:%s"
172 return fmt % (self.service_type, self.service_version)
174 def get_xml_description(self):
175 """Returns xml description of service."""
176 return _service_description_toxml(self)
179 """Close UPnP service safely."""
180 for sub in self._subs.values():
183 ##############################################
185 ##############################################
190 self._logger.log("SERVICE", "%s %s" % (self.service_id, msg))
192 ##############################################
193 # SUBSCRIBE / NOTIFY API
194 ##############################################
196 def notify(self, evented_variables):
197 """Notify all subscribers of updated event variables."""
198 self._remove_expired_subscriptions()
199 # Dispatch Event Messages to all subscribers
200 # of the given serviceid.
201 # Make sure all stateVariables are evented variables.
202 for sub in self._subs.values():
203 sub.notify(evented_variables)
205 def subscribe(self, callback_urls, requested_duration):
206 """Process new subscription request."""
207 # requested duration == 0 => infinite
208 self._remove_expired_subscriptions()
209 # For the moment, just accept a single callbackUrl
210 # Subscriber defined by callbackUrl
211 callback_url = callback_urls[0]
212 if self._subs.has_key(callback_url):
213 # Subscriber already exists
217 sub = _Subscription(self, callback_url, requested_duration)
218 self._subs[callback_url] = sub
219 # Dispatch Initial Event Message
221 return (sub.sid, sub.duration)
223 def renew(self, sid_str, requested_duration):
224 """Request to renew an existing subscription."""
225 # requested duration == 0 => infinite
226 for sub in self._subs.values():
227 if str(sub.sid) == sid_str:
228 return sub.renew(requested_duration)
231 def unsubscribe(self, sid_str):
232 """Request to unsubscribe an existing subscription."""
234 for sub in self._subs.values():
235 if str(sub.sid) == sid_str:
239 del self._subs[sub.callback_url]
244 def _remove_expired_subscriptions(self):
245 """Scans subscriptions and removes invalidated."""
246 for url, sub in self._subs.items()[:]:
251 ##############################################
253 ##############################################
255 def define_action(self, method, in_args=None, out_args=None,
257 """Define an action that the service implements.
264 action_name = method.__name__
267 # In/Out Args must be tuples of (name, type<?>)
268 in_args = [_InArg(t[0], t[1]) for t in in_args]
269 out_args = [_OutArg(t[0], t[1]) for t in out_args]
270 action = _Action(action_name, method, in_args, out_args)
271 self._actions[action_name] = action
273 def invoke_action(self, action_name, in_args):
274 """Invoke and action that the service implements.
275 Used by httpserver as part of UPnP control interface."""
276 # in_args is assumed to be tuple of (name, data) all unicode string.
278 if not self._actions.has_key(action_name):
279 raise ActionError, "Action Not Supported"
281 action = self._actions[action_name]
282 return action.execute(in_args)
283 except ActionError, why:
286 def get_actions(self):
287 """Returns all actions that the service implements."""
288 return self._actions.values()
291 ##############################################
292 # EVENTED VARIABLE API
293 ##############################################
295 def define_evented_variable(self, event_name, the_type, default_value):
296 """Define an evented variable for the service. Used by subclass."""
297 evar = _EventedVariable(self, event_name, the_type, default_value)
298 self._events[event_name] = evar
301 def get_evented_variable(self, event_name):
302 """Return evented variable given name."""
303 return self._events.get(event_name, None)
305 def get_evented_variables(self):
306 """Return all evented variables defined by the service."""
307 return self._events.values()
309 def set_evented_variables(self, list_):
311 Update a list of state variables at once.
312 Input will be a list of tuples [(eventedVariable, newValue)]
313 The method avoids sending one notification to every subscriber,
314 for each state variable. Instead, a single subscriber receives
315 one eventMessage containing all the updated state Variables
319 changed_variables = []
320 for evar, new_value in list_:
321 changed = evar.set(new_value, notify_ok=False)
323 changed_variables.append(evar)
324 # notify all in one batch
325 self.notify(changed_variables)
328 ##############################################
330 ##############################################
332 class _EventedVariable:
334 """This class defines an evented variable. The class hides
335 complexity related to event notification."""
337 def __init__(self, service, the_name, the_type, default_value):
338 self._service = service
339 self.the_name = the_name
340 if type(the_type) == types.TypeType:
341 self.the_type = the_type
343 msg = "Argument 'the_type' is not actually a python type."
345 self._value = default_value
346 self.default_value = default_value
348 def set(self, new_value, notify_ok=True):
349 """Set a new value for the evented variable. If the value
350 is different from the old value, notifications will be generated."""
351 if type(new_value) != self.the_type:
352 msg = "Argument 'the_type' is not actually a python type."
354 if new_value != self._value:
356 self._value = new_value
359 self._service.notify([self])
364 """Get the value of an evented variable."""
368 ##############################################
370 ##############################################
374 """The class defines an argument by holding a type and
375 and argument name."""
376 def __init__(self, the_name, the_type):
377 self.the_name = the_name
378 self.the_type = the_type
380 class _InArg(_Argument):
381 """The class defines an input argument by holding a type and
382 and argument name."""
385 class _OutArg(_Argument):
386 """The class defines an output argument (result value) by
387 holding a type and and argument name."""
390 ##############################################
392 ##############################################
396 """This class represents an action implemented by the
399 def __init__(self, name, method, in_arg_list, out_arg_list):
402 self.in_arg_list = in_arg_list
403 self.out_arg_list = out_arg_list
405 def execute(self, in_args):
406 """Execute the action."""
407 # in_args is assumed to be tuple of (name, data) all unicode string.
408 # the tuple is supposed to be ordered according to in_arg_list
409 if len(in_args) != len(self.in_arg_list):
410 raise ActionError, "Wrong number of input arguments"
412 for i in range(len(in_args)):
413 name, data = in_args[i]
414 in_arg = self.in_arg_list[i]
415 if name != in_arg.the_name:
416 raise ActionError, "Wrong name/order for input argument"
418 value = upnpmarshal.loads(in_arg.the_type, data)
419 except upnpmarshal.MarshalError, why:
420 raise ActionError, why
421 typed_args.append(value)
425 result = self.method(*typed_args)
426 except TypeError, why:
427 raise ActionError, "Method Execution Failed (%s)" % why
429 # Result is eiter a single value (incl. None) or a tuple of values.
430 # Make it into a list in both cases.
433 elif result == types.TupleType:
434 result = list(result)
438 # Check that result holds the correct number of values
439 if len(result) != len(self.out_arg_list):
440 raise ActionError, "Wrong number of Results"
441 # Check that each value has the correct type
442 # Also convert python type objects to string representations.
443 # Construct out_args list of tuples [(name, data), ...]
445 for i in range(len(result)):
446 out_arg = self.out_arg_list[i]
448 if not isinstance(value, out_arg.the_type):
449 raise ActionError, "Result is wrong type."
452 data = upnpmarshal.dumps(value)
453 except upnpmarshal.MarshalError, why:
454 raise ActionError, why
455 out_args.append((out_arg.the_name, data))
459 ##############################################
461 ##############################################
463 class NotifyError (exceptions.Exception):
464 """Error associated with event notification."""
469 """This class represents a subscription made to the service,
470 for notification whenever one of its evented variables is updated."""
472 def __init__(self, service, callback_url, requested_duration):
473 # requested_duration == 0 implies INFINITE
474 # requested_duration > 0 implies FINITE
475 # requested_duration < 0 not legal
476 self.service = service
477 self.sid = uuid.uuid1()
479 self.callback_url = callback_url
480 self.duration = 1800 # ignore requested_duration
481 self.is_expired = False
483 def notify(self, evented_variables):
484 """Notify this subscriber that given evented variables
485 have been updated."""
487 return False # should not be neccesary
490 # Construct list of tuples [(name, value), ...]
492 for evar in evented_variables:
494 data = upnpmarshal.dumps(evar.get())
495 except upnpmarshal.MarshalError, why:
496 raise NotifyError, why
497 variables.append((evar.the_name, data))
499 # Dispatch Notification
500 edp = self.service.service_manager.get_event_dispatcher()
501 edp.dispatch(self.sid, self.event_key, self.callback_url, variables)
504 def initial_notify(self):
505 """Notify this subscriber of all evented state
506 variables and their values"""
509 # Event Key must be 0
510 if self.event_key != 0:
512 # All Evented Variables
513 evented_variables = self.service.get_evented_variables()
515 for evar in evented_variables:
517 data = upnpmarshal.dumps(evar.get())
518 except upnpmarshal.MarshalError, why:
519 raise NotifyError, why
520 variables.append((evar.the_name, data))
522 # Dispatch Notification
523 edp = self.service.service_manager.get_event_dispatcher()
524 edp.dispatch(self.sid, 0, self.callback_url, variables)
527 def renew(self, requested_duration):
528 """Renew subscription for this subscriber."""
529 self.duration = requested_duration
530 self.is_expired = False
534 """Cancel subscription for this subscriber."""
535 self.is_expired = True
539 """Close this subscription safely."""
542 ##############################################
544 ##############################################
546 if __name__ == '__main__':
548 class MockEventDispatcher:
549 """Mock Event Dispatcher."""
552 def dispatch(self, sid, event_key, callback_url, variables):
554 print "Notify", sid, event_key, callback_url, variables
556 class MockServiceManager:
557 """Mock Service Manager."""
559 self._ed = MockEventDispatcher()
560 def get_event_dispatcher(self):
563 def get_base_url(self):
565 return "http://myhost:44444"
566 def get_logger(self):
570 SM = MockServiceManager()
571 from BaseLib.UPnP.services import SwitchPower
572 SERVICE = SwitchPower('SwitchPower')
573 SERVICE.set_service_manager(SM)
574 print SERVICE.get_xml_description()