easy_upnp 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/.gitignore +35 -0
- data/Gemfile +6 -0
- data/LICENSE +22 -0
- data/README.md +2 -0
- data/easy_upnp.gemspec +32 -0
- data/lib/easy_upnp/device_control_point.rb +78 -0
- data/lib/easy_upnp/ssdp_searcher.rb +127 -0
- data/lib/easy_upnp/upnp_device.rb +77 -0
- data/lib/easy_upnp/version.rb +3 -0
- data/lib/easy_upnp.rb +3 -0
- metadata +122 -0
    
        checksums.yaml
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            !binary "U0hBMQ==":
         | 
| 3 | 
            +
              metadata.gz: 58750344e60c3c1ccba0ec054ea0a8e1707b70b1
         | 
| 4 | 
            +
              data.tar.gz: d73392dec5230c617f2f1cca4596e9950fd67a79
         | 
| 5 | 
            +
            !binary "U0hBNTEy":
         | 
| 6 | 
            +
              metadata.gz: c9f9def01cb5147d365b7edebe1cb1f27efc55c3f039f2e17111417f6ecfbab1e6104e05cbebf4f63dfa4f0ade3785874669a1751f9e29ce0c581daba6011648
         | 
| 7 | 
            +
              data.tar.gz: e2ae7f65a0616476d9e8c593eb753d5b766372e62a03e18309f4f30de87b7d361940b1ab62c09c0e86ac65a61edad6c689ce9f3bb7fbafedc6a2fe4d019fc234
         | 
    
        data/.gitignore
    ADDED
    
    | @@ -0,0 +1,35 @@ | |
| 1 | 
            +
            *.gem
         | 
| 2 | 
            +
            *.rbc
         | 
| 3 | 
            +
            /.config
         | 
| 4 | 
            +
            /coverage/
         | 
| 5 | 
            +
            /InstalledFiles
         | 
| 6 | 
            +
            /pkg/
         | 
| 7 | 
            +
            /spec/reports/
         | 
| 8 | 
            +
            /test/tmp/
         | 
| 9 | 
            +
            /test/version_tmp/
         | 
| 10 | 
            +
            /tmp/
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            ## Specific to RubyMotion:
         | 
| 13 | 
            +
            .dat*
         | 
| 14 | 
            +
            .repl_history
         | 
| 15 | 
            +
            build/
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            ## Documentation cache and generated files:
         | 
| 18 | 
            +
            /.yardoc/
         | 
| 19 | 
            +
            /_yardoc/
         | 
| 20 | 
            +
            /doc/
         | 
| 21 | 
            +
            /rdoc/
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            ## Environment normalisation:
         | 
| 24 | 
            +
            /.bundle/
         | 
| 25 | 
            +
            /vendor/bundle
         | 
| 26 | 
            +
            /lib/bundler/man/
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            # for a library or gem, you might want to ignore these files since the code is
         | 
| 29 | 
            +
            # intended to run in multiple environments; otherwise, check them in:
         | 
| 30 | 
            +
            Gemfile.lock
         | 
| 31 | 
            +
            .ruby-version
         | 
| 32 | 
            +
            .ruby-gemset
         | 
| 33 | 
            +
             | 
| 34 | 
            +
            # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
         | 
| 35 | 
            +
            .rvmrc
         | 
    
        data/Gemfile
    ADDED
    
    
    
        data/LICENSE
    ADDED
    
    | @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            The MIT License (MIT)
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            Copyright (c) 2015 Chris Mullins
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Permission is hereby granted, free of charge, to any person obtaining a copy
         | 
| 6 | 
            +
            of this software and associated documentation files (the "Software"), to deal
         | 
| 7 | 
            +
            in the Software without restriction, including without limitation the rights
         | 
| 8 | 
            +
            to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
         | 
| 9 | 
            +
            copies of the Software, and to permit persons to whom the Software is
         | 
| 10 | 
            +
            furnished to do so, subject to the following conditions:
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            The above copyright notice and this permission notice shall be included in all
         | 
| 13 | 
            +
            copies or substantial portions of the Software.
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
         | 
| 16 | 
            +
            IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
         | 
