playful 0.1.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/.gemtest +0 -0
  3. data/.gitignore +19 -0
  4. data/.rspec +1 -0
  5. data/.travis.yml +5 -0
  6. data/Gemfile +6 -0
  7. data/History.rdoc +3 -0
  8. data/LICENSE.rdoc +22 -0
  9. data/README.rdoc +194 -0
  10. data/Rakefile +20 -0
  11. data/features/control_point.feature +13 -0
  12. data/features/device.feature +22 -0
  13. data/features/device_discovery.feature +9 -0
  14. data/features/step_definitions/control_point_steps.rb +19 -0
  15. data/features/step_definitions/device_discovery_steps.rb +40 -0
  16. data/features/step_definitions/device_steps.rb +28 -0
  17. data/features/support/common.rb +9 -0
  18. data/features/support/env.rb +17 -0
  19. data/features/support/fake_upnp_device_collection.rb +108 -0
  20. data/features/support/world_extensions.rb +15 -0
  21. data/lib/core_ext/hash_patch.rb +5 -0
  22. data/lib/core_ext/socket_patch.rb +16 -0
  23. data/lib/core_ext/to_upnp_s.rb +65 -0
  24. data/lib/playful.rb +5 -0
  25. data/lib/playful/control_point.rb +175 -0
  26. data/lib/playful/control_point/base.rb +74 -0
  27. data/lib/playful/control_point/device.rb +511 -0
  28. data/lib/playful/control_point/error.rb +13 -0
  29. data/lib/playful/control_point/service.rb +404 -0
  30. data/lib/playful/device.rb +28 -0
  31. data/lib/playful/logger.rb +8 -0
  32. data/lib/playful/ssdp.rb +195 -0
  33. data/lib/playful/ssdp/broadcast_searcher.rb +114 -0
  34. data/lib/playful/ssdp/error.rb +6 -0
  35. data/lib/playful/ssdp/listener.rb +38 -0
  36. data/lib/playful/ssdp/multicast_connection.rb +112 -0
  37. data/lib/playful/ssdp/network_constants.rb +17 -0
  38. data/lib/playful/ssdp/notifier.rb +41 -0
  39. data/lib/playful/ssdp/searcher.rb +87 -0
  40. data/lib/playful/version.rb +3 -0
  41. data/lib/rack/upnp_control_point.rb +70 -0
  42. data/playful.gemspec +38 -0
  43. data/spec/spec_helper.rb +16 -0
  44. data/spec/support/search_responses.rb +134 -0
  45. data/spec/unit/core_ext/to_upnp_s_spec.rb +105 -0
  46. data/spec/unit/playful/control_point/device_spec.rb +7 -0
  47. data/spec/unit/playful/control_point_spec.rb +45 -0
  48. data/spec/unit/playful/ssdp/listener_spec.rb +29 -0
  49. data/spec/unit/playful/ssdp/multicast_connection_spec.rb +157 -0
  50. data/spec/unit/playful/ssdp/notifier_spec.rb +76 -0
  51. data/spec/unit/playful/ssdp/searcher_spec.rb +110 -0
  52. data/spec/unit/playful/ssdp_spec.rb +214 -0
  53. data/tasks/control_point.html +30 -0
  54. data/tasks/control_point.thor +43 -0
  55. data/tasks/search.thor +128 -0
  56. data/tasks/test_js/FABridge.js +1425 -0
  57. data/tasks/test_js/WebSocketMain.swf +807 -0
  58. data/tasks/test_js/swfobject.js +825 -0
  59. data/tasks/test_js/web_socket.js +1133 -0
  60. data/test/test_ssdp.rb +298 -0
  61. data/test/test_ssdp_notification.rb +74 -0
  62. data/test/test_ssdp_response.rb +31 -0
  63. data/test/test_ssdp_search.rb +23 -0
  64. metadata +339 -0
