frisky 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gemtest +0 -0
- data/.gitignore +19 -0
- data/.rspec +1 -0
- data/.travis.yml +6 -0
- data/Gemfile +23 -0
- data/History.md +3 -0
- data/LICENSE.md +23 -0
- data/README.md +185 -0
- data/Rakefile +16 -0
- data/features/device_discovery.feature +9 -0
- data/features/step_definitions/.gitkeep +0 -0
- data/features/support/env.rb +21 -0
- data/features/support/world_extensions.rb +7 -0
- data/frisky.gemspec +28 -0
- data/lib/core_ext/hash_patch.rb +5 -0
- data/lib/core_ext/socket_patch.rb +16 -0
- data/lib/core_ext/to_upnp_s.rb +65 -0
- data/lib/frisky.rb +5 -0
- data/lib/frisky/logger.rb +8 -0
- data/lib/frisky/ssdp.rb +188 -0
- data/lib/frisky/ssdp/broadcast_searcher.rb +114 -0
- data/lib/frisky/ssdp/error.rb +6 -0
- data/lib/frisky/ssdp/listener.rb +38 -0
- data/lib/frisky/ssdp/multicast_connection.rb +112 -0
- data/lib/frisky/ssdp/network_constants.rb +17 -0
- data/lib/frisky/ssdp/notifier.rb +41 -0
- data/lib/frisky/ssdp/searcher.rb +87 -0
- data/lib/frisky/version.rb +3 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/support/search_responses.rb +134 -0
- data/spec/unit/core_ext/to_upnp_s_spec.rb +105 -0
- data/spec/unit/frisky/ssdp/listener_spec.rb +29 -0
- data/spec/unit/frisky/ssdp/multicast_connection_spec.rb +157 -0
- data/spec/unit/frisky/ssdp/notifier_spec.rb +76 -0
- data/spec/unit/frisky/ssdp/searcher_spec.rb +110 -0
- data/spec/unit/frisky/ssdp_spec.rb +214 -0
- data/tasks/search.thor +35 -0
- metadata +181 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 2deaa661a30948e0d168faa316a7bb81f089ce59
|
4
|
+
data.tar.gz: 1cec1f6c3430a00632c11dfb9c1e2f1a1a49fc89
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8715b77ee7c823c6ecad28f0a2110082e8952db901f91cdba1275a59f9fddf8852c5af0dddd35feb49185355a7ef41a0673a3f6d1685981091223b1df987398c
|
7
|
+
data.tar.gz: a39f0325d37d0c95ef0222baa92db352d5d2097baa4a7389467c7e678d7910ea2c699257a2b286de73eab003f0e2096294f1fc849fd0e41636651c8c52564681
|
data/.gemtest
ADDED
File without changes
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# rubocop:disable Style/LeadingCommentSpace
|
2
|
+
#ruby-gemset=frisky
|
3
|
+
# rubocop:enable Style/LeadingCommentSpace
|
4
|
+
source 'http://rubygems.org'
|
5
|
+
gemspec
|
6
|
+
ruby "2.2.3"
|
7
|
+
|
8
|
+
#gem "httpi", '>=2.0.0.rc1'
|
9
|
+
#gem 'em-synchrony'
|
10
|
+
|
11
|
+
group :development do
|
12
|
+
gem 'coveralls', require: false
|
13
|
+
gem 'cucumber', '>=1.0.0', require: false
|
14
|
+
gem 'em-websocket', '>=0.3.6'
|
15
|
+
gem 'rake', require: false
|
16
|
+
gem 'log_buddy'
|
17
|
+
gem 'rspec', '>=3.0.0.beta', require: false
|
18
|
+
gem 'simplecov', '>=0.4.2', require: false
|
19
|
+
gem 'thin'
|
20
|
+
gem 'thor', '>=0.1.6', require: false
|
21
|
+
gem 'yard', '>=0.7.0', require: false
|
22
|
+
gem "pry", require: false
|
23
|
+
end
|
data/History.md
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
(The MIT License)
|
2
|
+
|
3
|
+
Copyright (c) 2012-2014 Steve Loveless
|
4
|
+
Copyright (c) 2015 Jon Frisby
|
5
|
+
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
7
|
+
a copy of this software and associated documentation files (the
|
8
|
+
'Software'), to deal in the Software without restriction, including
|
9
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
10
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
11
|
+
permit persons to whom the Software is furnished to do so, subject to
|
12
|
+
the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be
|
15
|
+
included in all copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
19
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
20
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
21
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
22
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
23
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,185 @@
|
|
1
|
+
# Frisky
|
2
|
+
|
3
|
+
* [Homepage](http://github.com/MrJoy/frisky)
|
4
|
+
* [UPnP Device Architecture Documentation](http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.0.pdf)
|
5
|
+
|
6
|
+
|
7
|
+
[<img src="https://travis-ci.org/MrJoy/frisky.png?branch=master" alt="Build Status" />](https://travis-ci.org/MrJoy/frisky) [<img src="https://coveralls.io/repos/MrJoy/frisky/badge.png" alt="Coverage Status" />](https://coveralls.io/r/MrJoy/frisky)
|
8
|
+
|
9
|
+
## Description
|
10
|
+
|
11
|
+
Ruby's UPnP RubyGem was outdated in Ruby 1.9 when support for Soap4r was
|
12
|
+
dropped. This gem intends to fill that void for Ruby >= 1.9 and allow for
|
13
|
+
SSDP search, discovery, advertisement, the ability to act as a UPnP control
|
14
|
+
point, as well as provide UPnP devices and services.
|
15
|
+
|
16
|
+
This uses [EventMachine](http://github.com/eventmachine/eventmachine), so if
|
17
|
+
you're not already, getting familiar with its concepts will be helpful here.
|
18
|
+
|
19
|
+
### This Fork
|
20
|
+
|
21
|
+
This fork updates the [playful](http://github.com/turboladen/playful) gem,
|
22
|
+
which hasn't seen updates in a while.
|
23
|
+
|
24
|
+
Specifically:
|
25
|
+
|
26
|
+
1. Brings a few dependencies up to date.
|
27
|
+
2. Gets rid of unfinished/broken, overly-ambitious functionality.
|
28
|
+
|
29
|
+
In short, this fork aims to be a simple, minimal way to run SSDP queries and handle the
|
30
|
+
results.
|
31
|
+
|
32
|
+
### Er, what's UPnP??
|
33
|
+
|
34
|
+
"Universal Plug and Play" is a mashup of network protocols that let network
|
35
|
+
devices identify themselves and discover and use each other's services.
|
36
|
+
Common implementations of UPnP devices are things like:
|
37
|
+
|
38
|
+
* [Media Servers and Clients](http://en.wikipedia.org/wiki/List_of_UPnP_AV_media_servers_and_clients) like...
|
39
|
+
* PS3
|
40
|
+
* Slingbox
|
41
|
+
* Xbox
|
42
|
+
* XBMC
|
43
|
+
* Plex
|
44
|
+
* VLC
|
45
|
+
* Twonky
|
46
|
+
* Mediatomb
|
47
|
+
* Home Automation
|
48
|
+
* Philips Hue
|
49
|
+
|
50
|
+
|
51
|
+
If you have a device that implements UPnP, you can most likely control it
|
52
|
+
programmatically with `frisky`. You can't today, but eventually you'll be
|
53
|
+
able to build your own devices & services with `frisky` that can be consumed
|
54
|
+
by other UPnP clients (ex. build a media server with frisky and listen on
|
55
|
+
your PS3...).
|
56
|
+
|
57
|
+
## Features
|
58
|
+
|
59
|
+
### Implemented
|
60
|
+
|
61
|
+
* SSDP search, discovery.
|
62
|
+
|
63
|
+
|
64
|
+
### Coming
|
65
|
+
|
66
|
+
* UPnP Devices & Services (server)
|
67
|
+
|
68
|
+
|
69
|
+
## Examples
|
70
|
+
|
71
|
+
Take a look at the `tasks` directory; I've created some working examples using
|
72
|
+
[Thor](https://github.com/wycats/thor). You can get a list of these tasks by
|
73
|
+
doing `thor -T`.
|
74
|
+
|
75
|
+
There's also a more involved, in-progress, working example at
|
76
|
+
http://github.com/turboladen/upnp_cp_on_sinatra that uses the Rack middleware
|
77
|
+
to build a Sinatra app that allows for controling devices in your network.
|
78
|
+
|
79
|
+
### SSDP Searches
|
80
|
+
|
81
|
+
An SSDP search simply sends the M-SEARCH out to the multicast group and
|
82
|
+
listens for responses for a given (or default of 5 seconds) amount of time.
|
83
|
+
The return from this depends on if you're running it within an EventMachine
|
84
|
+
reactor or not. If not, it returns is an Array of responses as Hashes, where
|
85
|
+
keys are the header names, values are the header values. Take a look at the
|
86
|
+
SSDP.search docs for more on the options here.
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
require 'frisky/ssdp'
|
90
|
+
|
91
|
+
# Search for all devices (do an M-SEARCH with the ST header set to 'ssdp:all')
|
92
|
+
all_devices = Frisky::SSDP.search # this is default
|
93
|
+
all_devices = Frisky::SSDP.search 'ssdp:all' # or be explicit
|
94
|
+
all_devices = Frisky::SSDP.search :all # or use short-hand
|
95
|
+
|
96
|
+
# Search for root devices (do an M-SEARCH with ST header set to 'upnp:rootdevices')
|
97
|
+
root_devices = Frisky::SSDP.search 'upnp:rootdevices'
|
98
|
+
root_devices = Frisky::SSDP.search :root # or use short-hand
|
99
|
+
|
100
|
+
# Search for a device with a specific UUID
|
101
|
+
my_device = Frisky::SSDP.search 'uuid:3c202906-992d-3f0f-b94c-90e1902a136d'
|
102
|
+
|
103
|
+
# Search for devices of a specific type
|
104
|
+
my_media_server = Frisky::SSDP.search 'urn:schemas-upnp-org:device:MediaServer:1'
|
105
|
+
|
106
|
+
# All of these searches will return something that looks like
|
107
|
+
# => [
|
108
|
+
# {
|
109
|
+
# :control => "max-age=1200",
|
110
|
+
# :date => "Sun, 23 Sep 2012 20:31:48 GMT",
|
111
|
+
# :location => "http://192.168.10.3:5001/description/fetch",
|
112
|
+
# :server => "Linux-i386-2.6.38-15-generic-pae, UPnP/1.0, PMS/1.50.0",
|
113
|
+
# :st => "upnp:rootdevice",
|
114
|
+
# :ext => "",
|
115
|
+
# :usn => "uuid:3c202906-992d-3f0f-b94c-90e1902a136d::upnp:rootdevice",
|
116
|
+
# :length => "0"
|
117
|
+
# }
|
118
|
+
# ]
|
119
|
+
```
|
120
|
+
|
121
|
+
If you do the search inside of an EventMachine reactor, as the
|
122
|
+
Frisky::SSDP::Searcher receives and parses responses, it adds them to the
|
123
|
+
accessor #discovery_responses, which is an EventMachine::Channel. This lets
|
124
|
+
you subscribe to the resposnes and do what you want with them as you receive them.
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
require 'frisky/ssdp'
|
128
|
+
|
129
|
+
EM.run do
|
130
|
+
searcher = Frisky::SSDP.search 'uuid:3c202906-992d-3f0f-b94c-90e1902a136d'
|
131
|
+
|
132
|
+
# Create a deferrable object that can be notified when the device we want
|
133
|
+
# has been found and created.
|
134
|
+
device_controller = EventMachine::DefaultDeferrable.new
|
135
|
+
|
136
|
+
# This callback will get called when the device_creator callback is called
|
137
|
+
# (which is called after the device has been created).
|
138
|
+
device_controller.callback do |device|
|
139
|
+
p device.service_list.first.service_type # "urn:schemas-upnp-org:service:ContentDirectory:1"
|
140
|
+
|
141
|
+
# SOAP actions are converted to Ruby methods--show those
|
142
|
+
p device.service_list.first.singleton_methods # [:GetSystemUpdateID, :Search, :GetSearchCapabilities, :GetSortCapabilities, :Browse]
|
143
|
+
|
144
|
+
# Call a SOAP method defined in the service. The response is extracted from the
|
145
|
+
# XML SOAP response and the value is converted from the UPnP dataType to
|
146
|
+
# the related Ruby type. Reponses are always contained in a Hash, so as
|
147
|
+
# to maintain the relation defined in the service.
|
148
|
+
p device.service_list.first.GetSystemUpdateID # { :Id => 1 }
|
149
|
+
end
|
150
|
+
|
151
|
+
# Note that you don't have to check for items in the Channel or for when the
|
152
|
+
# Channel is empty: EventMachine will pop objects off the Channel as soon as
|
153
|
+
# they're put there and stop when there are none left.
|
154
|
+
searcher.discovery_responses.pop do |notification|
|
155
|
+
# Do stuff here.
|
156
|
+
end
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
## Requirements
|
161
|
+
|
162
|
+
* Rubies (tested)
|
163
|
+
* 1.9.3
|
164
|
+
* 2.0.0
|
165
|
+
* 2.1.0
|
166
|
+
* Gems
|
167
|
+
* eventmachine
|
168
|
+
* em-http-request
|
169
|
+
* em-synchrony
|
170
|
+
* nori
|
171
|
+
* log_switch
|
172
|
+
* savon
|
173
|
+
|
174
|
+
|
175
|
+
|
176
|
+
## Install
|
177
|
+
|
178
|
+
$ gem install frisky
|
179
|
+
|
180
|
+
## Copyright
|
181
|
+
|
182
|
+
Copyright (c) 2015 Jon Frisby
|
183
|
+
Copyright (c) 2012-2014 Steve Loveless
|
184
|
+
|
185
|
+
See LICENSE.md for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
require 'cucumber/rake/task'
|
4
|
+
require 'yard'
|
5
|
+
|
6
|
+
|
7
|
+
YARD::Rake::YardocTask.new
|
8
|
+
Cucumber::Rake::Task.new(:features)
|
9
|
+
RSpec::Core::RakeTask.new
|
10
|
+
|
11
|
+
# Alias for rubygems-test
|
12
|
+
desc "Run all test suites."
|
13
|
+
task test: [:spec, :features]
|
14
|
+
|
15
|
+
task default: :test
|
16
|
+
|
@@ -0,0 +1,9 @@
|
|
1
|
+
Feature: Device discovery
|
2
|
+
As a device controller, I want to be able to discover devices
|
3
|
+
so that I can use the services those devices provide
|
4
|
+
|
5
|
+
Scenario: A single root device
|
6
|
+
Given there's at least 1 root device in my network
|
7
|
+
When I come online
|
8
|
+
Then I should discover at least 1 root device
|
9
|
+
And the location of that device should match my fake device's location
|
File without changes
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'log_buddy'
|
3
|
+
require 'cucumber/rspec/doubles'
|
4
|
+
require_relative "../../lib/frisky/logger"
|
5
|
+
|
6
|
+
def local_ip_and_port
|
7
|
+
orig, Socket.do_not_reverse_lookup = Socket.do_not_reverse_lookup, true
|
8
|
+
|
9
|
+
UDPSocket.open do |s|
|
10
|
+
s.connect '64.233.187.99', 1
|
11
|
+
s.addr.last
|
12
|
+
[s.addr.last, s.addr[1]]
|
13
|
+
end
|
14
|
+
ensure
|
15
|
+
Socket.do_not_reverse_lookup = orig
|
16
|
+
end
|
17
|
+
|
18
|
+
ENV['RUBY_UPNP_ENV'] = 'testing'
|
19
|
+
|
20
|
+
Thread.abort_on_exception = true
|
21
|
+
Frisky.logging_enabled = false
|
data/frisky.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path('../lib', __FILE__)
|
3
|
+
require 'frisky/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'frisky'
|
7
|
+
s.version = Frisky::VERSION
|
8
|
+
s.author = 'Jon Frisby'
|
9
|
+
s.email = 'jfrisby@mrjoy.com'
|
10
|
+
s.homepage = 'http://github.com/MrJoy/frisky'
|
11
|
+
s.summary = 'Use me to build a UPnP app!'
|
12
|
+
s.description = %q{frisky provides the tools you need to build an app that runs
|
13
|
+
in a UPnP environment.}
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.extra_rdoc_files = %w(History.md README.md LICENSE.md)
|
19
|
+
s.require_paths = ['lib']
|
20
|
+
s.required_ruby_version = Gem::Requirement.new('>=1.9.1')
|
21
|
+
|
22
|
+
s.add_dependency 'eventmachine', '>=1.0.0'
|
23
|
+
s.add_dependency 'em-http-request', '>=1.0.2'
|
24
|
+
s.add_dependency 'em-synchrony'
|
25
|
+
s.add_dependency 'nori', '>=2.0.2'
|
26
|
+
s.add_dependency 'log_switch', '~>1.0.0'
|
27
|
+
s.add_dependency 'savon', '~>2.0'
|
28
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
# Workaround for missing constants on Windows
|
4
|
+
module Socket::Constants
|
5
|
+
IP_ADD_MEMBERSHIP = 12 unless defined? IP_ADD_MEMBERSHIP
|
6
|
+
IP_MULTICAST_LOOP = 11 unless defined? IP_MULTICAST_LOOP
|
7
|
+
IP_MULTICAST_TTL = 10 unless defined? IP_MULTICAST_TTL
|
8
|
+
IP_TTL = 4 unless defined? IP_TTL
|
9
|
+
end
|
10
|
+
|
11
|
+
class Socket
|
12
|
+
IP_ADD_MEMBERSHIP = 12 unless defined? IP_ADD_MEMBERSHIP
|
13
|
+
IP_MULTICAST_LOOP = 11 unless defined? IP_MULTICAST_LOOP
|
14
|
+
IP_MULTICAST_TTL = 10 unless defined? IP_MULTICAST_TTL
|
15
|
+
IP_TTL = 4 unless defined? IP_TTL
|
16
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
class Hash
|
2
|
+
|
3
|
+
# Converts Hash search targets to SSDP search target String. Conversions are
|
4
|
+
# as follows:
|
5
|
+
# uuid: "someUUID" # => "uuid:someUUID"
|
6
|
+
# device_type: "someDeviceType:1" # => "urn:schemas-upnp-org:device:someDeviceType:1"
|
7
|
+
# service_type: "someServiceType:2" # => "urn:schemas-upnp-org:service:someServiceType:2"
|
8
|
+
#
|
9
|
+
# You can use custom UPnP domain names too:
|
10
|
+
# { device_type: "someDeviceType:3",
|
11
|
+
# domain_name: "mydomain-com" } # => "urn:my-domain:device:someDeviceType:3"
|
12
|
+
# { service_type: "someServiceType:4",
|
13
|
+
# domain_name: "mydomain-com" } # => "urn:my-domain:service:someDeviceType:4"
|
14
|
+
#
|
15
|
+
# @return [String] The converted String, according to the UPnP spec.
|
16
|
+
def to_upnp_s
|
17
|
+
if self.has_key? :uuid
|
18
|
+
return "uuid:#{self[:uuid]}"
|
19
|
+
elsif self.has_key? :device_type
|
20
|
+
if self.has_key? :domain_name
|
21
|
+
return "urn:#{self[:domain_name]}:device:#{self[:device_type]}"
|
22
|
+
else
|
23
|
+
return "urn:schemas-upnp-org:device:#{self[:device_type]}"
|
24
|
+
end
|
25
|
+
elsif self.has_key? :service_type
|
26
|
+
if self.has_key? :domain_name
|
27
|
+
return "urn:#{self[:domain_name]}:service:#{self[:service_type]}"
|
28
|
+
else
|
29
|
+
return "urn:schemas-upnp-org:service:#{self[:service_type]}"
|
30
|
+
end
|
31
|
+
else
|
32
|
+
self.to_s
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
class Symbol
|
39
|
+
|
40
|
+
# Converts Symbol search targets to SSDP search target String. Conversions are
|
41
|
+
# as follows:
|
42
|
+
# :all # => "ssdp:all"
|
43
|
+
# :root # => "upnp:rootdevice"
|
44
|
+
# "root" # => "upnp:rootdevice"
|
45
|
+
#
|
46
|
+
# @return [String] The converted String, according to the UPnP spec.
|
47
|
+
def to_upnp_s
|
48
|
+
if self == :all
|
49
|
+
'ssdp:all'
|
50
|
+
elsif self == :root
|
51
|
+
'upnp:rootdevice'
|
52
|
+
else
|
53
|
+
self
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
class String
|
60
|
+
# This doesn't do anything to the string; just allows users to call the
|
61
|
+
# method without having to check type first.
|
62
|
+
def to_upnp_s
|
63
|
+
self
|
64
|
+
end
|
65
|
+
end
|