| 17 | 
            +
            FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
         | 
| 18 | 
            +
            AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
         | 
| 19 | 
            +
            LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
         | 
| 20 | 
            +
            OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
         | 
| 21 | 
            +
            SOFTWARE.
         | 
| 22 | 
            +
             | 
    
        data/README.md
    ADDED
    
    
    
        data/easy_upnp.gemspec
    ADDED
    
    | @@ -0,0 +1,32 @@ | |
| 1 | 
            +
            $:.push File.expand_path('../lib', __FILE__)
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "easy_upnp/version"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Gem::Specification.new do |gem|
         | 
| 6 | 
            +
              gem.name    = 'easy_upnp'
         | 
| 7 | 
            +
              gem.version = EasyUpnp::VERSION
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              gem.summary = "A super easy to use UPnP control point client"
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              gem.authors  = ['Christopher Mullins']
         | 
| 12 | 
            +
              gem.email    = 'chris@sidoh.org'
         | 
| 13 | 
            +
              gem.homepage = 'http://github.com/sidoh/easy-upnp'
         | 
| 14 | 
            +
             | 
| 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"])
         | 
| 21 | 
            +
             | 
| 22 | 
            +
              ignores  = File.readlines(".gitignore").grep(/\S+/).map(&:chomp)
         | 
| 23 | 
            +
              dotfiles = %w[.gitignore]
         | 
| 24 | 
            +
             | 
| 25 | 
            +
              all_files_without_ignores = Dir["**/*"].reject { |f|
         | 
| 26 | 
            +
                File.directory?(f) || ignores.any? { |i| File.fnmatch(i, f) }
         | 
| 27 | 
            +
              }
         | 
| 28 | 
            +
             | 
| 29 | 
            +
              gem.files = (all_files_without_ignores + dotfiles).sort
         | 
| 30 | 
            +
             | 
| 31 | 
            +
              gem.require_path = "lib" 
         | 
| 32 | 
            +
            end
         | 
| @@ -0,0 +1,78 @@ | |
| 1 | 
            +
            require 'nokogiri'
         | 
| 2 | 
            +
            require 'open-uri'
         | 
| 3 | 
            +
            require 'nori'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module EasyUpnp
         | 
| 6 | 
            +
              class DeviceControlPoint
         | 
| 7 | 
            +
                def initialize client, service_type, definition_url
         | 
| 8 | 
            +
                  @client = client
         | 
| 9 | 
            +
                  @service_type = service_type
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  definition = Nokogiri::XML(open(definition_url))
         | 
| 12 | 
            +
                  definition.remove_namespaces!
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  definition.xpath('//actionList/action').map do |action|
         | 
| 15 | 
            +
                    define_action action
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                private
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def define_action action
         | 
| 22 | 
            +
                  action = Nori.new.parse(action.to_xml)['action']
         | 
| 23 | 
            +
                  action_name = action['name']
         | 
| 24 | 
            +
                  args = action['argumentList']['argument']
         | 
| 25 | 
            +
                  args = [args] unless args.is_a? Array
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  input_args = args.
         | 
| 28 | 
            +
                      reject { |x| x['direction'] != 'in' }.
         | 
| 29 | 
            +
                      map { |x| x['name'].to_sym }
         | 
| 30 | 
            +
                  output_args = args.
         | 
| 31 | 
            +
                      reject { |x| x['direction'] != 'out' }.
         | 
| 32 | 
            +
                      map { |x| x['name'].to_sym }
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  define_singleton_method(action['name']) do |args_hash|
         | 
| 35 | 
            +
                    if (args_hash.keys - input_args).any?
         | 
| 36 | 
            +
                      raise RuntimeError.new "Unsupported arguments: #{(args_hash.keys - input_args)}." <<
         | 
| 37 | 
            +
                                                 " Supported args: #{input_args}"
         | 
| 38 | 
            +
                    end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    attrs = {
         | 
| 41 | 
            +
                        soap_action: "#{@service_type}##{action_name}",
         | 
| 42 | 
            +
                        attributes: {
         | 
| 43 | 
            +
                            :'xmlns:u' => @service_type
         | 
| 44 | 
            +
                        }
         | 
| 45 | 
            +
                    }
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    response = @client.call action['name'], attrs do
         | 
