playful 0.1.0.alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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