rupnp 0.1.0
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.
- data/MIT-LICENSE +34 -0
- data/README.md +45 -0
- data/Rakefile +3 -0
- data/bin/discover +4 -0
- data/lib/rupnp/constants.rb +31 -0
- data/lib/rupnp/control_point.rb +173 -0
- data/lib/rupnp/cp/base.rb +67 -0
- data/lib/rupnp/cp/event_server.rb +52 -0
- data/lib/rupnp/cp/event_subscriber.rb +49 -0
- data/lib/rupnp/cp/remote_device.rb +271 -0
- data/lib/rupnp/cp/remote_service.rb +304 -0
- data/lib/rupnp/discover.rb +55 -0
- data/lib/rupnp/event.rb +32 -0
- data/lib/rupnp/log_mixin.rb +30 -0
- data/lib/rupnp/ssdp/http.rb +45 -0
- data/lib/rupnp/ssdp/listener.rb +45 -0
- data/lib/rupnp/ssdp/msearch_responder.rb +19 -0
- data/lib/rupnp/ssdp/multicast_connection.rb +48 -0
- data/lib/rupnp/ssdp/notifier.rb +93 -0
- data/lib/rupnp/ssdp/search_responder.rb +115 -0
- data/lib/rupnp/ssdp/searcher.rb +69 -0
- data/lib/rupnp/ssdp/usearch_responder.rb +29 -0
- data/lib/rupnp/ssdp.rb +47 -0
- data/lib/rupnp/tools.rb +66 -0
- data/lib/rupnp.rb +62 -0
- data/spec/spec_helper.rb +70 -0
- data/spec/ssdp/listener_spec.rb +102 -0
- data/spec/ssdp/notifier_spec.rb +61 -0
- data/spec/ssdp/searcher_spec.rb +53 -0
- data/tasks/gem.rake +39 -0
- data/tasks/spec.rake +3 -0
- data/tasks/yard.rake +6 -0
- metadata +209 -0
@@ -0,0 +1,271 @@
|
|
1
|
+
require 'nori'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
|
5
|
+
module RUPNP
|
6
|
+
|
7
|
+
# A device is a UPnP service provider.
|
8
|
+
# @author Sylvain Daubert.
|
9
|
+
class CP::RemoteDevice < CP::Base
|
10
|
+
# Get control point which controls this device
|
11
|
+
# @return [ControlPoint]
|
12
|
+
attr_reader :control_point
|
13
|
+
|
14
|
+
# Get search target.
|
15
|
+
# @return [String]
|
16
|
+
attr_reader :st
|
17
|
+
# Get Unique Service Name
|
18
|
+
# @return [String]
|
19
|
+
attr_reader :usn
|
20
|
+
# Get SERVER string
|
21
|
+
# @return [String]
|
22
|
+
attr_reader :server
|
23
|
+
# URL to the UPnP description of the root device
|
24
|
+
# @return [String]
|
25
|
+
attr_reader :location
|
26
|
+
# @return [String]
|
27
|
+
attr_reader :ext
|
28
|
+
# Date when response was generated
|
29
|
+
# @return [String]
|
30
|
+
attr_reader :date
|
31
|
+
# Contains +max-age+ directive used to specify advertisement validity
|
32
|
+
# @return [String]
|
33
|
+
attr_reader :cache_control
|
34
|
+
# Expiration time for the advertisement
|
35
|
+
# @return [Time]
|
36
|
+
attr_reader :expiration
|
37
|
+
|
38
|
+
# UPnP version used by the device
|
39
|
+
# @return [String]
|
40
|
+
attr_reader :upnp_version
|
41
|
+
# XML namespace for device description
|
42
|
+
# @return [String]
|
43
|
+
attr_reader :xmlns
|
44
|
+
# URL base for device access
|
45
|
+
# @return [String]
|
46
|
+
attr_reader :url_base
|
47
|
+
# Device type
|
48
|
+
# @return [String]
|
49
|
+
attr_reader :type
|
50
|
+
# Short description for end users
|
51
|
+
# @return [String]
|
52
|
+
attr_reader :friendly_name
|
53
|
+
# Manufacturer's name
|
54
|
+
# @return [String]
|
55
|
+
attr_reader :manufacturer
|
56
|
+
# Web site for manufacturer
|
57
|
+
# @return [String]
|
58
|
+
attr_reader :manufacturer_url
|
59
|
+
# Long decription for end user
|
60
|
+
# @return [String]
|
61
|
+
attr_reader :model_description
|
62
|
+
# Model name
|
63
|
+
# @return [String]
|
64
|
+
attr_reader :model_name
|
65
|
+
# Model number
|
66
|
+
# @return [String]
|
67
|
+
attr_reader :model_number
|
68
|
+
# Web site for model
|
69
|
+
# @return [String]
|
70
|
+
attr_reader :model_url
|
71
|
+
# Serial number
|
72
|
+
# @return [String]
|
73
|
+
attr_reader :serial_umber
|
74
|
+
# Unique Device Name
|
75
|
+
# @return [String]
|
76
|
+
attr_reader :udn
|
77
|
+
# Universal Product Code
|
78
|
+
# @return [String]
|
79
|
+
attr_reader :upc
|
80
|
+
# URL to presentation for device
|
81
|
+
# @return [String]
|
82
|
+
attr_reader :presentation_url
|
83
|
+
# Array of icons to depict device in control point UI
|
84
|
+
# @return [Array<OpenStruct>]
|
85
|
+
attr_reader :icons
|
86
|
+
# List of device's services
|
87
|
+
# @return [Array<Service>]
|
88
|
+
attr_reader :services
|
89
|
+
# List of embedded devices
|
90
|
+
# @return [Array<Device>]
|
91
|
+
attr_reader :devices
|
92
|
+
|
93
|
+
|
94
|
+
# @param [ControlPoint] control_point
|
95
|
+
# @param [Hash] notification
|
96
|
+
def initialize(control_point, notification)
|
97
|
+
super()
|
98
|
+
@control_point = control_point
|
99
|
+
@notification = notification
|
100
|
+
|
101
|
+
@icons = []
|
102
|
+
@services = []
|
103
|
+
@devices = []
|
104
|
+
end
|
105
|
+
|
106
|
+
# Get device from its description
|
107
|
+
# @return [void]
|
108
|
+
def fetch
|
109
|
+
description_getter = EM::DefaultDeferrable.new
|
110
|
+
|
111
|
+
description_getter.errback do
|
112
|
+
msg = "Failed getting description"
|
113
|
+
log :error, "Fetching device: #{msg}"
|
114
|
+
fail self, msg
|
115
|
+
end
|
116
|
+
|
117
|
+
extract_from_ssdp_notification description_getter
|
118
|
+
|
119
|
+
description_getter.callback do |description|
|
120
|
+
@description = description
|
121
|
+
unless description
|
122
|
+
fail self, 'Blank description returned'
|
123
|
+
next
|
124
|
+
end
|
125
|
+
|
126
|
+
if bad_description?
|
127
|
+
fail self, "Bad description returned: #@description"
|
128
|
+
next
|
129
|
+
end
|
130
|
+
|
131
|
+
extract_url_base
|
132
|
+
extract_device_info
|
133
|
+
extract_icons
|
134
|
+
|
135
|
+
@services_extracted = @devices_extracted = false
|
136
|
+
extract_services
|
137
|
+
extract_devices
|
138
|
+
|
139
|
+
tick_loop = EM.tick_loop do
|
140
|
+
:stop if @services_extracted and @devices_extracted
|
141
|
+
end
|
142
|
+
tick_loop.on_stop { succeed self }
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
|
150
|
+
def extract_from_ssdp_notification(getter)
|
151
|
+
@st = @notification['st']
|
152
|
+
@usn = @notification['usn']
|
153
|
+
@server = @notification['server']
|
154
|
+
@location = @notification['location']
|
155
|
+
@ext = @notification['ext']
|
156
|
+
@date = @notification['date'] || ''
|
157
|
+
@cache_control = @notification['cache-control'] || ''
|
158
|
+
|
159
|
+
max_age = @cache_control.match(/max-age\s*=\s*(\d+)/)[1].to_i
|
160
|
+
@expiration = if @date.empty?
|
161
|
+
Time.now + max_age
|
162
|
+
else
|
163
|
+
Time.parse(@date) + max_age
|
164
|
+
end
|
165
|
+
|
166
|
+
if @location
|
167
|
+
get_description @location, getter
|
168
|
+
else
|
169
|
+
fail self, 'M-SEARCH response has no location'
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def bad_description?
|
174
|
+
if @description[:root]
|
175
|
+
bd = false
|
176
|
+
@xmlns = @description[:root][:@xmlns]
|
177
|
+
bd |= @xmlns != 'urn:schemas-upnp-org:device-1-0'
|
178
|
+
bd |= @description[:root][:spec_version][:major].to_i != 1
|
179
|
+
@upnp_version = @description[:root][:spec_version][:major] + '.'
|
180
|
+
@upnp_version += @description[:root][:spec_version][:minor]
|
181
|
+
bd |= !@description[:root][:device]
|
182
|
+
else
|
183
|
+
true
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def extract_url_base
|
188
|
+
if @description[:root][:url_base] and @upnp_version != '1.1'
|
189
|
+
@url_base = @description[:root][:url_base]
|
190
|
+
@url_base += '/' unless @url_base.end_with?('/')
|
191
|
+
else
|
192
|
+
@url_base = @location.match(/[^\/]*\z/).pre_match
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def extract_device_info
|
197
|
+
device = @description[:root][:device]
|
198
|
+
@type = device[:device_type]
|
199
|
+
@friendly_name = device[:friendly_name]
|
200
|
+
@manufacturer = device[:manufacturer]
|
201
|
+
@manufacturer_url = device[:manufacturer_url] || ''
|
202
|
+
@model_description = device[:model_description] || ''
|
203
|
+
@model_name = device[:model_name]
|
204
|
+
@model_number = device[:model_number] || ''
|
205
|
+
@model_url = device[:model_url] || ''
|
206
|
+
@serial_umber = device[:serial_number] || ''
|
207
|
+
@udn = device[:udn]
|
208
|
+
@upc = device[:upc] || ''
|
209
|
+
@presentation_url = device[:presentation_url] || ''
|
210
|
+
end
|
211
|
+
|
212
|
+
def extract_icons
|
213
|
+
@description[:root][:device][:icon_list][:icon].each do |h|
|
214
|
+
icon = OpenStruct.new(h)
|
215
|
+
icon.url = build_url(@url_base, icon.url)
|
216
|
+
@icons << icon
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def extract_services
|
221
|
+
if @description[:root][:device][:service_list] &&
|
222
|
+
@description[:root][:device][:service_list][:service]
|
223
|
+
sl = @description[:root][:device][:service_list][:service]
|
224
|
+
|
225
|
+
proc_each = Proc.new do |s, iter|
|
226
|
+
service = CP::RemoteService.new(self, @url_base, s)
|
227
|
+
|
228
|
+
service.errback do |msg|
|
229
|
+
log :error, "failed to extract service #{s[:service_id]}: #{msg}"
|
230
|
+
iter.next
|
231
|
+
end
|
232
|
+
|
233
|
+
service.callback do |serv|
|
234
|
+
@services << serv
|
235
|
+
create_method_from_service serv
|
236
|
+
iter.next
|
237
|
+
end
|
238
|
+
|
239
|
+
service.fetch
|
240
|
+
end
|
241
|
+
|
242
|
+
proc_after = Proc.new do
|
243
|
+
@services_extracted = true
|
244
|
+
end
|
245
|
+
|
246
|
+
EM::Iterator.new(sl).each(proc_each, proc_after)
|
247
|
+
else
|
248
|
+
@services_extracted = true
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def extract_devices
|
253
|
+
if @description[:root][:device_list]
|
254
|
+
if @description[:root][:device_list][:device]
|
255
|
+
dl = @description[:root][:device_list][:device]
|
256
|
+
## TODO
|
257
|
+
end
|
258
|
+
end
|
259
|
+
@devices_extracted = true ## TEMP
|
260
|
+
end
|
261
|
+
|
262
|
+
def create_method_from_service(service)
|
263
|
+
if service.type =~ /urn:.*:service:(\w+):\d/
|
264
|
+
name = snake_case($1).to_sym
|
265
|
+
define_singleton_method(name) { service }
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
end
|
270
|
+
|
271
|
+
end
|
@@ -0,0 +1,304 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'savon'
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module RUPNP
|
6
|
+
|
7
|
+
# Service class for device's services.
|
8
|
+
#
|
9
|
+
# ==Actions
|
10
|
+
# This class defines ruby methods from actions defined in
|
11
|
+
# service description, as provided by the device.
|
12
|
+
#
|
13
|
+
# By example, from this description:
|
14
|
+
# <action>
|
15
|
+
# <name>actionName</name>
|
16
|
+
# <argumentList>
|
17
|
+
# <argument>
|
18
|
+
# <name>argumentNameIn</name>
|
19
|
+
# <direction>in</direction>
|
20
|
+
# <relatedStateVariable>stateVariableName</relatedStateVariable>
|
21
|
+
# </argument>
|
22
|
+
# <argument>
|
23
|
+
# <name>argumentNameOut</name>
|
24
|
+
# <direction>out</direction>
|
25
|
+
# <relatedStateVariable>stateVariableName</relatedStateVariable>
|
26
|
+
# </argument>
|
27
|
+
# </action>
|
28
|
+
# a +#action_name+ method is created. This method requires a hash with
|
29
|
+
# an element named +argument_name_in+.
|
30
|
+
# If no <i>in</i> argument is required, an empty hash (<code>{}</code>)
|
31
|
+
# must be passed to the method.
|
32
|
+
#
|
33
|
+
# A Hash is returned, with a key for each <i>out</i> argument.
|
34
|
+
#
|
35
|
+
# @author Sylvain Daubert
|
36
|
+
class CP::RemoteService < CP::Base
|
37
|
+
|
38
|
+
# @private
|
39
|
+
@@event_sub_count = 0
|
40
|
+
|
41
|
+
# Get event subscription count for all services
|
42
|
+
# (unique ID for subscription)
|
43
|
+
# @return [Integer]
|
44
|
+
def self.event_sub_count
|
45
|
+
@@event_sub_count += 1
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
# @private SOAP integer types
|
50
|
+
INTEGER_TYPES = %w(ui1 ui2 ui4 i1 i2 i4 int).freeze
|
51
|
+
# @private SOAP float types
|
52
|
+
FLOAT_TYPES = %w(r4 r8 number float).freeze
|
53
|
+
# @private SOAP string types
|
54
|
+
STRING_TYPES = %w(char string uuid).freeze
|
55
|
+
# @private SOAP true values
|
56
|
+
TRUE_TYPES = %w(1 true yes).freeze
|
57
|
+
# @private SOAP false values
|
58
|
+
FALSE_TYPES = %w(0 false no).freeze
|
59
|
+
|
60
|
+
# Get device to which this service belongs to
|
61
|
+
# @return [Device]
|
62
|
+
attr_reader :device
|
63
|
+
|
64
|
+
# Get service type
|
65
|
+
# @return [String]
|
66
|
+
attr_reader :type
|
67
|
+
# URL for service description
|
68
|
+
# @return [String]
|
69
|
+
attr_reader :scpd_url
|
70
|
+
# URL for control
|
71
|
+
# @return [String]
|
72
|
+
attr_reader :control_url
|
73
|
+
# URL for eventing
|
74
|
+
# @return [String]
|
75
|
+
attr_reader :event_sub_url
|
76
|
+
|
77
|
+
# XML namespace for device description
|
78
|
+
# @return [String]
|
79
|
+
attr_reader :xmlns
|
80
|
+
# Define architecture on which the service is implemented
|
81
|
+
# @return [String]
|
82
|
+
attr_reader :spec_version
|
83
|
+
# Available actions on this service
|
84
|
+
# @return [Array<Hash>]
|
85
|
+
attr_reader :actions
|
86
|
+
# State table for the service
|
87
|
+
# @return [Array<Hash>]
|
88
|
+
attr_reader :state_table
|
89
|
+
|
90
|
+
# @param [Device] device
|
91
|
+
# @param [String] url_base
|
92
|
+
# @param [Hash] service
|
93
|
+
def initialize(device, url_base, service)
|
94
|
+
super()
|
95
|
+
@device = device
|
96
|
+
@description = service
|
97
|
+
|
98
|
+
@type = service[:service_type].to_s
|
99
|
+
@scpd_url = build_url(url_base, service[:scpdurl].to_s)
|
100
|
+
@control_url = build_url(url_base, service[:control_url].to_s)
|
101
|
+
@event_sub_url = build_url(url_base, service[:event_sub_url].to_s)
|
102
|
+
@actions = []
|
103
|
+
|
104
|
+
initialize_savon
|
105
|
+
end
|
106
|
+
|
107
|
+
# Get service from its description
|
108
|
+
# @return [void]
|
109
|
+
def fetch
|
110
|
+
if @scpd_url.empty?
|
111
|
+
fail 'no SCPD URL'
|
112
|
+
return
|
113
|
+
end
|
114
|
+
|
115
|
+
scpd_getter = EM::DefaultDeferrable.new
|
116
|
+
|
117
|
+
scpd_getter.errback do
|
118
|
+
fail "cannot get SCPD from #@scpd_url"
|
119
|
+
end
|
120
|
+
|
121
|
+
scpd_getter.callback do |scpd|
|
122
|
+
if !scpd or scpd.empty?
|
123
|
+
fail "SCPD from #@scpd_url is empty"
|
124
|
+
next
|
125
|
+
end
|
126
|
+
|
127
|
+
if bad_description?(scpd)
|
128
|
+
fail 'not a UPNP 1.0/1.1 SCPD'
|
129
|
+
next
|
130
|
+
end
|
131
|
+
|
132
|
+
extract_service_state_table scpd
|
133
|
+
extract_actions scpd
|
134
|
+
|
135
|
+
succeed self
|
136
|
+
end
|
137
|
+
|
138
|
+
get_description @scpd_url, scpd_getter
|
139
|
+
end
|
140
|
+
|
141
|
+
# Subscribe to event
|
142
|
+
# @param [Hash] options
|
143
|
+
# @option options [Integer] timeout
|
144
|
+
# @yieldparam [Event] event event received
|
145
|
+
def subscribe_to_event(options={}, &blk)
|
146
|
+
cp = device.control_point
|
147
|
+
|
148
|
+
cp.start_event_server
|
149
|
+
|
150
|
+
port = cp.event_port
|
151
|
+
num = self.class.event_sub_count
|
152
|
+
@callback_url = "http://#{HOST_IP}:#{port}/event#{num}}"
|
153
|
+
|
154
|
+
uri = URI(@event_sub_url)
|
155
|
+
options[:timeout] ||= EVENT_SUB_DEFAULT_TIMEOUT
|
156
|
+
subscribe_req = <<EOR
|
157
|
+
SUSCRIBE #{uri.path} HTTP/1.1\r
|
158
|
+
HOST: #{HOST_IP}:#{port}\r
|
159
|
+
USER-AGENT: #{RUPNP::USER_AGENT}\r
|
160
|
+
CALLBACK: #@callback_url\r
|
161
|
+
NT: upnp:event
|
162
|
+
TIMEOUT: Second-#{options[:timeout]}\r
|
163
|
+
\r
|
164
|
+
EOR
|
165
|
+
|
166
|
+
server = uri.host
|
167
|
+
port = (uri.port || 80).to_i
|
168
|
+
ap @event_sub_url
|
169
|
+
ap server
|
170
|
+
ap port
|
171
|
+
con = EM.connect(server, port, CP::EventSubscriber, subscribe_req)
|
172
|
+
|
173
|
+
con.response.subscribe do |resp|
|
174
|
+
if resp[:status_code] != 200
|
175
|
+
log :warn, "Cannot subscribe to event #@event_sub_url: #{resp[:status]}"
|
176
|
+
else
|
177
|
+
event = Event.new(resp[:sid], resp[:timeout].match(/(\d+)/)[1].to_i)
|
178
|
+
cp.add_event_url << ["/event#{num}", event]
|
179
|
+
event.subscribe(event, blk)
|
180
|
+
end
|
181
|
+
log :info, 'Close connection to subscribe event URL'
|
182
|
+
con.close_connection
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
def bad_description?(scpd)
|
190
|
+
if scpd[:scpd]
|
191
|
+
bd = false
|
192
|
+
@xmlns = scpd[:scpd][:@xmlns]
|
193
|
+
bd |= @xmlns != "urn:schemas-upnp-org:service-1-0"
|
194
|
+
bd |= scpd[:scpd][:spec_version][:major].to_i != 1
|
195
|
+
@spec_version = scpd[:scpd][:spec_version][:major] + '.'
|
196
|
+
@spec_version += scpd[:scpd][:spec_version][:minor]
|
197
|
+
bd |= !scpd[:scpd][:service_state_table]
|
198
|
+
bd | scpd[:scpd][:service_state_table].empty?
|
199
|
+
else
|
200
|
+
true
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def extract_service_state_table(scpd)
|
205
|
+
if scpd[:scpd][:service_state_table][:state_variable]
|
206
|
+
@state_table = scpd[:scpd][:service_state_table][:state_variable]
|
207
|
+
# ease debug print
|
208
|
+
@state_table.each { |s| s.each { |k, v| s[k] = v.to_s } }
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def extract_actions(scpd)
|
213
|
+
if scpd[:scpd][:action_list] and scpd[:scpd][:action_list][:action]
|
214
|
+
log :info, "extract actions for service #@type"
|
215
|
+
@actions = scpd[:scpd][:action_list][:action]
|
216
|
+
@actions.each do |action|
|
217
|
+
action[:arguments] = action[:argument_list][:argument]
|
218
|
+
action.delete :argument_list
|
219
|
+
define_method_from_action action
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def define_method_from_action(action)
|
225
|
+
action[:name] = action[:name].to_s
|
226
|
+
action_name = action[:name]
|
227
|
+
name = snake_case(action_name).to_sym
|
228
|
+
define_singleton_method(name) do |params|
|
229
|
+
response = @soap.call(action_name) do |locals|
|
230
|
+
locals.attributes 'xmlns:u' => @type
|
231
|
+
locals.soap_action "#{type}##{action_name}"
|
232
|
+
if params
|
233
|
+
unless params.is_a? Hash
|
234
|
+
raise ArgumentError, 'only hash arguments are accepted'
|
235
|
+
end
|
236
|
+
locals.message params
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
if action[:arguments].is_a? Hash
|
241
|
+
log :debug, 'only one argument in argument list'
|
242
|
+
if action[:arguments][:direction] == 'out'
|
243
|
+
process_soap_response name, response, action[:arguments]
|
244
|
+
end
|
245
|
+
else
|
246
|
+
log :debug, 'true argument list'
|
247
|
+
action[:arguments].map do |arg|
|
248
|
+
if params arg[:direction] == 'out'
|
249
|
+
process_soap_response name, response, arg
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def process_soap_response(action, resp, out_arg)
|
257
|
+
if resp.success? and resp.to_xml.empty?
|
258
|
+
log :debug, 'Successful SOAP request but empty response'
|
259
|
+
return {}
|
260
|
+
end
|
261
|
+
|
262
|
+
state_var = @state_table.find do |h|
|
263
|
+
h[:name] == out_arg[:related_state_variable]
|
264
|
+
end
|
265
|
+
|
266
|
+
action_response = "#{action}_response".to_sym
|
267
|
+
out_arg_name = snake_case(out_arg[:name]).to_sym
|
268
|
+
value = resp.hash[:envelope][:body][action_response][out_arg_name]
|
269
|
+
|
270
|
+
transform_method = if INTEGER_TYPES.include? state_var[:data_type]
|
271
|
+
:to_i
|
272
|
+
elsif FLOAT_TYPES.include? state_var[:data_type]
|
273
|
+
:to_f
|
274
|
+
elsif STRING_TYPES.include? state_var[:data_type]
|
275
|
+
:to_s
|
276
|
+
end
|
277
|
+
if transform_method
|
278
|
+
{ out_arg_name => value.send(transform_method) }
|
279
|
+
elsif TRUE_TYPES.include? state_var[:data_type]
|
280
|
+
{ out_arg_name => true }
|
281
|
+
elsif FALSE_TYPES.include? state_var[:data_type]
|
282
|
+
{ out_arg_name => false }
|
283
|
+
else
|
284
|
+
log :warn, "SOAP response has an unknown type: #{state_var[:data_type]}"
|
285
|
+
{}
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def initialize_savon
|
290
|
+
@soap = Savon.client do |globals|
|
291
|
+
globals.log_level :error
|
292
|
+
globals.endpoint @control_url
|
293
|
+
globals.namespace @type
|
294
|
+
globals.convert_request_keys_to :camel_case
|
295
|
+
globals.log true
|
296
|
+
globals.headers :HOST => "#{HOST_IP}"
|
297
|
+
globals.env_namespace 's'
|
298
|
+
globals.namespace_identifier 'u'
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
end
|
303
|
+
|
304
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'eventmachine-le'
|
2
|
+
require 'pry'
|
3
|
+
require 'rupnp'
|
4
|
+
|
5
|
+
class RUPNP::Discover
|
6
|
+
attr_reader :devices
|
7
|
+
|
8
|
+
def self.run
|
9
|
+
d = new
|
10
|
+
d.pry
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
configure_rupnp
|
15
|
+
configure_pry
|
16
|
+
create_command_set
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def configure_rupnp
|
23
|
+
RUPNP.logdev = devnull = File.open('/dev/null', 'w')
|
24
|
+
at_exit { devnull.close }
|
25
|
+
end
|
26
|
+
|
27
|
+
def configure_pry
|
28
|
+
::Pry.config.should_load_rc = false
|
29
|
+
::Pry.config.history.should_save = false
|
30
|
+
::Pry.config.history.should_load = false
|
31
|
+
::Pry.config.prompt = [proc { 'discover> ' }, proc { 'discover* ' }]
|
32
|
+
end
|
33
|
+
|
34
|
+
def create_command_set
|
35
|
+
discover = self
|
36
|
+
|
37
|
+
command_set = Pry::CommandSet.new do
|
38
|
+
block_command 'search', 'Search for devices' do |target|
|
39
|
+
target ||= :all
|
40
|
+
cp = RUPNP::ControlPoint.new(target)
|
41
|
+
EM.run do
|
42
|
+
cp.search_only
|
43
|
+
EM.add_timer(RUPNP::ControlPoint::DEFAULT_RESPONSE_WAIT_TIME+2) do
|
44
|
+
discover.instance_eval { @devices = cp.devices }
|
45
|
+
output.puts "#{discover.devices.size} devices found"
|
46
|
+
EM.stop
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
Pry::Commands.import command_set
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
data/lib/rupnp/event.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module RUPNP
|
2
|
+
|
3
|
+
# Event class to handle events from devices
|
4
|
+
# @todo Renewal and cancellation of subscription are not coded
|
5
|
+
# @author Sylvain Daubert
|
6
|
+
class Event < EM::Channel
|
7
|
+
|
8
|
+
# Get service ID
|
9
|
+
# @return [Integer]
|
10
|
+
attr_reader :sid
|
11
|
+
|
12
|
+
# @param [#to_i] sid
|
13
|
+
# @param [Integer] timeout for event (in seconds)
|
14
|
+
def initialize(sid, timeout)
|
15
|
+
@sid, @timeout = sid, timeout
|
16
|
+
|
17
|
+
@timeout_timer = EM.add_timer(@timeout) { self << :timeout }
|
18
|
+
end
|
19
|
+
|
20
|
+
# Renew subscription to event
|
21
|
+
def renew_subscription
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
|
25
|
+
# Cancel subscription to event
|
26
|
+
def cancel_subscription
|
27
|
+
raise NotImplementedError
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module RUPNP
|
2
|
+
|
3
|
+
# Mixin to add log facility to others classes.
|
4
|
+
# @author Sylvain Daubert
|
5
|
+
module LogMixin
|
6
|
+
|
7
|
+
|
8
|
+
# Log severity levels
|
9
|
+
LOG_LEVEL = {
|
10
|
+
:failure => 5,
|
11
|
+
:error => 4,
|
12
|
+
:warn => 3,
|
13
|
+
:info => 2,
|
14
|
+
:debug => 1
|
15
|
+
}
|
16
|
+
LOG_LEVEL.default = 0
|
17
|
+
|
18
|
+
# log a message
|
19
|
+
# @param [Symbol] level severity level. May be +:debug+,
|
20
|
+
# +:info+, +warn+, or +:error+
|
21
|
+
# @param [String] msg message to log
|
22
|
+
def log(level, msg='')
|
23
|
+
if LOG_LEVEL[level] >= LOG_LEVEL[RUPNP.log_level]
|
24
|
+
RUPNP.logdev.puts "[#{level}] #{msg}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|