| 48 | 
            +
                      message(args_hash)
         | 
| 49 | 
            +
                    end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                    # Response is usually wrapped in <#{ActionName}Response></>. For example:
         | 
| 52 | 
            +
                    # <BrowseResponse>...</BrowseResponse>. Extract the body since consumers
         | 
| 53 | 
            +
                    # won't care about wrapper stuff.
         | 
| 54 | 
            +
                    if response.body.keys.count > 1
         | 
| 55 | 
            +
                      raise RuntimeError.new "Unexpected keys in response body: #{response.body.keys}"
         | 
| 56 | 
            +
                    end
         | 
| 57 | 
            +
                    result = response.body.first[1]
         | 
| 58 | 
            +
                    output = {}
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                    # Keys returned by savon are underscore style. Convert them to camelcase.
         | 
| 61 | 
            +
                    output_args.map do |arg|
         | 
| 62 | 
            +
                      output[arg] = result[underscore(arg.to_s).to_sym]
         | 
| 63 | 
            +
                    end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                    output
         | 
| 66 | 
            +
                  end
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                # This is included in ActiveSupport, but don't want to pull that in for just this method...
         | 
| 70 | 
            +
                def underscore s
         | 
| 71 | 
            +
                  s.gsub(/::/, '/').
         | 
| 72 | 
            +
                      gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
         | 
| 73 | 
            +
                      gsub(/([a-z\d])([A-Z])/, '\1_\2').
         | 
| 74 | 
            +
                      tr("-", "_").
         | 
| 75 | 
            +
                      downcase
         | 
| 76 | 
            +
                end
         | 
| 77 | 
            +
              end
         | 
| 78 | 
            +
            end
         | 
| @@ -0,0 +1,127 @@ | |
| 1 | 
            +
            require 'socket'
         | 
| 2 | 
            +
            require 'ipaddr'
         | 
| 3 | 
            +
            require 'timeout'
         | 
| 4 | 
            +
            require_relative 'upnp_device'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module EasyUpnp
         | 
| 7 | 
            +
              class SsdpSearcher
         | 
| 8 | 
            +
                # These are dictated by the SSDP protocol and cannot be changed
         | 
| 9 | 
            +
                MULTICAST_ADDR = '239.255.255.250'
         | 
| 10 | 
            +
                MULTICAST_PORT = 1900
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                DEFAULT_OPTIONS = {
         | 
| 13 | 
            +
                    :bind_addr => '0.0.0.0',
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                    # Number of seconds to wait for responses
         | 
| 16 | 
            +
                    :timeout => 2,
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                    # Part of the SSDP protocol. Servers should delay a random amount of time between 0 and N
         | 
| 19 | 
            +
                    # seconds before sending the response.
         | 
| 20 | 
            +
                    :mx => 1,
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    # Sometimes recommended to send repeat M-SEARCH queries. Control that here.
         | 
| 23 | 
            +
                    :repeat_queries => 1
         | 
| 24 | 
            +
                }
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def initialize(options = {})
         | 
| 27 | 
            +
                  unsupported_args = options.keys.reject { |x| DEFAULT_OPTIONS[x] }
         | 
| 28 | 
            +
                  raise RuntimeError.new "Unsupported arguments: #{unsupported_args}" if unsupported_args.any?
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  @options = DEFAULT_OPTIONS.merge options
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def option key
         | 
| 34 | 
            +
                  @options[key]
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                def search urn
         | 
| 38 | 
            +
                  listen_socket = build_listen_socket
         | 
| 39 | 
            +
                  send_socket = build_send_socket
         | 
| 40 | 
            +
                  packet = construct_msearch_packet(urn)
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  # Send M-SEARCH packet over UDP socket
         | 
| 43 | 
            +
                  option(:repeat_queries).times do
         | 
| 44 | 
            +
                    send_socket.send packet, 0, MULTICAST_ADDR, MULTICAST_PORT
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  raw_messages = []
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                  # Wait for responses. Timeout after a specified number of seconds
         | 
