1 # Written by Ingar Arntzen
2 # see LICENSE.txt for license information
5 This module implements the SSDP Server deamon,
6 part of the UPnP archictecture.
11 # SSDP Message Configuration
14 'target': "upnp:rootdevice",
15 'usn': "uuid:%(uuid)s::upnp:rootdevice",
18 _DEVICE_CONF_TEMPL_1 = {
19 'target': "uuid:%(uuid)s",
20 'usn': "uuid:%(uuid)s",
23 _DEVICE_CONF_TEMPL_2 = {
24 'target': "urn:%(device_domain)s:device:" + \
25 "%(device_type)s:%(device_version)s",
26 'usn': "uuid:%(uuid)s::urn:%(device_domain)s:device:" + \
27 "%(device_type)s:%(device_version)s",
30 _SERVICE_CONF_TEMPL = {
31 'target': "urn:schemas-upnp-org:service:" + \
32 "%(service_type)s:%(service_version)s",
33 'usn': "uuid:%(uuid)s::urn:schemas-upnp-org:service:" + \
34 "%(service_type)s:%(service_version)s",
39 _REANNOUNCE_AGE = _MAX_AGE * 0.9
40 _SSDP_SERVER_CONFIG = {
41 'max_delay': _MAX_DELAY,
45 _LOG_TAG = "SSDPServer"
48 def _create_msg_config(config_template, kwargs):
49 """Create a single message config dict from a template and
52 'target': config_template['target'] % kwargs,
53 'usn': config_template['usn'] % kwargs
56 def _create_msg_configs(root_device):
57 """Create all message configs for all devices and services."""
58 # Expect rootdevice to be the root of a dictionary hierarchy,
59 # representing the nested organisation of devices and services.
60 # Create 1 special message config for root device
61 configs = [_create_msg_config(_ROOT_CONF_TEMPL, root_device.__dict__)]
62 # Walk the device/service hierarchy (incl. rootdevice)
63 device_queue = [root_device]
65 device = device_queue.pop()
66 # Iterate recursively over all devices (top-down/breath-first)
67 device_queue += device.get_devices()
68 # Create two messages configs per device
69 conf_1 = _create_msg_config(_DEVICE_CONF_TEMPL_1, device.__dict__)
70 conf_2 = _create_msg_config(_DEVICE_CONF_TEMPL_2, device.__dict__)
71 configs += [conf_1, conf_2]
72 # Create one message config per service in device
73 # todo : should really only create one message per service type
74 for service in device.get_services():
75 service.uuid = device.uuid
76 conf = _create_msg_config(_SERVICE_CONF_TEMPL, service.__dict__)
81 def _initialise_message(ssdp_config, msg):
82 """Utility method for initialising SSDP messages with common data."""
84 location=ssdp_config['location'],
85 osversion=ssdp_config['osversion'],
86 productversion = ssdp_config['productversion'],
87 max_age=ssdp_config['max_age']
92 ##############################################
94 ##############################################
96 class SSDPServer(ssdpdaemon.SSDPDaemon):
99 This implements the SSDP server deamon, part of the UPnP architecture.
101 This class is implemented in a non-blocking, event-based manner.
102 Execution is outsourced to the given task_runner.
105 def __init__(self, task_runner, logger=None):
106 ssdpdaemon.SSDPDaemon.__init__(self, task_runner, logger)
109 # Announce Timeout Task
110 self._timeout_task = None
112 self._root_device = None
115 def set_service_manager(self, service_manager):
116 """The service manger initialises SSDPServer with a
117 reference to itself."""
118 self._sm = service_manager
120 ##############################################
121 # PRIVATE PROTOCOL OPERATIONS
122 ##############################################
125 """Extends superclass startup when taskrunner starts."""
126 ssdpdaemon.SSDPDaemon.startup(self)
128 self._root_device = self._sm.get_root_device()
130 self._config = _SSDP_SERVER_CONFIG
131 self._config['location'] = self._sm.get_description_url()
132 self._config['osversion'] = self._sm.get_os_version()
133 self._config['productversion'] = self._sm.get_product_version()
137 ##############################################
139 ##############################################
141 def handle_search(self, msg, sock_addr):
142 """Handles the receipt of a SSDP Search message."""
144 # Create Reply Message
145 reply_message = ssdpmessage.ReplyMessage()
146 _initialise_message(self._config, reply_message)
148 # Decide the number of reply messages, and their configuration.
150 if msg.st == "ssdp:all":
151 # Reply messages for all devices and services
152 configs = _create_msg_configs(self._root_device)
153 elif msg.st == "upnp:rootdevice":
154 # Reply only single special message for root device
155 configs = [_create_msg_config(_ROOT_CONF_TEMPL, \
156 self._root_device.__dict__)]
158 device_type = msg.st.split(':')[-2]
159 self.log("IGNORE %s %s [%s]" % (msg.type,
160 device_type, sock_addr[0]))
163 device_type = msg.st.split(':')[-2]
164 self.log("RECEIVE %s %s [%s]" % (msg.type,
165 device_type, sock_addr[0]))
169 reply_message.st = conf['target']
170 reply_message.usn = conf['usn']
171 self.unicast(reply_message, sock_addr)
173 def _handle_reply(self, msg, sock_addr):
174 """Handles the receipt of a SSDP Reply message."""
175 self.log("IGNORE %s from %s" % (msg.type, sock_addr))
177 def _handle_announce(self, msg, sock_addr):
178 """Handles the receipt of a SSDP Announce message."""
180 self.log("IGNORE %s from %s" % (msg.type, sock_addr))
182 def _handle_unannounce(self, msg, sock_addr):
183 """Handles the receipt of a SSDP UnAnnounce message."""
184 self.log("IGNORE %s from %s" % (msg.type, sock_addr))
186 ##############################################
188 ##############################################
191 """Multicast SSDP announce messages."""
192 # Reset timeout task for next announce
193 if self._timeout_task:
194 self._timeout_task.cancel()
195 # Register new announce timeout
196 self._timeout_task = self.task_runner.add_delay_task(
201 msg = ssdpmessage.AnnounceMessage()
202 _initialise_message(self._config, msg)
203 for conf in _create_msg_configs(self._root_device):
204 msg.nt = conf['target']
205 msg.usn = conf['usn']
208 def unannounce(self):
209 """Multicast SSDP unannounce messages."""
210 msg = ssdpmessage.UnAnnounceMessage()
211 for conf in _create_msg_configs(self._root_device):
212 msg.nt = conf['target']
213 msg.usn = conf['usn']
217 """Close the SSDP Server deamon. Send unannounce messages."""
219 ssdpdaemon.SSDPDaemon.close(self)
222 ##############################################
224 ##############################################
226 if __name__ == '__main__':
229 class MockRootDevice:
230 """Mockup root device."""
232 self.uuid = uuid.uuid1()
233 self.device_domain = "schemas-upnp-org"
234 self.device_type = "Basic"
235 self.device_version = 1
236 def get_devices(self):
237 """Get mock devices."""
239 def get_services(self):
240 """Get mock services."""
243 class MockServiceManager:
244 """Mock up service manager."""
247 def get_root_device(self):
248 """Get mock root device."""
249 return MockRootDevice()
250 def get_description_url(self):
251 """Get mock description URL."""
252 return "http://192.168.1.235:44444/description.xml"
253 def get_os_version(self):
254 """Get mock os version."""
256 def get_product_version(self):
257 """Get mock product version."""
259 def set_ssdp_port(self, port):
264 """MockLogger object."""
267 def log(self, log_tag, msg):
268 """Log to std out."""
272 import BaseLib.UPnP.common.taskrunner as taskrunner
273 TR = taskrunner.TaskRunner()
274 SM = MockServiceManager()
275 SERVER = SSDPServer(TR, logger=MockLogger())
276 SERVER.set_service_manager(SM)
277 TR.add_task(SERVER.startup)
280 except KeyboardInterrupt: