easy_upnp 0.4.4 → 1.0.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 +4 -4
- data/README.md +43 -0
- data/easy_upnp.gemspec +2 -5
- data/lib/easy_upnp/{argument_validator.rb → control_point/argument_validator.rb} +24 -0
- data/lib/easy_upnp/control_point/client_wrapper.rb +50 -0
- data/lib/easy_upnp/control_point/device_control_point.rb +149 -0
- data/lib/easy_upnp/control_point/service_method.rb +93 -0
- data/lib/easy_upnp/control_point/validator_provider.rb +39 -0
- data/lib/easy_upnp/ssdp_searcher.rb +4 -3
- data/lib/easy_upnp/upnp_device.rb +16 -10
- data/lib/easy_upnp/version.rb +1 -1
- metadata +11 -37
- data/lib/easy_upnp/device_control_point.rb +0 -254
- data/lib/easy_upnp/logger.rb +0 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c58d2d816ce224f674b5bca2393a6f25007b27b3
|
4
|
+
data.tar.gz: 97eca3f41437f100470a5bb8cdda6815a5fc7959
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ac8b59b38423087c105266b9781e93f33e92a9771a851ef8c0c4beb89754117674694c5231889486fb96dcfc26d95fb40e12af39d65b7d2e6576dd19a1d18d24
|
7
|
+
data.tar.gz: b875881fd185faef0f4b8dc4c07aca21f04ce0d80acee5b2a96dd457681085ee186b086323215c86cbe69b550d062eab1dcbdb6494e3bb7c519c1361a752e092
|
data/README.md
CHANGED
@@ -84,3 +84,46 @@ EasyUpnp::Log.enabled = false
|
|
84
84
|
# Change log level (only has an effect if logging is enabled)
|
85
85
|
EasyUpnp::Log.level = :debug
|
86
86
|
```
|
87
|
+
|
88
|
+
## Validation
|
89
|
+
|
90
|
+
Clients can validate the arguments passed to its methods. By default, this behavior is disabled. You can enable it when initializing a client:
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
client = device.service('urn:schemas-upnp-org:service:ContentDirectory:1') do |o|
|
94
|
+
o.validate_arguments = true
|
95
|
+
end
|
96
|
+
```
|
97
|
+
|
98
|
+
This enables type checking in addition to whatever validation information is available in the UPnP service's definition. For example:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
client.GetVolume(InstanceID: '0', Channel: 'Master')
|
102
|
+
#: ArgumentError: Invalid value for argument InstanceID: 0 is the wrong type. Should be one of: [Integer]
|
103
|
+
client.GetVolume(InstanceID: 0, Channel: 'Master2')
|
104
|
+
#: ArgumentError: Invalid value for argument Channel: Master2 is not in list of allowed values: ["Master"]
|
105
|
+
client.GetVolume(InstanceID: 0, Channel: 'Master')
|
106
|
+
#=> {:CurrentVolume=>"32"}
|
107
|
+
```
|
108
|
+
|
109
|
+
It's also possible to retrieve information about arguments:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
client.method_args(:SetVolume)
|
113
|
+
#=> [:InstanceID, :Channel, :DesiredVolume]
|
114
|
+
validator = client.arg_validator(:SetVolume, :DesiredVolume)
|
115
|
+
validator.required_class
|
116
|
+
#=> Integer
|
117
|
+
validator.valid_range
|
118
|
+
#=> #<Enumerator: 0..100:step(1)>
|
119
|
+
validator.valid_range.max
|
120
|
+
#=> 100
|
121
|
+
validator.validate(32)
|
122
|
+
#=> true
|
123
|
+
validator.validate(101)
|
124
|
+
#: ArgumentError: 101 is not in allowed range of values: #<Enumerator: 0..100:step(1)>
|
125
|
+
|
126
|
+
validator = client.arg_validator(:SetVolume, :Channel)
|
127
|
+
validator.allowed_values
|
128
|
+
#=> ["Master"]
|
129
|
+
```
|
data/easy_upnp.gemspec
CHANGED
@@ -13,11 +13,8 @@ Gem::Specification.new do |gem|
|
|
13
13
|
gem.homepage = 'http://github.com/sidoh/easy_upnp'
|
14
14
|
|
15
15
|
gem.add_dependency 'rake'
|
16
|
-
gem.add_dependency 'savon', '~> 2.11
|
17
|
-
gem.add_dependency '
|
18
|
-
gem.add_dependency 'nokogiri', '~> 1.6.6.2'
|
19
|
-
|
20
|
-
gem.add_development_dependency('rspec', [">= 2.0.0"])
|
16
|
+
gem.add_dependency 'savon', '~> 2.11'
|
17
|
+
gem.add_dependency 'nokogiri', '~> 1.6'
|
21
18
|
|
22
19
|
ignores = File.readlines(".gitignore").grep(/\S+/).map(&:chomp)
|
23
20
|
dotfiles = %w[.gitignore]
|
@@ -121,5 +121,29 @@ module EasyUpnp
|
|
121
121
|
def self.build(&block)
|
122
122
|
Builder.new(&block).build
|
123
123
|
end
|
124
|
+
|
125
|
+
def self.no_op
|
126
|
+
build
|
127
|
+
end
|
128
|
+
|
129
|
+
def self.from_xml(xml)
|
130
|
+
build do |v|
|
131
|
+
v.type(xml.xpath('dataType').text)
|
132
|
+
|
133
|
+
if (range = xml.xpath('allowedValueRange')).any?
|
134
|
+
min = range.xpath('minimum').text
|
135
|
+
max = range.xpath('maximum').text
|
136
|
+
step = range.xpath('step')
|
137
|
+
step = step.any? ? step.text : 1
|
138
|
+
|
139
|
+
v.in_range(min.to_i, max.to_i, step.to_i)
|
140
|
+
end
|
141
|
+
|
142
|
+
if (list = xml.xpath('allowedValueList')).any?
|
143
|
+
allowed_values = list.xpath('allowedValue').map { |x| x.text }
|
144
|
+
v.allowed_values(*allowed_values)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
124
148
|
end
|
125
149
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module EasyUpnp
|
2
|
+
class ClientWrapper
|
3
|
+
def initialize(endpoint,
|
4
|
+
urn,
|
5
|
+
call_options:,
|
6
|
+
advanced_typecasting:,
|
7
|
+
log_enabled:,
|
8
|
+
log_level:)
|
9
|
+
|
10
|
+
# For some reason was not able to pass these options in the config block
|
11
|
+
# in Savon 2.11
|
12
|
+
options = {
|
13
|
+
log: log_enabled,
|
14
|
+
log_level: log_level
|
15
|
+
}
|
16
|
+
|
17
|
+
@client = Savon.client(options) do |c|
|
18
|
+
c.endpoint endpoint
|
19
|
+
c.namespace urn
|
20
|
+
|
21
|
+
# I found this was necessary on some of my UPnP devices (namely, a Sony TV).
|
22
|
+
c.namespaces({:'s:encodingStyle' => "http://schemas.xmlsoap.org/soap/encoding/"})
|
23
|
+
|
24
|
+
# This makes XML tags be like <ObjectID> instead of <objectID>.
|
25
|
+
c.convert_request_keys_to :camelcase
|
26
|
+
|
27
|
+
c.namespace_identifier :u
|
28
|
+
c.env_namespace :s
|
29
|
+
end
|
30
|
+
|
31
|
+
@urn = urn
|
32
|
+
@call_options = call_options
|
33
|
+
@advanced_typecasting = advanced_typecasting
|
34
|
+
end
|
35
|
+
|
36
|
+
def call(action_name, args)
|
37
|
+
attrs = {
|
38
|
+
soap_action: "#{@urn}##{action_name}",
|
39
|
+
attributes: {
|
40
|
+
:'xmlns:u' => @urn
|
41
|
+
},
|
42
|
+
}.merge(@call_options)
|
43
|
+
|
44
|
+
response = @client.call(action_name, attrs) do
|
45
|
+
advanced_typecasting @advanced_typecasting
|
46
|
+
message(args)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'open-uri'
|
3
|
+
require 'nori'
|
4
|
+
|
5
|
+
require_relative 'validator_provider'
|
6
|
+
require_relative 'client_wrapper'
|
7
|
+
require_relative 'service_method'
|
8
|
+
|
9
|
+
module EasyUpnp
|
10
|
+
class DeviceControlPoint
|
11
|
+
attr_reader :service_endpoint
|
12
|
+
|
13
|
+
class Options
|
14
|
+
DEFAULTS = {
|
15
|
+
advanced_typecasting: true,
|
16
|
+
validate_arguments: false,
|
17
|
+
log_enabled: true,
|
18
|
+
log_level: :error,
|
19
|
+
call_options: {}
|
20
|
+
}
|
21
|
+
|
22
|
+
attr_reader :options
|
23
|
+
|
24
|
+
def initialize(o = {}, &block)
|
25
|
+
@options = o.merge(DEFAULTS)
|
26
|
+
|
27
|
+
DEFAULTS.map do |k, v|
|
28
|
+
define_singleton_method(k) do
|
29
|
+
@options[k]
|
30
|
+
end
|
31
|
+
|
32
|
+
define_singleton_method("#{k}=") do |v|
|
33
|
+
@options[k] = v
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
block.call(self) unless block.nil?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(urn, service_endpoint, definition, options, &block)
|
42
|
+
@urn = urn
|
43
|
+
@service_endpoint = service_endpoint
|
44
|
+
@definition = definition
|
45
|
+
@options = Options.new(options, &block)
|
46
|
+
|
47
|
+
@client = ClientWrapper.new(
|
48
|
+
service_endpoint,
|
49
|
+
urn,
|
50
|
+
call_options: @options.call_options,
|
51
|
+
advanced_typecasting: @options.advanced_typecasting,
|
52
|
+
log_enabled: @options.log_enabled,
|
53
|
+
log_level: @options.log_level
|
54
|
+
)
|
55
|
+
|
56
|
+
definition_xml = Nokogiri::XML(definition)
|
57
|
+
definition_xml.remove_namespaces!
|
58
|
+
|
59
|
+
@validator_provider = EasyUpnp::ValidatorProvider.from_xml(definition_xml)
|
60
|
+
|
61
|
+
@service_methods = {}
|
62
|
+
definition_xml.xpath('//actionList/action').map do |action|
|
63
|
+
method = EasyUpnp::ServiceMethod.from_xml(action)
|
64
|
+
@service_methods[method.name] = method
|
65
|
+
|
66
|
+
# Adds a method to the class
|
67
|
+
define_service_method(method, @client, @validator_provider, @options)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def to_params
|
72
|
+
{
|
73
|
+
urn: @urn,
|
74
|
+
service_endpoint: @service_endpoint,
|
75
|
+
definition: @definition,
|
76
|
+
options: @options.options
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.from_params(params)
|
81
|
+
DeviceControlPoint.new(
|
82
|
+
params[:urn],
|
83
|
+
params[:service_endpoint],
|
84
|
+
params[:definition],
|
85
|
+
params[:options]
|
86
|
+
)
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.from_service_definition(definition, options, &block)
|
90
|
+
urn = definition[:st]
|
91
|
+
root_uri = definition[:location]
|
92
|
+
|
93
|
+
xml = Nokogiri::XML(open(root_uri))
|
94
|
+
xml.remove_namespaces!
|
95
|
+
|
96
|
+
service = xml.xpath("//device/serviceList/service[serviceType=\"#{urn}\"]").first
|
97
|
+
|
98
|
+
if service.nil?
|
99
|
+
raise RuntimeError, "Couldn't find service with urn: #{urn}"
|
100
|
+
else
|
101
|
+
service = Nokogiri::XML(service.to_xml)
|
102
|
+
service_definition_uri = URI.join(root_uri, service.xpath('service/SCPDURL').text).to_s
|
103
|
+
service_definition = open(service_definition_uri) { |f| f.read }
|
104
|
+
|
105
|
+
DeviceControlPoint.new(
|
106
|
+
urn,
|
107
|
+
URI.join(root_uri, service.xpath('service/controlURL').text).to_s,
|
108
|
+
service_definition,
|
109
|
+
options,
|
110
|
+
&block
|
111
|
+
)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def arg_validator(method_ref, arg_name)
|
116
|
+
arg_ref = service_method(method_ref).arg_reference(arg_name)
|
117
|
+
raise ArgumentError, "Unknown argument: #{arg_name}" if arg_ref.nil?
|
118
|
+
|
119
|
+
@validator_provider.validator(arg_ref)
|
120
|
+
end
|
121
|
+
|
122
|
+
def method_args(method_ref)
|
123
|
+
service_method(method_ref).in_args
|
124
|
+
end
|
125
|
+
|
126
|
+
def service_method(method_ref)
|
127
|
+
method = @service_methods[method_ref]
|
128
|
+
raise ArgumentError, "Unknown method: #{method_ref}" if method.nil?
|
129
|
+
|
130
|
+
method
|
131
|
+
end
|
132
|
+
|
133
|
+
def service_methods
|
134
|
+
@service_methods.keys
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def define_service_method(method, client, validator_provider, options)
|
140
|
+
if !options.validate_arguments
|
141
|
+
validator_provider = EasyUpnp::ValidatorProvider.no_op_provider
|
142
|
+
end
|
143
|
+
|
144
|
+
define_singleton_method(method.name) do |args_hash = {}|
|
145
|
+
method.call_method(client, args_hash, validator_provider)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module EasyUpnp
|
2
|
+
class ServiceMethod
|
3
|
+
attr_reader :name, :in_args, :out_args
|
4
|
+
|
5
|
+
def initialize(name, in_args, out_args, arg_references)
|
6
|
+
@name = name
|
7
|
+
@in_args = in_args
|
8
|
+
@out_args = out_args
|
9
|
+
@arg_references = arg_references
|
10
|
+
end
|
11
|
+
|
12
|
+
def call_method(client, args_hash, validator_provider)
|
13
|
+
raise ArgumentError, 'Service args must be a hash' unless args_hash.is_a?(Hash)
|
14
|
+
|
15
|
+
present_args = args_hash.keys.map(&:to_sym)
|
16
|
+
|
17
|
+
if (unsupported_args = (present_args - in_args)).any?
|
18
|
+
raise ArgumentError, "Unsupported arguments: #{unsupported_args.join(', ')}." <<
|
19
|
+
" Supported args: #{in_args.join(', ')}"
|
20
|
+
end
|
21
|
+
|
22
|
+
if (missing_args = (in_args - present_args)).any?
|
23
|
+
raise ArgumentError, "Missing required arguments: #{missing_args.join(', ')}"
|
24
|
+
end
|
25
|
+
|
26
|
+
args_hash.each do |arg, val|
|
27
|
+
begin
|
28
|
+
validator_provider.validator(arg_reference(arg)).validate(val)
|
29
|
+
rescue ArgumentError => e
|
30
|
+
raise ArgumentError, "Invalid value for argument #{arg}: #{e}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
raw_response = client.call(name, args_hash)
|
35
|
+
parse_response(raw_response)
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse_response(response)
|
39
|
+
# Response is usually wrapped in <#{ActionName}Response></>. For example:
|
40
|
+
# <BrowseResponse>...</BrowseResponse>. Extract the body since consumers
|
41
|
+
# won't care about wrapper stuff.
|
42
|
+
if response.body.keys.count > 1
|
43
|
+
raise RuntimeError, "Unexpected keys in response body: #{response.body.keys}"
|
44
|
+
end
|
45
|
+
|
46
|
+
result = response.body.first[1]
|
47
|
+
output = {}
|
48
|
+
|
49
|
+
# Keys returned by savon are underscore style. Convert them to camelcase.
|
50
|
+
out_args.map do |arg|
|
51
|
+
output[arg] = result[underscore(arg.to_s).to_sym]
|
52
|
+
end
|
53
|
+
|
54
|
+
output
|
55
|
+
end
|
56
|
+
|
57
|
+
def arg_reference(arg)
|
58
|
+
@arg_references[arg.to_sym]
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.from_xml(xml)
|
62
|
+
name = xml.xpath('name').text.to_sym
|
63
|
+
args = xml.xpath('argumentList')
|
64
|
+
|
65
|
+
arg_references = {}
|
66
|
+
|
67
|
+
extract_args = ->(v) do
|
68
|
+
arg_name = v.xpath('name').text.to_sym
|
69
|
+
ref = v.xpath('relatedStateVariable').text.to_sym
|
70
|
+
|
71
|
+
arg_references[arg_name] = ref
|
72
|
+
arg_name
|
73
|
+
end
|
74
|
+
|
75
|
+
in_args = args.xpath('argument[direction = "in"]').map(&extract_args)
|
76
|
+
out_args = args.xpath('argument[direction = "out"]').map(&extract_args)
|
77
|
+
|
78
|
+
ServiceMethod.new(name, in_args, out_args, arg_references)
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
# This is included in ActiveSupport, but don't want to pull that in for just this method...
|
84
|
+
def underscore(s)
|
85
|
+
s.gsub(/::/, '/').
|
86
|
+
gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
|
87
|
+
gsub(/([a-z\d])([A-Z])/, '\1_\2').
|
88
|
+
tr("-", "_").
|
89
|
+
downcase
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative 'argument_validator'
|
2
|
+
|
3
|
+
module EasyUpnp
|
4
|
+
module ValidatorProvider
|
5
|
+
def self.no_op_provider
|
6
|
+
NoOpValidatorProvider.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.from_xml(xml)
|
10
|
+
validators = {}
|
11
|
+
|
12
|
+
xml.xpath('//serviceStateTable/stateVariable').each do |var|
|
13
|
+
name = var.xpath('name').text.to_sym
|
14
|
+
validators[name] = EasyUpnp::ArgumentValidator.from_xml(var)
|
15
|
+
end
|
16
|
+
|
17
|
+
DefaultValidatorProvider.new(validators)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
class DefaultValidatorProvider
|
22
|
+
def initialize(validators)
|
23
|
+
@validators = validators
|
24
|
+
end
|
25
|
+
|
26
|
+
def validator(arg_ref)
|
27
|
+
validator = @validators[arg_ref.to_sym]
|
28
|
+
raise ArgumentError, "Unknown argument reference: #{arg_ref}" if arg_ref.nil?
|
29
|
+
validator
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class NoOpValidatorProvider
|
34
|
+
def validator(method, arg)
|
35
|
+
ArgumentValidator.no_op
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'socket'
|
2
2
|
require 'ipaddr'
|
3
3
|
require 'timeout'
|
4
|
+
|
4
5
|
require_relative 'upnp_device'
|
5
6
|
|
6
7
|
module EasyUpnp
|
@@ -28,11 +29,11 @@ module EasyUpnp
|
|
28
29
|
@options = DEFAULT_OPTIONS.merge options
|
29
30
|
end
|
30
31
|
|
31
|
-
def option
|
32
|
+
def option(key)
|
32
33
|
@options[key]
|
33
34
|
end
|
34
35
|
|
35
|
-
def search
|
36
|
+
def search(urn = 'ssdp:all')
|
36
37
|
socket = build_socket
|
37
38
|
packet = construct_msearch_packet(urn)
|
38
39
|
|
@@ -78,7 +79,7 @@ ST: #{urn}\r
|
|
78
79
|
MSEARCH
|
79
80
|
end
|
80
81
|
|
81
|
-
def parse_message
|
82
|
+
def parse_message(message)
|
82
83
|
lines = message.split "\r\n"
|
83
84
|
headers = lines.map do |line|
|
84
85
|
if !(match = line.match(/([^:]+):\s?(.*)/i)).nil?
|
@@ -3,7 +3,7 @@ require 'nokogiri'
|
|
3
3
|
require 'open-uri'
|
4
4
|
require 'savon'
|
5
5
|
|
6
|
-
require_relative 'device_control_point'
|
6
|
+
require_relative 'control_point/device_control_point'
|
7
7
|
|
8
8
|
module EasyUpnp
|
9
9
|
class UpnpDevice
|
@@ -34,15 +34,7 @@ module EasyUpnp
|
|
34
34
|
end
|
35
35
|
|
36
36
|
def device_name
|
37
|
-
|
38
|
-
raise RuntimeError, "Couldn't resolve device name because no endpoints are defined"
|
39
|
-
end
|
40
|
-
|
41
|
-
document = open(service_definition(all_services.first)[:location]) { |f| f.read }
|
42
|
-
xml = Nokogiri::XML(document)
|
43
|
-
xml.remove_namespaces!
|
44
|
-
|
45
|
-
xml.xpath("//device/friendlyName").text
|
37
|
+
@device_name ||= fetch_device_name
|
46
38
|
end
|
47
39
|
|
48
40
|
def all_services
|
@@ -66,5 +58,19 @@ module EasyUpnp
|
|
66
58
|
reject { |s| s[:st] != urn }.
|
67
59
|
first
|
68
60
|
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def fetch_device_name
|
65
|
+
if all_services.empty?
|
66
|
+
raise RuntimeError, "Couldn't resolve device name because no endpoints are defined"
|
67
|
+
end
|
68
|
+
|
69
|
+
document = open(service_definition(all_services.first)[:location]) { |f| f.read }
|
70
|
+
xml = Nokogiri::XML(document)
|
71
|
+
xml.remove_namespaces!
|
72
|
+
|
73
|
+
xml.xpath("//device/friendlyName").text
|
74
|
+
end
|
69
75
|
end
|
70
76
|
end
|
data/lib/easy_upnp/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: easy_upnp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christopher Mullins
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-04-
|
11
|
+
date: 2016-04-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -30,56 +30,28 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 2.11
|
33
|
+
version: '2.11'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 2.11
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: nori
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - "~>"
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: 2.6.0
|
48
|
-
type: :runtime
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - "~>"
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: 2.6.0
|
40
|
+
version: '2.11'
|
55
41
|
- !ruby/object:Gem::Dependency
|
56
42
|
name: nokogiri
|
57
43
|
requirement: !ruby/object:Gem::Requirement
|
58
44
|
requirements:
|
59
45
|
- - "~>"
|
60
46
|
- !ruby/object:Gem::Version
|
61
|
-
version: 1.6
|
47
|
+
version: '1.6'
|
62
48
|
type: :runtime
|
63
49
|
prerelease: false
|
64
50
|
version_requirements: !ruby/object:Gem::Requirement
|
65
51
|
requirements:
|
66
52
|
- - "~>"
|
67
53
|
- !ruby/object:Gem::Version
|
68
|
-
version: 1.6
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: rspec
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - ">="
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: 2.0.0
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - ">="
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: 2.0.0
|
54
|
+
version: '1.6'
|
83
55
|
description:
|
84
56
|
email: chris@sidoh.org
|
85
57
|
executables:
|
@@ -94,9 +66,11 @@ files:
|
|
94
66
|
- Rakefile
|
95
67
|
- bin/upnp-list
|
96
68
|
- easy_upnp.gemspec
|
97
|
-
- lib/easy_upnp/argument_validator.rb
|
98
|
-
- lib/easy_upnp/
|
99
|
-
- lib/easy_upnp/
|
69
|
+
- lib/easy_upnp/control_point/argument_validator.rb
|
70
|
+
- lib/easy_upnp/control_point/client_wrapper.rb
|
71
|
+
- lib/easy_upnp/control_point/device_control_point.rb
|
72
|
+
- lib/easy_upnp/control_point/service_method.rb
|
73
|
+
- lib/easy_upnp/control_point/validator_provider.rb
|
100
74
|
- lib/easy_upnp/ssdp_searcher.rb
|
101
75
|
- lib/easy_upnp/upnp_device.rb
|
102
76
|
- lib/easy_upnp/version.rb
|
@@ -1,254 +0,0 @@
|
|
1
|
-
require 'nokogiri'
|
2
|
-
require 'open-uri'
|
3
|
-
require 'nori'
|
4
|
-
|
5
|
-
require_relative 'logger'
|
6
|
-
require_relative 'argument_validator'
|
7
|
-
|
8
|
-
module EasyUpnp
|
9
|
-
class DeviceControlPoint
|
10
|
-
attr_reader :service_methods, :service_endpoint
|
11
|
-
|
12
|
-
class Options
|
13
|
-
DEFAULTS = {
|
14
|
-
advanced_typecasting: true,
|
15
|
-
validate_arguments: false
|
16
|
-
}
|
17
|
-
|
18
|
-
attr_reader :options
|
19
|
-
|
20
|
-
def initialize(o = {}, &block)
|
21
|
-
@options = o.merge(DEFAULTS)
|
22
|
-
|
23
|
-
@options.map do |k, v|
|
24
|
-
define_singleton_method(k) do
|
25
|
-
@options[k]
|
26
|
-
end
|
27
|
-
|
28
|
-
define_singleton_method("#{k}=") do |v|
|
29
|
-
@options[k] = v
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
block.call(self) unless block.nil?
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
def initialize(urn, service_endpoint, definition, call_options, &block)
|
38
|
-
@urn = urn
|
39
|
-
@service_endpoint = service_endpoint
|
40
|
-
@call_options = call_options
|
41
|
-
@definition = definition
|
42
|
-
@options = Options.new(&block)
|
43
|
-
|
44
|
-
@client = Savon.client(log: EasyUpnp::Log.enabled?, log_level: EasyUpnp::Log.level) do |c|
|
45
|
-
c.endpoint service_endpoint
|
46
|
-
c.namespace urn
|
47
|
-
|
48
|
-
# I found this was necessary on some of my UPnP devices (namely, a Sony TV).
|
49
|
-
c.namespaces({:'s:encodingStyle' => "http://schemas.xmlsoap.org/soap/encoding/"})
|
50
|
-
|
51
|
-
# This makes XML tags be like <ObjectID> instead of <objectID>.
|
52
|
-
c.convert_request_keys_to :camelcase
|
53
|
-
|
54
|
-
c.namespace_identifier :u
|
55
|
-
c.env_namespace :s
|
56
|
-
end
|
57
|
-
|
58
|
-
definition_xml = Nokogiri::XML(definition)
|
59
|
-
definition_xml.remove_namespaces!
|
60
|
-
|
61
|
-
service_methods = []
|
62
|
-
service_methods_args = {}
|
63
|
-
definition_xml.xpath('//actionList/action').map do |action|
|
64
|
-
service_methods.push(define_action(action))
|
65
|
-
|
66
|
-
name = action.xpath('name').text
|
67
|
-
args = {}
|
68
|
-
action.xpath('argumentList/argument').map do |arg|
|
69
|
-
arg_name = arg.xpath('name').text
|
70
|
-
arg_ref = arg.xpath('relatedStateVariable').text
|
71
|
-
arg_dir = arg.xpath('direction').text
|
72
|
-
|
73
|
-
if direction == 'in'
|
74
|
-
args[arg_name.to_sym] = arg_ref.to_sym
|
75
|
-
end
|
76
|
-
end
|
77
|
-
service_methods_args[name.to_sym] = args
|
78
|
-
end
|
79
|
-
|
80
|
-
arg_validators = {}
|
81
|
-
definition_xml.xpath('//serviceStateTable/stateVariable').map do |var|
|
82
|
-
name = var.xpath('name').text
|
83
|
-
arg_validators[name.to_sym] = extract_validators(var)
|
84
|
-
end
|
85
|
-
|
86
|
-
@service_methods = service_methods
|
87
|
-
@service_methods_args = service_methods_args
|
88
|
-
@arg_validators = arg_validators
|
89
|
-
end
|
90
|
-
|
91
|
-
def to_params
|
92
|
-
{
|
93
|
-
urn: @urn,
|
94
|
-
service_endpoint: @service_endpoint,
|
95
|
-
definition: @definition,
|
96
|
-
call_options: @call_options,
|
97
|
-
options: @options.options
|
98
|
-
}
|
99
|
-
end
|
100
|
-
|
101
|
-
def self.from_params(params)
|
102
|
-
DeviceControlPoint.new(
|
103
|
-
params[:urn],
|
104
|
-
params[:service_endpoint],
|
105
|
-
params[:definition],
|
106
|
-
params[:call_options]
|
107
|
-
) { |c|
|
108
|
-
params[:options].map { |k, v| c.options[k] = v }
|
109
|
-
}
|
110
|
-
end
|
111
|
-
|
112
|
-
def self.from_service_definition(definition, call_options = {}, &block)
|
113
|
-
urn = definition[:st]
|
114
|
-
root_uri = definition[:location]
|
115
|
-
|
116
|
-
xml = Nokogiri::XML(open(root_uri))
|
117
|
-
xml.remove_namespaces!
|
118
|
-
|
119
|
-
service = xml.xpath("//device/serviceList/service[serviceType=\"#{urn}\"]").first
|
120
|
-
|
121
|
-
if service.nil?
|
122
|
-
raise RuntimeError.new "Couldn't find service with urn: #{urn}"
|
123
|
-
else
|
124
|
-
service = Nokogiri::XML(service.to_xml)
|
125
|
-
service_definition_uri = URI.join(root_uri, service.xpath('service/SCPDURL').text).to_s
|
126
|
-
service_definition = open(service_definition_uri) { |f| f.read }
|
127
|
-
|
128
|
-
DeviceControlPoint.new(
|
129
|
-
urn,
|
130
|
-
URI.join(root_uri, service.xpath('service/controlURL').text).to_s,
|
131
|
-
service_definition,
|
132
|
-
call_options,
|
133
|
-
&block
|
134
|
-
)
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
def arg_validator(method, arg)
|
139
|
-
method_args = @service_methods_args[method.to_sym]
|
140
|
-
raise ArgumentError, "Unknown method: #{method}" if method_args.nil?
|
141
|
-
|
142
|
-
arg_ref = method_args[arg.to_sym]
|
143
|
-
raise ArgumentError, "Unknown argument: #{arg}" if arg_ref.nil?
|
144
|
-
|
145
|
-
@arg_validators[arg_ref]
|
146
|
-
end
|
147
|
-
|
148
|
-
def method_args(method)
|
149
|
-
method_args = @service_methods_args[method.to_sym]
|
150
|
-
raise ArgumentError, "Unknown method: #{method}" if method_args.nil?
|
151
|
-
|
152
|
-
method_args.keys
|
153
|
-
end
|
154
|
-
|
155
|
-
private
|
156
|
-
|
157
|
-
def extract_validators(var)
|
158
|
-
ArgumentValidator.build do |v|
|
159
|
-
v.type(var.xpath('dataType').text)
|
160
|
-
|
161
|
-
if (range = var.xpath('allowedValueRange')).any?
|
162
|
-
min = range.xpath('minimum').text
|
163
|
-
max = range.xpath('maximum').text
|
164
|
-
step = range.xpath('step')
|
165
|
-
step = step.any? ? step.text : 1
|
166
|
-
|
167
|
-
v.in_range(min.to_i, max.to_i, step.to_i)
|
168
|
-
end
|
169
|
-
|
170
|
-
if (list = var.xpath('allowedValueList')).any?
|
171
|
-
allowed_values = list.xpath('allowedValue').map { |x| x.text }
|
172
|
-
v.allowed_values(*allowed_values)
|
173
|
-
end
|
174
|
-
end
|
175
|
-
end
|
176
|
-
|
177
|
-
def define_action(action)
|
178
|
-
action = Nori.new.parse(action.to_xml)['action']
|
179
|
-
action_name = action['name']
|
180
|
-
args = action['argumentList']['argument']
|
181
|
-
args = [args] unless args.is_a? Array
|
182
|
-
|
183
|
-
input_args = args.
|
184
|
-
reject { |x| x['direction'] != 'in' }.
|
185
|
-
map { |x| x['name'].to_sym }
|
186
|
-
output_args = args.
|
187
|
-
reject { |x| x['direction'] != 'out' }.
|
188
|
-
map { |x| x['name'].to_sym }
|
189
|
-
|
190
|
-
define_singleton_method(action['name']) do |args_hash = {}|
|
191
|
-
if !args_hash.is_a? Hash
|
192
|
-
raise RuntimeError.new "Input arg must be a hash"
|
193
|
-
end
|
194
|
-
|
195
|
-
args_hash = args_hash.inject({}) { |m,(k,v)| m[k.to_sym] = v; m }
|
196
|
-
|
197
|
-
if (args_hash.keys - input_args).any?
|
198
|
-
raise RuntimeError.new "Unsupported arguments: #{(args_hash.keys - input_args)}." <<
|
199
|
-
" Supported args: #{input_args}"
|
200
|
-
end
|
201
|
-
|
202
|
-
if @options.validate_arguments
|
203
|
-
args_hash.each { |k,v|
|
204
|
-
begin
|
205
|
-
arg_validator(action['name'], k).validate(v)
|
206
|
-
rescue ArgumentError => e
|
207
|
-
raise ArgumentError, "Invalid value for argument #{k}: #{e}"
|
208
|
-
end
|
209
|
-
}
|
210
|
-
end
|
211
|
-
|
212
|
-
attrs = {
|
213
|
-
soap_action: "#{@urn}##{action_name}",
|
214
|
-
attributes: {
|
215
|
-
:'xmlns:u' => @urn
|
216
|
-
},
|
217
|
-
}.merge(@call_options)
|
218
|
-
|
219
|
-
options = @options
|
220
|
-
response = @client.call action['name'], attrs do
|
221
|
-
advanced_typecasting options.advanced_typecasting
|
222
|
-
message(args_hash)
|
223
|
-
end
|
224
|
-
|
225
|
-
# Response is usually wrapped in <#{ActionName}Response></>. For example:
|
226
|
-
# <BrowseResponse>...</BrowseResponse>. Extract the body since consumers
|
227
|
-
# won't care about wrapper stuff.
|
228
|
-
if response.body.keys.count > 1
|
229
|
-
raise RuntimeError.new "Unexpected keys in response body: #{response.body.keys}"
|
230
|
-
end
|
231
|
-
result = response.body.first[1]
|
232
|
-
output = {}
|
233
|
-
|
234
|
-
# Keys returned by savon are underscore style. Convert them to camelcase.
|
235
|
-
output_args.map do |arg|
|
236
|
-
output[arg] = result[underscore(arg.to_s).to_sym]
|
237
|
-
end
|
238
|
-
|
239
|
-
output
|
240
|
-
end
|
241
|
-
|
242
|
-
action['name']
|
243
|
-
end
|
244
|
-
|
245
|
-
# This is included in ActiveSupport, but don't want to pull that in for just this method...
|
246
|
-
def underscore s
|
247
|
-
s.gsub(/::/, '/').
|
248
|
-
gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
|
249
|
-
gsub(/([a-z\d])([A-Z])/, '\1_\2').
|
250
|
-
tr("-", "_").
|
251
|
-
downcase
|
252
|
-
end
|
253
|
-
end
|
254
|
-
end
|
data/lib/easy_upnp/logger.rb
DELETED