| 50 | 
            +
                  begin
         | 
| 51 | 
            +
                    Timeout::timeout(option :timeout) do
         | 
| 52 | 
            +
                      loop do
         | 
| 53 | 
            +
                        raw_messages.push send_socket.recv(4196)
         | 
| 54 | 
            +
                      end
         | 
| 55 | 
            +
                    end
         | 
| 56 | 
            +
                  rescue Timeout::Error
         | 
| 57 | 
            +
                    # This is expected
         | 
| 58 | 
            +
                  ensure
         | 
| 59 | 
            +
                    send_socket.close
         | 
| 60 | 
            +
                    listen_socket.close
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  # Parse messages (extract HTTP headers)
         | 
| 64 | 
            +
                  parsed_messages = raw_messages.map { |x| parse_message x }
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  # Group messages by device they come from (identified by a UUID in the 'USN' header),
         | 
| 67 | 
            +
                  # and create UpnpDevices for them. This wrap the services advertized by the SSDP
         | 
| 68 | 
            +
                  # results.
         | 
| 69 | 
            +
                  parsed_messages.reject { |x| !x[:usn] }.group_by { |x| x[:usn].split('::').first }.map do |k, v|
         | 
| 70 | 
            +
                    UpnpDevice.new k, v
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                def construct_msearch_packet(urn)
         | 
| 75 | 
            +
                  <<-MSEARCH
         | 
| 76 | 
            +
            M-SEARCH * HTTP/1.1\r
         | 
| 77 | 
            +
            HOST: #{MULTICAST_ADDR}:#{MULTICAST_PORT}\r
         | 
| 78 | 
            +
            MAN: "ssdp:discover"\r
         | 
| 79 | 
            +
            MX: #{option :mx}\r
         | 
| 80 | 
            +
            ST: #{urn}\r
         | 
| 81 | 
            +
            \r
         | 
| 82 | 
            +
                  MSEARCH
         | 
| 83 | 
            +
                end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                def parse_message message
         | 
| 86 | 
            +
                  lines = message.split "\r\n"
         | 
| 87 | 
            +
                  headers = lines[1...-1].map do |line|
         | 
| 88 | 
            +
                    header, value = line.match(/([^:]+):\s?(.*)/i).captures
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                    key = header.
         | 
| 91 | 
            +
                        downcase.
         | 
| 92 | 
            +
                        gsub('-', '_').
         | 
| 93 | 
            +
                        to_sym
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                    [key, value]
         | 
| 96 | 
            +
                  end
         | 
| 97 | 
            +
                  Hash[headers]
         | 
| 98 | 
            +
                end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                private
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                def build_listen_socket
         | 
| 103 | 
            +
                  socket = UDPSocket.new
         | 
| 104 | 
            +
                  socket.do_not_reverse_lookup = true
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                  membership = IPAddr.new(MULTICAST_ADDR).hton + IPAddr.new(option :bind_addr).hton
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                  socket.setsockopt(:IPPROTO_IP, :IP_ADD_MEMBERSHIP, membership)
         | 
| 109 | 
            +
                  socket.setsockopt(:SOL_SOCKET, :SO_REUSEADDR, true)
         | 
| 110 | 
            +
                  socket.setsockopt(:IPPROTO_IP, :IP_TTL, 1)
         | 
| 111 | 
            +
             | 
| 112 | 
            +
                  socket.bind(option(:bind_addr), MULTICAST_PORT)
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                  socket
         | 
| 115 | 
            +
                end
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                def build_send_socket
         | 
| 118 | 
            +
                  socket = UDPSocket.open
         | 
| 119 | 
            +
                  socket.do_not_reverse_lookup = true
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                  socket.setsockopt(:IPPROTO_IP, :IP_MULTICAST_TTL, true)
         | 
| 122 | 
            +
                  socket.setsockopt(:SOL_SOCKET, :SO_REUSEADDR, true)
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                  socket
         | 
| 125 | 
            +
                end
         | 
| 126 | 
            +
              end
         | 
| 127 | 
            +
              end
         | 
