easy_upnp 0.4.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3b1b0ab93e8ae488f40f517fdb931fbe59b14178
4
- data.tar.gz: eae09c694d7864593a0eb89989078a54b9231914
3
+ metadata.gz: c58d2d816ce224f674b5bca2393a6f25007b27b3
4
+ data.tar.gz: 97eca3f41437f100470a5bb8cdda6815a5fc7959
5
5
  SHA512:
6
- metadata.gz: 441a2af6447b95c95ecb2d4ce840a0a9a6f9f47d02d6e53c4d7b87c1964ddc9062f36e538a9e9c1e94b57f54ed62a79f876cb73d89335b97c9bafe9a418033a5
7
- data.tar.gz: 393c879cd71967dca3cab3f72d6a5b190ac533fc3e77fa6fb708d24bb27d8794a98119d4c932a8a818429ddcb58d144db0d9a2b4d3f649d274ec7afd94705872
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.1'
17
- gem.add_dependency 'nori', '~> 2.6.0'
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 key
32
+ def option(key)
32
33
  @options[key]
33
34
  end
34
35
 
35
- def search urn = 'ssdp:all'
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 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
- if all_services.empty?
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
@@ -1,3 +1,3 @@
1
1
  module EasyUpnp
2
- VERSION = '0.4.4'
2
+ VERSION = '1.0.0'
3
3
  end
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.4
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-12 00:00:00.000000000 Z
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.1
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.1
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.6.2
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.6.2
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/device_control_point.rb
99
- - lib/easy_upnp/logger.rb
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
@@ -1,15 +0,0 @@
1
- module EasyUpnp
2
- module Log
3
- class <<self
4
- attr_accessor :enabled, :level
5
- end
6
-
7
- def self.enabled?
8
- @enabled.nil? ? @enabled = true : @enabled
9
- end
10
-
11
- def self.level
12
- @level ||= :info
13
- end
14
- end
15
- end