rbupnptools 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.
- checksums.yaml +7 -0
- data/lib/http_header.rb +84 -0
- data/lib/ssdp.rb +168 -0
- data/lib/upnp_control_point.rb +197 -0
- data/lib/upnp_event.rb +86 -0
- data/lib/upnp_model.rb +419 -0
- data/lib/upnp_server.rb +316 -0
- data/lib/upnp_soap.rb +96 -0
- data/lib/upnp_xml.rb +47 -0
- data/lib/usn.rb +31 -0
- metadata +52 -0
data/lib/upnp_event.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'net/http'
|
3
|
+
require_relative 'upnp_xml.rb'
|
4
|
+
|
5
|
+
class NotifyRequest < Net::HTTPRequest
|
6
|
+
METHOD = 'NOTIFY'
|
7
|
+
REQUEST_HAS_BODY = true
|
8
|
+
RESPONSE_HAS_BODY = false
|
9
|
+
end
|
10
|
+
|
11
|
+
class SubscribeRequest < Net::HTTPRequest
|
12
|
+
METHOD = 'SUBSCRIBE'
|
13
|
+
REQUEST_HAS_BODY = false
|
14
|
+
RESPONSE_HAS_BODY = false
|
15
|
+
end
|
16
|
+
|
17
|
+
class UnsubscribeRequest < Net::HTTPRequest
|
18
|
+
METHOD = 'UNSUBSCRIBE'
|
19
|
+
REQUEST_HAS_BODY = false
|
20
|
+
RESPONSE_HAS_BODY = false
|
21
|
+
end
|
22
|
+
|
23
|
+
class UPnPEventProperty < Hash
|
24
|
+
def to_xml
|
25
|
+
propertyset = XmlTag.new 'e:propertyset'
|
26
|
+
propertyset.attributes = {
|
27
|
+
'xmlns:e' => "urn:schemas-upnp-org:event-1-0"
|
28
|
+
}
|
29
|
+
self.each do |k,v|
|
30
|
+
prop = propertyset.append XmlTag.new 'e:property'
|
31
|
+
prop.append(XmlTag.new(k)).append(XmlText.new(v))
|
32
|
+
end
|
33
|
+
return propertyset.to_s
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def UPnPEventProperty.to_xml_doc(property)
|
38
|
+
return '<?xml version="1.0" encoding="UTF-8"?>' + "\n#{property.to_xml}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def UPnPEventProperty.read(xml)
|
42
|
+
props = UPnPEventProperty.new
|
43
|
+
doc = Nokogiri::XML(xml)
|
44
|
+
doc.root.elements.each do |elem|
|
45
|
+
if elem.name == 'property'
|
46
|
+
name = elem.elements.first.name
|
47
|
+
value = elem.elements.first.text
|
48
|
+
props[name] = value
|
49
|
+
end
|
50
|
+
end
|
51
|
+
props
|
52
|
+
end
|
53
|
+
|
54
|
+
class UPnPEventListener
|
55
|
+
def initialize(on_event_notify = nil)
|
56
|
+
@on_event_notify = on_event_notify
|
57
|
+
end
|
58
|
+
|
59
|
+
def on_event_notify(sid, props)
|
60
|
+
if @on_event_notify
|
61
|
+
@on_event_notify.call sid, props
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class UPnPEventSubscription
|
67
|
+
def initialize(device, service, sid, timeout, callback_urls = nil)
|
68
|
+
@device = device
|
69
|
+
@service = service
|
70
|
+
@sid = sid
|
71
|
+
@timeout = timeout
|
72
|
+
@reg_date = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
|
73
|
+
@callback_urls = callback_urls
|
74
|
+
end
|
75
|
+
|
76
|
+
attr_accessor :device, :service, :sid, :timeout, :callback_urls
|
77
|
+
|
78
|
+
def renew_timeout
|
79
|
+
@reg_date = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
|
80
|
+
end
|
81
|
+
|
82
|
+
def expired?
|
83
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
|
84
|
+
return (now - @reg_date) >= @tiemout
|
85
|
+
end
|
86
|
+
end
|
data/lib/upnp_model.rb
ADDED
@@ -0,0 +1,419 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'securerandom'
|
3
|
+
require_relative 'upnp_xml.rb'
|
4
|
+
|
5
|
+
|
6
|
+
class UPnPModel < Hash
|
7
|
+
def self.to_method_name(name)
|
8
|
+
return name.gsub(/([a-z])([A-Z]+)/, '\1_\2').downcase
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.define_xml_attr(*names)
|
12
|
+
names.each do |name|
|
13
|
+
name = "#{name}"
|
14
|
+
define_method "#{to_method_name(name)}" do
|
15
|
+
self[name]
|
16
|
+
end
|
17
|
+
define_method "#{to_method_name(name)}=" do |v|
|
18
|
+
self[name] = v
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class UPnPSpecVersion
|
25
|
+
|
26
|
+
attr_accessor :major, :minor
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
"UPnPSpecVersion -- 'major: #{@major}, minor: #{minor}'"
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_xml
|
33
|
+
spec_version = XmlTag.new('specVersion')
|
34
|
+
spec_version.append(XmlTag.new('major')).append(XmlText.new(@major))
|
35
|
+
spec_version.append(XmlTag.new('minor')).append(XmlText.new(@minor))
|
36
|
+
return spec_version.to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
def UPnPSpecVersion.read_xml_node(node)
|
42
|
+
spec_version = UPnPSpecVersion.new
|
43
|
+
node.elements.each { |elem|
|
44
|
+
if elem.name == 'major'
|
45
|
+
spec_version.major = elem.text
|
46
|
+
elsif elem.name == 'minor'
|
47
|
+
spec_version.minor = elem.text
|
48
|
+
end
|
49
|
+
}
|
50
|
+
return spec_version
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
class UPnPDevice < UPnPModel
|
55
|
+
def initialize
|
56
|
+
@child_devices = []
|
57
|
+
@services = []
|
58
|
+
end
|
59
|
+
|
60
|
+
attr_accessor :base_url
|
61
|
+
attr_accessor :child_devices
|
62
|
+
attr_accessor :services
|
63
|
+
|
64
|
+
define_xml_attr :deviceType, :UDN, :friendlyName, :manufacturer, :manufacturerURL,
|
65
|
+
:modelDescription, :modelName, :modelNumber, :modelURL, :serialNumber
|
66
|
+
|
67
|
+
def usn
|
68
|
+
return udn + '::' + device_type
|
69
|
+
end
|
70
|
+
|
71
|
+
def all_usn
|
72
|
+
usn_list = []
|
73
|
+
usn_list << usn
|
74
|
+
@services.each do |service|
|
75
|
+
usn_list << udn + '::' + service.service_type
|
76
|
+
end
|
77
|
+
@child_devices.each do |device|
|
78
|
+
usn_list += device.all_usn
|
79
|
+
end
|
80
|
+
return usn_list
|
81
|
+
end
|
82
|
+
|
83
|
+
def get_device(type)
|
84
|
+
all_devices.each do |device|
|
85
|
+
if device.device_type == type
|
86
|
+
return device
|
87
|
+
end
|
88
|
+
end
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
|
92
|
+
def get_service(type)
|
93
|
+
all_services.each do |service|
|
94
|
+
if service.service_type == type
|
95
|
+
return service
|
96
|
+
end
|
97
|
+
end
|
98
|
+
nil
|
99
|
+
end
|
100
|
+
|
101
|
+
def all_devices
|
102
|
+
devices = []
|
103
|
+
devices << self
|
104
|
+
@child_devices.each do |device|
|
105
|
+
devices += device.all_devices
|
106
|
+
end
|
107
|
+
return devices
|
108
|
+
end
|
109
|
+
|
110
|
+
def all_services
|
111
|
+
services = @services
|
112
|
+
@child_devices.each do |device|
|
113
|
+
services += device.all_services
|
114
|
+
end
|
115
|
+
return services
|
116
|
+
end
|
117
|
+
|
118
|
+
def to_s
|
119
|
+
"UPnPDevice -- '#{friendly_name}' (#{udn})"
|
120
|
+
end
|
121
|
+
|
122
|
+
def to_xml
|
123
|
+
device = XmlTag.new 'device'
|
124
|
+
self.each { |k,v|
|
125
|
+
device.append(XmlTag.new(k)).append(XmlText.new(v))
|
126
|
+
}
|
127
|
+
|
128
|
+
if @services.any?
|
129
|
+
serviceList = device.append XmlTag.new('serviceList')
|
130
|
+
@services.each { |service|
|
131
|
+
serviceList.append service.to_xml
|
132
|
+
}
|
133
|
+
end
|
134
|
+
|
135
|
+
if @child_devices.any?
|
136
|
+
deviceList = device.append XmlTag.new('deviceList')
|
137
|
+
@child_devices.each { |device|
|
138
|
+
deviceList.append device.to_xml
|
139
|
+
}
|
140
|
+
end
|
141
|
+
return device.to_s
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def UPnPDevice.to_xml_doc(device)
|
146
|
+
root = XmlTag.new 'root'
|
147
|
+
root.attributes['xmlns'] = 'urn:schemas-upnp-org:device-1-0'
|
148
|
+
|
149
|
+
specVersion = root.append(XmlTag.new('specVersion'))
|
150
|
+
specVersion.append(XmlTag.new('major')).append(XmlText.new(1))
|
151
|
+
specVersion.append(XmlTag.new('major')).append(XmlText.new(0))
|
152
|
+
|
153
|
+
root.append device.to_xml
|
154
|
+
return '<?xml version="1.0" encoding="UTF-8"?>' + "\n#{root}"
|
155
|
+
end
|
156
|
+
|
157
|
+
def UPnPDevice.read(xml)
|
158
|
+
doc = Nokogiri::XML(xml)
|
159
|
+
doc.root.elements.each do |elem|
|
160
|
+
if elem.name == 'specVersion'
|
161
|
+
spec_version = UPnPSpecVersion.read_xml_node elem
|
162
|
+
elsif elem.name == 'device'
|
163
|
+
return UPnPDevice.read_xml_node elem
|
164
|
+
end
|
165
|
+
end
|
166
|
+
nil
|
167
|
+
end
|
168
|
+
|
169
|
+
def UPnPDevice.read_xml_node(node)
|
170
|
+
device = UPnPDevice.new
|
171
|
+
node.elements.each do |elem|
|
172
|
+
if elem.elements.empty?
|
173
|
+
device[elem.name] = elem.text
|
174
|
+
elsif elem.name == 'deviceList'
|
175
|
+
elem.elements.each do |device_node|
|
176
|
+
if device_node.name == 'device'
|
177
|
+
device.child_devices << UPnPDevice.read_xml_node(device_node)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
elsif elem.name == 'serviceList'
|
181
|
+
elem.elements.each do |service_node|
|
182
|
+
if service_node.name == 'service'
|
183
|
+
device.services << UPnPService.read_xml_node(service_node)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
device
|
189
|
+
end
|
190
|
+
|
191
|
+
|
192
|
+
class UPnPService < UPnPModel
|
193
|
+
attr_accessor :scpd
|
194
|
+
|
195
|
+
define_xml_attr :serviceId, :serviceType, :SCPDURL, :controlURL, :eventSubURL
|
196
|
+
|
197
|
+
def to_xml
|
198
|
+
service = XmlTag.new 'service'
|
199
|
+
self.each { |k,v|
|
200
|
+
service.append(XmlTag.new(k)).append(XmlText.new(v))
|
201
|
+
}
|
202
|
+
return service.to_s
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def UPnPService.read_xml_node(node)
|
207
|
+
service = UPnPService.new
|
208
|
+
node.elements.each do |elem|
|
209
|
+
if elem.elements.empty?
|
210
|
+
service[elem.name] = elem.text
|
211
|
+
end
|
212
|
+
end
|
213
|
+
service
|
214
|
+
end
|
215
|
+
|
216
|
+
|
217
|
+
class UPnPScpd < UPnPModel
|
218
|
+
|
219
|
+
def initialize
|
220
|
+
@actions = []
|
221
|
+
@state_variables = []
|
222
|
+
end
|
223
|
+
|
224
|
+
attr_accessor :spec_version, :actions, :state_variables
|
225
|
+
|
226
|
+
def to_xml
|
227
|
+
scpd = XmlTag.new 'scpd'
|
228
|
+
scpd.attributes['xmlns'] = 'urn:schemas-upnp-org:service-1-0'
|
229
|
+
|
230
|
+
spec_version = scpd.append XmlTag.new('specVersion')
|
231
|
+
spec_version.append(XmlTag.new('major')).append(XmlText.new(1))
|
232
|
+
spec_version.append(XmlTag.new('minor')).append(XmlText.new(0))
|
233
|
+
|
234
|
+
action_list = scpd.append XmlTag.new('actionList')
|
235
|
+
@actions.each { |action|
|
236
|
+
action_list.append action.to_xml
|
237
|
+
}
|
238
|
+
|
239
|
+
service_state_table = scpd.append XmlTag.new('serviceStateTable');
|
240
|
+
@state_variables.each { |state_variable|
|
241
|
+
service_state_table.append state_variable.to_xml
|
242
|
+
}
|
243
|
+
return scpd.to_s
|
244
|
+
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def UPnPScpd.to_xml_doc(scpd)
|
249
|
+
return '<?xml version="1.0" encoding="UTF-8"?>' + "\n#{scpd.to_xml}"
|
250
|
+
end
|
251
|
+
|
252
|
+
def UPnPScpd.read(xml)
|
253
|
+
scpd = UPnPScpd.new
|
254
|
+
doc = Nokogiri::XML(xml)
|
255
|
+
doc.root.elements.each do |elem|
|
256
|
+
case elem.name
|
257
|
+
when 'specVersion'
|
258
|
+
scpd.spec_version = [1,0]
|
259
|
+
when 'actionList'
|
260
|
+
elem.elements.each { |action_node|
|
261
|
+
if action_node.name == 'action'
|
262
|
+
action = UPnPAction.read_xml_node(action_node)
|
263
|
+
scpd.actions << action
|
264
|
+
end
|
265
|
+
}
|
266
|
+
when 'serviceStateTable'
|
267
|
+
elem.elements.each { |state_variable_node|
|
268
|
+
if state_variable_node.name == 'stateVariable'
|
269
|
+
state_variable = UPnPStateVariable.read_xml_node(state_variable_node)
|
270
|
+
scpd.state_variables << state_variable
|
271
|
+
end
|
272
|
+
}
|
273
|
+
end
|
274
|
+
end
|
275
|
+
return scpd
|
276
|
+
end
|
277
|
+
|
278
|
+
|
279
|
+
class UPnPAction < UPnPModel
|
280
|
+
def initialize
|
281
|
+
@arguments = []
|
282
|
+
end
|
283
|
+
|
284
|
+
attr_accessor :name
|
285
|
+
attr_accessor :arguments
|
286
|
+
|
287
|
+
def get_argument(name)
|
288
|
+
@arguments.each { |argument|
|
289
|
+
if argument.name == name
|
290
|
+
return argument
|
291
|
+
end
|
292
|
+
}
|
293
|
+
nil
|
294
|
+
end
|
295
|
+
|
296
|
+
def in_arguments
|
297
|
+
@arguments.select { |argument| argument.direction == 'in' }
|
298
|
+
end
|
299
|
+
|
300
|
+
def out_arguments
|
301
|
+
@arguments.select { |argument| argument.direction == 'out' }
|
302
|
+
end
|
303
|
+
|
304
|
+
def to_s
|
305
|
+
"UPnPAction -- #{@name}"
|
306
|
+
end
|
307
|
+
|
308
|
+
def to_xml
|
309
|
+
action = XmlTag.new 'action'
|
310
|
+
|
311
|
+
prop = action.append XmlTag.new 'name'
|
312
|
+
prop.append XmlText.new @name
|
313
|
+
|
314
|
+
argument_list = action.append XmlTag.new 'argumentList'
|
315
|
+
@arguments.each { |argument|
|
316
|
+
argument_list.append argument.to_xml
|
317
|
+
}
|
318
|
+
|
319
|
+
return action.to_s
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
def UPnPAction.read_xml_node(node)
|
324
|
+
action = UPnPAction.new
|
325
|
+
node.elements.each do |elem|
|
326
|
+
if elem.name == 'name'
|
327
|
+
action.name = elem.text
|
328
|
+
elsif elem.name == 'argumentList'
|
329
|
+
elem.elements.each { |argument_node|
|
330
|
+
if argument_node.name == 'argument'
|
331
|
+
action.arguments << UPnPActionArgument.read_xml_node(argument_node)
|
332
|
+
end
|
333
|
+
}
|
334
|
+
end
|
335
|
+
end
|
336
|
+
return action
|
337
|
+
end
|
338
|
+
|
339
|
+
|
340
|
+
class UPnPActionArgument < UPnPModel
|
341
|
+
attr_accessor :name, :direction, :related_state_variable
|
342
|
+
|
343
|
+
def to_s
|
344
|
+
"UPnPActionArgument -- #{@name} (direction: '#{@direction}' related state variable: '#{@related_state_variable}')"
|
345
|
+
end
|
346
|
+
|
347
|
+
def to_xml
|
348
|
+
argument = XmlTag.new 'argument'
|
349
|
+
|
350
|
+
prop = argument.append XmlTag.new 'name'
|
351
|
+
prop.append XmlText.new @name
|
352
|
+
|
353
|
+
prop = argument.append XmlTag.new 'direction'
|
354
|
+
prop.append XmlText.new @direction
|
355
|
+
|
356
|
+
prop = argument.append XmlTag.new 'relatedStateVariable'
|
357
|
+
prop.append XmlText.new @related_state_variable
|
358
|
+
|
359
|
+
return argument.to_s
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
def UPnPActionArgument.read_xml_node(node)
|
364
|
+
argument = UPnPActionArgument.new
|
365
|
+
node.elements.each do |elem|
|
366
|
+
case elem.name
|
367
|
+
when 'name'
|
368
|
+
argument.name = elem.text
|
369
|
+
when 'direction'
|
370
|
+
argument.direction = elem.text
|
371
|
+
when 'relatedStateVariable'
|
372
|
+
argument.related_state_variable = elem.text
|
373
|
+
end
|
374
|
+
end
|
375
|
+
return argument
|
376
|
+
end
|
377
|
+
|
378
|
+
|
379
|
+
class UPnPStateVariable < UPnPModel
|
380
|
+
attr_accessor :name, :data_type, :send_events, :multicast
|
381
|
+
|
382
|
+
def to_s
|
383
|
+
"State Variable -- #{@name} (data type: '#{@data_type}' send events? '#{@send_events}' multicast? '#{@multicast}')"
|
384
|
+
end
|
385
|
+
|
386
|
+
def to_xml
|
387
|
+
state_variable = XmlTag.new 'stateVariable'
|
388
|
+
if @send_events != nil
|
389
|
+
state_variable.attributes['sendEvents'] = @send_events
|
390
|
+
end
|
391
|
+
|
392
|
+
if @multicast != nil
|
393
|
+
state_variable.attributes['multicast'] = @multicast
|
394
|
+
end
|
395
|
+
|
396
|
+
prop = state_variable.append XmlTag.new 'name'
|
397
|
+
prop.append XmlText.new @name
|
398
|
+
|
399
|
+
prop = state_variable.append XmlTag.new 'dataType'
|
400
|
+
prop.append XmlText.new @data_type
|
401
|
+
|
402
|
+
return state_variable.to_s
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
def UPnPStateVariable.read_xml_node(node)
|
407
|
+
state_variable = UPnPStateVariable.new
|
408
|
+
state_variable.send_events = node.attribute('sendEvents')
|
409
|
+
state_variable.multicast = node.attribute('multicast')
|
410
|
+
node.elements.each do |elem|
|
411
|
+
case elem.name
|
412
|
+
when 'name'
|
413
|
+
state_variable.name = elem.text
|
414
|
+
when 'dataType'
|
415
|
+
state_variable.data_type = elem.text
|
416
|
+
end
|
417
|
+
end
|
418
|
+
return state_variable
|
419
|
+
end
|