| @@ -0,0 +1,77 @@ | |
| 1 | 
            +
            require 'uri'
         | 
| 2 | 
            +
            require 'nokogiri'
         | 
| 3 | 
            +
            require 'open-uri'
         | 
| 4 | 
            +
            require 'savon'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            require_relative 'device_control_point'
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            module EasyUpnp
         | 
| 9 | 
            +
              class UpnpDevice
         | 
| 10 | 
            +
                attr_reader :uuid
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def initialize uuid, messages
         | 
| 13 | 
            +
                  @uuid = uuid
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  @service_definitions = messages.
         | 
| 16 | 
            +
                      # Filter out messages that aren't service definitions. These include
         | 
| 17 | 
            +
                      # the root device and the root UUID
         | 
| 18 | 
            +
                      reject { |message| not message[:st].include? ':service:' }.
         | 
| 19 | 
            +
                      # Distinct by ST header -- might have repeats if we sent multiple
         | 
| 20 | 
            +
                      # M-SEARCH packets
         | 
| 21 | 
            +
                      group_by { |message| message[:st] }.
         | 
| 22 | 
            +
                      map { |_, matching_messages| matching_messages.first }.
         | 
| 23 | 
            +
                      map do |message|
         | 
| 24 | 
            +
                    {
         | 
| 25 | 
            +
                        :location => message[:location],
         | 
| 26 | 
            +
                        :st => message[:st]
         | 
| 27 | 
            +
                    }
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def has_service?(urn)
         | 
| 32 | 
            +
                  !service_definition(urn).nil?
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def service(urn)
         | 
| 36 | 
            +
                  definition = service_definition(urn)
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  if !definition.nil?
         | 
| 39 | 
            +
                    root_uri = definition[:location]
         | 
| 40 | 
            +
                    xml = Nokogiri::XML(open(root_uri))
         | 
| 41 | 
            +
                    xml.remove_namespaces!
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                    service = xml.xpath("//device/serviceList/service[serviceType=\"#{urn}\"]").first
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                    if service.nil?
         | 
| 46 | 
            +
                      raise RuntimeError.new "Couldn't find service with urn: #{urn}"
         | 
| 47 | 
            +
                    else
         | 
| 48 | 
            +
                      service = Nokogiri::XML(service.to_xml)
         | 
| 49 | 
            +
                      wsdl = URI.join(root_uri, service.xpath('service/SCPDURL').text).to_s
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                      client = Savon.client do |c|
         | 
| 52 | 
            +
                        c.endpoint URI.join(root_uri, service.xpath('service/controlURL').text).to_s
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                        c.namespace urn
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                        # I found this was necessary on some of my UPnP devices (namely, a Sony TV).
         | 
| 57 | 
            +
                        c.namespaces({:'s:encodingStyle' => "http://schemas.xmlsoap.org/soap/encoding/"})
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                        # This makes XML tags be like <ObjectID> instead of <objectID>.
         | 
| 60 | 
            +
                        c.convert_request_keys_to :camelcase
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                        c.namespace_identifier :u
         | 
| 63 | 
            +
                        c.env_namespace :s
         | 
| 64 | 
            +
                      end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                      DeviceControlPoint.new client, urn, wsdl
         | 
| 67 | 
            +
                    end
         | 
| 68 | 
            +
                  end
         | 
| 69 | 
            +
                end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                def service_definition(urn)
         | 
| 72 | 
            +
                  @service_definitions.
         | 
| 73 | 
            +
                      reject { |s| s[:st] == urn }.
         | 
| 74 | 
            +
                      first
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
              end
         | 
| 77 | 
            +
            end
         | 
    
        data/lib/easy_upnp.rb
    ADDED
    
    
    
        metadata
    ADDED
    
    | @@ -0,0 +1,122 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: easy_upnp
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 0.1.0
         | 
| 5 | 
            +
            platform: ruby
         | 
| 6 | 
            +
            authors:
         | 
| 7 | 
            +
            - Christopher Mullins
         | 
| 8 | 
            +
            autorequire: 
         | 
| 9 | 
            +
            bindir: bin
         | 
| 10 | 
            +
            cert_chain: []
         | 
