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 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