@@ -0,0 +1,511 @@
1
+ require_relative 'base'
2
+ require_relative 'service'
3
+ require_relative 'error'
4
+ require 'uri'
5
+ require 'eventmachine'
6
+ require 'time'
7
+
8
+
9
+ module Playful
10
+ class ControlPoint
11
+ class Device < Base
12
+ include EM::Deferrable
13
+ include LogSwitch::Mixin
14
+
15
+ attr_reader :ssdp_notification
16
+
17
+ #vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
18
+ # Passed in as part of +device_info+; given by the SSDP search response.
19
+ #
20
+ attr_reader :cache_control
21
+ attr_reader :date
22
+ attr_reader :ext
23
+ attr_reader :location
24
+ attr_reader :server
25
+ attr_reader :st
26
+ attr_reader :usn
27
+ #
28
+ # DONE +device_info+
29
+ #^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
30
+
31
+ # @return [Hash] The whole parsed description... just in case.
32
+ attr_reader :description
33
+
34
+ attr_reader :expiration
35
+
36
+ #vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
37
+ # Determined by device description file.
38
+ #
39
+
40
+ #------- <root> elements -------
41
+
42
+ # @return [String] The xmlns attribute of the device from the description file.
43
+ attr_reader :xmlns
44
+
45
+ # @return [Hash] :major and :minor revisions of the UPnP spec this device adheres to.
46
+ attr_reader :spec_version
47
+
48
+ # @return [String] URLBase from the device's description file.
49
+ attr_reader :url_base
50
+
51
+ #------- <root><device> elements -------
52
+
53
+ # @return [String] The type of UPnP device (URN) from the description file.
54
+ attr_reader :device_type
55
+
56
+ # @return [String] Short device description for the end user.
57
+ attr_reader :friendly_name
58
+
59
+ # @return [String] Manufacturer's name.
60
+ attr_reader :manufacturer
61
+
62
+ # @return [String] Manufacturer's web site.
63
+ attr_reader :manufacturer_url
64
+
65
+ # @return [String] Long model description for the end user, from the description file.
66
+ attr_reader :model_description
67
+
68
+ # @return [String] Model name of this device from the description file.
69
+ attr_reader :model_name
70
+
71
+ # @return [String] Model number of this device from the description file.
72
+ attr_reader :model_number
73
+
74
+ # @return [String] Web site for model of this device.
75
+ attr_reader :model_url
76
+
77
+ # @return [String] The serial number from the description file.
78
+ attr_reader :serial_number
79
+
80
+ # @return [String] The UDN for the device, from the description file.
81
+ attr_reader :udn
82
+
83
+ # @return [String] The UPC of the device from the description file.
84
+ attr_reader :upc
85
+
86
+ # @return [Array<Hash>] An Array where each element is a Hash that describes an icon.
87
+ attr_reader :icon_list
88
+
89
+ # Services provided directly by this device.
90
+ attr_reader :service_list
91
+
92
+ # Devices embedded directly into this device.
93
+ attr_reader :device_list
94
+
95
+ # @return [String] URL for device control via a browser.
96
+ attr_reader :presentation_url
97
+
98
+ #
99
+ # DONE description file
100
+ #^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
101
+
102
+ # @param [Hash] device_info
103
+ # @option device_info [Hash] ssdp_notification
104
+ # @option device_info [Hash] device_description
105
+ # @option device_info [Hash] parent_base_url
106
+ def initialize(device_info)
107
+ super()
108
+
109
+ @device_info = device_info
110
+ log "Got device info: #{@device_info}"
111
+ @device_list = []
112
+ @service_list = []
113
+ @icon_list = []
114
+ @xmlns = ''
115
+ @done_creating_devices = false
116
+ @done_creating_services = false
117
+ end
118
+
119
+ def fetch
120
+ description_getter = EventMachine::DefaultDeferrable.new
121
+
122
+ description_getter.errback do
123
+ msg = 'Failed getting description.'
124
+ log msg, :error
125
+ @done_creating_devices = true
126
+ @done_creating_services = true
127
+ set_deferred_status(:failed, msg)
128
+
129
+ if ControlPoint.raise_on_remote_error
130
+ raise ControlPoint::Error, msg
131
+ end
132
+ end
133
+
134
+ if @device_info.has_key? :ssdp_notification
135
+ extract_from_ssdp_notification(description_getter)
136
+ elsif @device_info.has_key? :device_description
137
+ log 'Creating device from device description file info.'
138
+ description_getter.set_deferred_success @device_info[:device_description]
139
+ else
140
+ log "Not sure what to extract from this device's info."
141
+ description_getter.set_deferred_failure
142
+ end
143
+
144
+ description_getter.callback do |description|
145
+ log "Description received from #{description_getter.object_id}"
146
+ @description = description
147
+
148
+ if @description.nil?
149
+ log 'Description is empty.', :error
150
+ set_deferred_status(:failed, 'Got back an empty description...')
151
+ return
152
+ end
153
+
154
+ extract_spec_version
155
+
156
+ @url_base = extract_url_base
157
+ log "Set url_base to #{@url_base}"
158
+
159
+ if @device_info[:ssdp_notification]
160
+ @xmlns = @description[:root][:@xmlns]
161
+ log "Extracting description for root device #{description_getter.object_id}"
162
+ extract_description(@description[:root][:device])
163
+ elsif @device_info.has_key? :device_description
164
+ log "Extracting description for non-root device #{description_getter.object_id}"
165
+ extract_description(@description)
166
+ end
167
+ end
168
+
169
+ tickloop = EM.tick_loop do
170
+ if @done_creating_devices && @done_creating_services
171
+ log 'All done creating stuff'
172
+ :stop
173
+ end
174
+ end
175
+
176
+ tickloop.on_stop { set_deferred_status :succeeded, self }
177
+ end
178
+
179
+ def extract_from_ssdp_notification(callback)
180
+ log 'Creating device from SSDP Notification info.'
181
+ @ssdp_notification = @device_info[:ssdp_notification]
182
+
183
+ @cache_control = @ssdp_notification[:cache_control]
184
+ @location = ssdp_notification[:location]
185
+ @server = ssdp_notification[:server]
186
+ @st = ssdp_notification[:st] || ssdp_notification[:nt]
187
+ @ext = ssdp_notification.has_key?(:ext) ? true : false
188
+ @usn = ssdp_notification[:usn]
189
+ @date = ssdp_notification[:date] || ''
190
+ @expiration = if @date.empty?
191
+ Time.now + @cache_control.match(/\d+/)[0].to_i
192
+ else
193
+ Time.at(Time.parse(@date).to_i + @cache_control.match(/\d+/)[0].to_i)
194
+ end
195
+
196
+ if @location
197
+ get_description(@location, callback)
198
+ else
199
+ message = 'M-SEARCH response is either missing the Location header or has an empty value.'
200
+ message << "Response: #{@ssdp_notification}"
201
+
202
+ if ControlPoint.raise_on_remote_error
203
+ raise ControlPoint::Error, message
204
+ end
205
+ end
206
+ end
207
+
208
+ def extract_url_base
209
+ if @description[:root] && @description[:root][:URLBase]
210
+ @description[:root][:URLBase]
211
+ elsif @device_info[:parent_base_url]
212
+ @device_info[:parent_base_url]
213
+ else
214
+ tmp_uri = URI(@location)
215
+ "#{tmp_uri.scheme}://#{tmp_uri.host}:#{tmp_uri.port}/"
216
+ end
217
+ end
218
+
219
+ # True if the device hasn't received an alive notification since it last
220
+ # told its max age.
221
+ def expired?
222
+ Time.now > @expiration if @expiration
223
+ end
224
+
225
+ def has_devices?
226
+ !@device_list.empty?
227
+ end
228
+
229
+ def has_services?
230
+ !@service_list.empty?
231
+ end
232
+
233
+ def extract_description(ddf)
234
+ log 'Extracting basic attributes from description...'
235
+
236
+ @device_type = ddf[:deviceType] || ''
237
+ @friendly_name = ddf[:friendlyName] || ''
238
+ @manufacturer = ddf[:manufacturer] || ''
239
+ @manufacturer_url = ddf[:manufacturerURL] || ''
240
+ @model_description = ddf[:modelDescription] || ''
241
+ @model_name = ddf[:modelName] || ''
242
+ @model_number = ddf[:modelNumber] || ''
243
+ @model_url = ddf[:modelURL] || ''
244
+ @serial_number = ddf[:serialNumber] || ''
245
+ @udn = ddf[:UDN] || ''
246
+ @upc = ddf[:UPC] || ''
247
+ @icon_list = extract_icons(ddf[:iconList])
248
+ @presentation_url = ddf[:presentationURL] || ''
249
+
250
+ log 'Basic attributes extracted.'
251
+
252
+ start_device_extraction
253
+ start_service_extraction
254
+ end
255
+
256
+ def extract_spec_version
257
+ if @description[:root]
258
+ "#{@description[:root][:specVersion][:major]}.#{@description[:root][:specVersion][:minor]}"
259
+ end
260
+ end
261
+
262
+ def start_service_extraction
263
+ services_extractor = EventMachine::DefaultDeferrable.new
264
+
265
+ if @description[:serviceList]
266
+ log 'Extracting services from non-root device.'
267
+ extract_services(@description[:serviceList], services_extractor)
268
+ elsif @description[:root][:device][:serviceList]
269
+ log 'Extracting services from root device.'
270
+ extract_services(@description[:root][:device][:serviceList], services_extractor)
271
+ end
272
+
273
+ services_extractor.errback do
274
+ msg = 'Failed extracting services.'
275
+ log msg, :error
276
+ @done_creating_services = true
277
+
278
+ if ControlPoint.raise_on_remote_error
279
+ raise ControlPoint::Error, msg
280
+ end
281
+ end
282
+
283
+ services_extractor.callback do |services|
284
+ log 'Done extracting services.'
285
+ @service_list = services
286
+
287
+ log "New service count: #{@service_list.size}."
288
+ @done_creating_services = true
289
+ end
290
+ end
291
+
292
+ def start_device_extraction
293
+ device_extractor = EventMachine::DefaultDeferrable.new
294
+ extract_devices(device_extractor)
295
+
296
+ device_extractor.errback do
297
+ msg = 'Failed extracting device.'
298
+ log msg, :error
299
+ @done_creating_devices = true
300
+
301
+ if ControlPoint.raise_on_remote_error
302
+ raise ControlPoint::Error, msg
303
+ end
304
+ end
305
+
306
+ device_extractor.callback do |device|
307
+ if device
308
+ log "Device extracted from #{device_extractor.object_id}."
309
+ @device_list << device
310
+ else
311
+ log "Device extraction done from #{device_extractor.object_id} but none were extracted."
312
+ end
313
+
314
+ log "Child device size is now: #{@device_list.size}"
315
+ @done_creating_devices = true
316
+ end
317
+ end
318
+
319
+ # @return [Array<Hash>]
320
+ def extract_icons(ddf_icon_list)
321
+ return [] unless ddf_icon_list
322
+ log "Icon list: #{ddf_icon_list}"
323
+
324
+ if ddf_icon_list[:icon].is_a? Array
325
+ ddf_icon_list[:icon].map do |values|
326
+ values[:url] = build_url(@url_base, values[:url])
327
+ values
328
+ end
329
+ else
330
+ [{
331
+ mimetype: ddf_icon_list[:icon][:mimetype],
332
+ width: ddf_icon_list[:icon][:width],
333
+ height: ddf_icon_list[:icon][:height],
334
+ depth: ddf_icon_list[:icon][:depth],
335
+ url: build_url(@url_base, ddf_icon_list[:icon][:url])
336
+ }]
337
+ end
338
+ end
339
+
340
+ def extract_devices(group_device_extractor)
341
+ log "Extracting child devices for #{self.object_id} using #{group_device_extractor.object_id}"
342
+
343
+ device_list_hash = if @description.has_key? :root
344
+ log 'Description has a :root key...'
345
+
346
+ if @description[:root][:device][:deviceList]
347
+ @description[:root][:device][:deviceList][:device]
348
+ else
349
+ log 'No child devices to extract.'
350
+ group_device_extractor.set_deferred_status(:succeeded)
351
+ end
352
+ elsif @description[:deviceList]
353
+ log 'Description does not have a :root key...'
354
+ @description[:deviceList][:device]
355
+ else
356
+ log 'No child devices to extract.'
357
+ group_device_extractor.set_deferred_status(:succeeded)
358
+ end
359
+
360
+ if device_list_hash.nil? || device_list_hash.empty?
361
+ group_device_extractor.set_deferred_status(:succeeded)
362
+ return
363
+ end
364
+
365
+ log "device list: #{device_list_hash}"
366
+
367
+ if device_list_hash.is_a? Array
368
+ EM::Iterator.new(device_list_hash, device_list_hash.count).map(
369
+ proc do |device, iter|
370
+ single_device_extractor = EventMachine::DefaultDeferrable.new
371
+
372
+ single_device_extractor.errback do
373
+ msg = 'Failed extracting device.'
374
+ log msg, :error
375
+
376
+ if ControlPoint.raise_on_remote_error
377
+ raise ControlPoint::Error, msg
378
+ end
379
+ end
380
+
381
+ single_device_extractor.callback do |d|
382
+ iter.return(d)
383
+ end
384
+
385
+ extract_device(device, single_device_extractor)
386
+ end,
387
+ proc do |found_devices|
388
+ group_device_extractor.set_deferred_status(:succeeded, found_devices)
389
+ end
390
+ )
391
+ else
392
+ single_device_extractor = EventMachine::DefaultDeferrable.new
393
+
394
+ single_device_extractor.errback do
395
+ msg = 'Failed extracting device.'
396
+ log msg, :error
397
+ group_device_extractor.set_deferred_status(:failed, msg)
398
+
399
+ if ControlPoint.raise_on_remote_error
400
+ raise ControlPoint::Error, msg
401
+ end
402
+ end
403
+
404
+ single_device_extractor.callback do |device|
405
+ group_device_extractor.set_deferred_status(:succeeded, [device])
406
+ end
407
+
408
+ log 'Extracting single device...'
409
+ extract_device(device_list_hash, group_device_extractor)
410
+ end
411
+ end
412
+
413
+ def extract_device(device, device_extractor)
414
+ deferred_device = Device.new(device_description: device, parent_base_url: @url_base)
415
+
416
+ deferred_device.errback do
417
+ msg = "Couldn't build device!"
418
+ log msg, :error
419
+ device_extractor.set_deferred_status(:failed, msg)
420
+
421
+ if ControlPoint.raise_on_remote_error
422
+ raise ControlPoint::Error, msg
423
+ end
424
+ end
425
+
426
+ deferred_device.callback do |built_device|
427
+ log "Device created: #{built_device.device_type}"
428
+ device_extractor.set_deferred_status(:succeeded, built_device)
429
+ end
430
+
431
+ deferred_device.fetch
432
+ end
433
+
434
+ def extract_services(service_list, group_service_extractor)
435
+ log 'Extracting services...'
436
+
437
+ log "service list: #{service_list}"
438
+ return if service_list.nil?
439
+
440
+ service_list.each_value do |service|
441
+ if service.is_a? Array
442
+ EM::Iterator.new(service, service.count).map(
443
+ proc do |s, iter|
444
+ single_service_extractor = EventMachine::DefaultDeferrable.new
445
+
446
+ single_service_extractor.errback do
447
+ msg = 'Failed to create service.'
448
+ log msg, :error
449
+
450
+ if ControlPoint.raise_on_remote_error
451
+ raise ControlPoint::Error, msg
452
+ end
453
+ end
454
+
455
+ single_service_extractor.callback do |s|
456
+ iter.return(s)
457
+ end
458
+
459
+ extract_service(s, single_service_extractor)
460
+ end,
461
+ proc do |found_services|
462
+ group_service_extractor.set_deferred_status(:succeeded, found_services)
463
+ end
464
+ )
465
+ else
466
+ single_service_extractor = EventMachine::DefaultDeferrable.new
467
+
468
+ single_service_extractor.errback do
469
+ msg = 'Failed to create service.'
470
+ log msg, :error
471
+ group_service_extractor.set_deferred_status :failed, msg
472
+
473
+ if ControlPoint.raise_on_remote_error
474
+ raise ControlPoint::Error, msg
475
+ end
476
+ end
477
+
478
+ single_service_extractor.callback do |s|
479
+ group_service_extractor.set_deferred_status :succeeded, [s]
480
+ end
481
+
482
+ log 'Extracting single service...'
483
+ extract_service(service, single_service_extractor)
484
+ end
485
+ end
486
+ end
487
+
488
+ def extract_service(service, single_service_extractor)
489
+ service_getter = Service.new(@url_base, service)
490
+ log "Extracting service with #{service_getter.object_id}"
491
+
492
+ service_getter.errback do |message|
493
+ msg = "Couldn't build service with info: #{service}"
494
+ log msg, :error
495
+ single_service_extractor.set_deferred_status(:failed, msg)
496
+
497
+ if ControlPoint.raise_on_remote_error
498
+ raise ControlPoint::Error, message
499
+ end
500
+ end
501
+
502
+ service_getter.callback do |built_service|
503
+ log "Service created: #{built_service.service_type}"
504
+ single_service_extractor.set_deferred_status(:succeeded, built_service)
505
+ end
506
+
507
+ service_getter.fetch
508
+ end
509
+ end
510
+ end
511
+ end