| 11 | 
            +
            date: 2015-06-26 00:00:00.000000000 Z
         | 
| 12 | 
            +
            dependencies:
         | 
| 13 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 14 | 
            +
              name: rake
         | 
| 15 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 16 | 
            +
                requirements:
         | 
| 17 | 
            +
                - - ">="
         | 
| 18 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 19 | 
            +
                    version: '0'
         | 
| 20 | 
            +
              type: :runtime
         | 
| 21 | 
            +
              prerelease: false
         | 
| 22 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 23 | 
            +
                requirements:
         | 
| 24 | 
            +
                - - ">="
         | 
| 25 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 26 | 
            +
                    version: '0'
         | 
| 27 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 28 | 
            +
              name: savon
         | 
| 29 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 30 | 
            +
                requirements:
         | 
| 31 | 
            +
                - - "~>"
         | 
| 32 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 33 | 
            +
                    version: 2.11.1
         | 
| 34 | 
            +
              type: :runtime
         | 
| 35 | 
            +
              prerelease: false
         | 
| 36 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 37 | 
            +
                requirements:
         | 
| 38 | 
            +
                - - "~>"
         | 
| 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
         | 
| 55 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 56 | 
            +
              name: nokogiri
         | 
| 57 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 58 | 
            +
                requirements:
         | 
| 59 | 
            +
                - - "~>"
         | 
| 60 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 61 | 
            +
                    version: 1.6.6.2
         | 
| 62 | 
            +
              type: :runtime
         | 
| 63 | 
            +
              prerelease: false
         | 
| 64 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 65 | 
            +
                requirements:
         | 
| 66 | 
            +
                - - "~>"
         | 
| 67 | 
            +
                  - !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
         | 
| 83 | 
            +
            description: 
         | 
| 84 | 
            +
            email: chris@sidoh.org
         | 
| 85 | 
            +
            executables: []
         | 
| 86 | 
            +
            extensions: []
         | 
| 87 | 
            +
            extra_rdoc_files: []
         | 
| 88 | 
            +
            files:
         | 
| 89 | 
            +
            - ".gitignore"
         | 
| 90 | 
            +
            - Gemfile
         | 
| 91 | 
            +
            - LICENSE
         | 
| 92 | 
            +
            - README.md
         | 
| 93 | 
            +
            - easy_upnp.gemspec
         | 
| 94 | 
            +
            - lib/easy_upnp.rb
         | 
| 95 | 
            +
            - lib/easy_upnp/device_control_point.rb
         | 
| 96 | 
            +
            - lib/easy_upnp/ssdp_searcher.rb
         | 
| 97 | 
            +
            - lib/easy_upnp/upnp_device.rb
         | 
| 98 | 
            +
            - lib/easy_upnp/version.rb
         | 
| 99 | 
            +
            homepage: http://github.com/sidoh/easy-upnp
         | 
| 100 | 
            +
            licenses: []
         | 
| 101 | 
            +
            metadata: {}
         | 
| 102 | 
            +
            post_install_message: 
         | 
| 103 | 
            +
            rdoc_options: []
         | 
| 104 | 
            +
            require_paths:
         | 
| 105 | 
            +
            - lib
         | 
| 106 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 107 | 
            +
              requirements:
         | 
| 108 | 
            +
              - - ">="
         | 
| 109 | 
            +
                - !ruby/object:Gem::Version
         | 
| 110 | 
            +
                  version: '0'
         | 
| 111 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 112 | 
            +
              requirements:
         | 
| 113 | 
            +
              - - ">="
         | 
| 114 | 
            +
                - !ruby/object:Gem::Version
         | 
| 115 | 
            +
                  version: '0'
         | 
| 116 | 
            +
            requirements: []
         | 
| 117 | 
            +
            rubyforge_project: 
         | 
| 118 | 
            +
            rubygems_version: 2.0.0.preview3.1
         | 
| 119 | 
            +
            signing_key: 
         | 
| 120 | 
            +
            specification_version: 4
         | 
| 121 | 
            +
            summary: A super easy to use UPnP control point client
         | 
| 122 | 
            +
            test_files: []
         |