frisky 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/.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
|