wemote 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b18903132febc3de01e2796ce0b7adde752b030f
4
- data.tar.gz: 07503eba981f501449f2b6f86afb90baca0ad7c7
3
+ metadata.gz: 88d7829f2bfa8a7892103c87c14696a2efcafcf0
4
+ data.tar.gz: 8b7e761fbfde70ab48e0ca55cdd50c388c51a9c6
5
5
  SHA512:
6
- metadata.gz: 3313aa5e43f3394d505f9a2b6c7dbfe06dc6f49effaaea8b86e43542e93f5cb4583793631b34cd6fac99be1aa3440e4ca3b9b40320a8d4ba36fea52418e1662a
7
- data.tar.gz: ad62e4b4497c8509246861e52479b677ce495d1859103fbd67823552af6b92f346ad7972cd1dbc8f54e1b7129aa52429172691be75eb2331e639bd09e879b237
6
+ metadata.gz: f7c758531b285a15f3fa50b0eadf8aae1c56ae746b73909789efa795d08414ca1b5f47f8af8fff56aefb48ac9ccd99c0a8d1844dedc4eb1b53a3e9569f84af12
7
+ data.tar.gz: a9ca631f0509ef3a4d8cab76a1e229b1507a003ccfb62f0e8261ea6669be7ff4799e89944732bc1698394a19b7e6f0e9c1d3f70ac8c9dbb04eb95505472acb06
data/Gemfile CHANGED
@@ -4,7 +4,6 @@ gemspec
4
4
  # Commented out until I can determine a way to set engine-specific dependencies
5
5
  # gem 'manticore', git: 'git://github.com/cheald/manticore.git'
6
6
  #gem 'typhoeus'
7
- gem 'coveralls', :require => false
8
7
  gem 'httparty'
9
8
  gem 'rspec'
10
9
  gem 'rake'
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Wemote [![Build Status](https://travis-ci.org/gisikw/wemote.png)](https://travis-ci.org/gisikw/wemote) [![Gem Version](https://badge.fury.io/rb/wemote.png)](http://badge.fury.io/rb/wemote) [![Coverage Status](https://coveralls.io/repos/gisikw/wemote/badge.png)](https://coveralls.io/r/gisikw/wemote) [![Code Climate](https://codeclimate.com/github/gisikw/wemote.png)](https://codeclimate.com/github/gisikw/wemote)
1
+ # Wemote [![Build Status](https://travis-ci.org/gisikw/wemote.png)](https://travis-ci.org/gisikw/wemote) [![Gem Version](https://badge.fury.io/rb/wemote.png)](http://badge.fury.io/rb/wemote) [![Code Climate](https://codeclimate.com/github/gisikw/wemote.png)](https://codeclimate.com/github/gisikw/wemote)
2
2
 
3
3
  Wemote is an interface for controlling WeMo light switches (and possibly outlets in the future). Unlike other implementations, it does not rely upon `playful` for upnp discovery, which makes it compatible with alternative Ruby engines, such as JRuby.
4
4
 
@@ -21,18 +21,17 @@ Or install it yourself as:
21
21
  You can fetch all lightswitches on your network (providing you've set them up with your smartphone), via:
22
22
 
23
23
  ```ruby
24
- switches = Wemote::Switch.all(force_reload=false) #=> [#<Remo::Switch:0x27f33aef @host="192.168.1.11", @name="Kitchen Switch", @port="49154">, #<Remo::Switch:0x51a23566 @host="192.168.1.12", @name="Main Room", @port="49154">, #<Remo::Switch:0x705fe568 @host="192.168.1.10", @name="Bathroom Switch", @port="49153">]
24
+ switches = Wemote::Switch.all #=> [#<Wemote::Switch:0x27f33aef @host="192.168.1.11", @name="Kitchen Switch", @port="49154">
25
25
  ```
26
26
 
27
27
  Or select a switch by its friendly name:
28
28
 
29
29
  ```ruby
30
- switch = Wemote::Switch.find('Kitchen Switch') #=> #<Remo::Switch:0x27f33aef @host="192.168.1.11", @name="Kitchen Switch", @port="49154">
30
+ switch = Wemote::Switch.find('Kitchen Switch') #=> #<Wemote::Switch:0x27f33aef @host="192.168.1.11", @name="Kitchen Switch", @port="49154">
31
31
  ```
32
32
 
33
33
  Given a Switch instance, you can call the following methods:
34
34
  ```ruby
35
- switch.get_state #=> [:off,:on]
36
35
  switch.off? #=> [true,false]
37
36
  switch.on? #=> [true,false]
38
37
  switch.on!
@@ -42,7 +41,7 @@ switch.toggle!
42
41
 
43
42
  ## Performance
44
43
 
45
- Wemote is designed to be performant - and as such, it will leverage the best HTTP library available for making requests. Currently, Wemote will use (in order of preference): `manticore`, `typhoeus`, `httparty`, and finally (miserably) `net/http`. Because you probably like things fast too, we recommend you `gem install manticore` on JRuby, or `gem install typhoeus` on another engine. In order to keep the gem as flexible as possible, none of these are direct dependencies. They just make Wemote happy and fast.
44
+ Wemote is designed to be performant - and as such, it will leverage the best HTTP library available for making requests. Currently, Wemote will use (in order of preference): `manticore`, `typhoeus`, and finally (miserably) `net/http`. Because you probably like things fast too, we recommend you `gem install manticore` on JRuby, or `gem install typhoeus` on another engine. In order to keep the gem as flexible as possible, none of these are direct dependencies. They just make Wemote happy and fast.
46
45
 
47
46
  ## Contributing
48
47
 
@@ -1,7 +1,16 @@
1
1
  require_relative './wemote/version'
2
2
 
3
3
  module Wemote
4
+ require_relative './wemote/collection/switch'
4
5
  require_relative './wemote/switch'
5
6
  require_relative './wemote/client'
6
7
  require_relative './wemote/xml'
8
+
9
+ class << self
10
+
11
+ def discover(broadcast='255.255.255.255')
12
+ Wemote::Switch.send(:discover,broadcast)
13
+ end
14
+
15
+ end
7
16
  end
@@ -6,6 +6,7 @@ module Wemote
6
6
  constants.collect {|const_name| const_get(const_name)}.select {|const| const.class == Module}.detect do |mod|
7
7
  fulfilled = false
8
8
  begin
9
+ next unless mod.const_defined?(:DEPENDENCIES)
9
10
  mod.const_get(:DEPENDENCIES).map{|d|require d}
10
11
  fulfilled = true
11
12
  rescue LoadError
@@ -15,63 +16,59 @@ module Wemote
15
16
  end
16
17
  end
17
18
 
18
- module Manticore
19
- DEPENDENCIES = ['manticore']
20
-
21
- def get(*args)
22
- _get(::Manticore,*args).call
19
+ module SmartLib
20
+ %w{get post put}.each do |name|
21
+ define_method name do |*args|
22
+ _req(@lib,name,*args).tap{|r|r.call if @call}
23
+ end
23
24
  end
25
+ end
24
26
 
25
- def post(*args)
26
- _post(::Manticore,*args).call
27
+ module Manticore
28
+ DEPENDENCIES = ['manticore']
29
+ def self.extended(base)
30
+ base.instance_variable_set(:@lib,::Manticore)
31
+ base.instance_variable_set(:@call,true)
32
+ base.extend(SmartLib)
27
33
  end
28
-
29
34
  end
30
35
 
31
36
  module Typhoeus
32
37
  DEPENDENCIES = ['typhoeus']
33
-
34
- def get(*args)
35
- _get(::Typhoeus,*args)
36
- end
37
-
38
- def post(*args)
39
- _post(::Typhoeus,*args)
38
+ def self.extended(base)
39
+ base.instance_variable_set(:@lib,::Typhoeus)
40
+ base.extend(SmartLib)
40
41
  end
41
-
42
42
  end
43
43
 
44
- module HTTParty
45
- DEPENDENCIES = ['httparty']
46
-
47
- def get(*args)
48
- _get(::HTTParty,*args)
49
- end
44
+ # HTTParty is temporarily disabled, as it's auto-parsing the XML
50
45
 
51
- def post(*args)
52
- _post(::HTTParty,*args)
53
- end
54
- end
46
+ #module HTTParty
47
+ # DEPENDENCIES = ['httparty']
48
+ # def self.extended(base)
49
+ # base.instance_variable_set(:@lib,::HTTParty)
50
+ # base.extend(SmartLib)
51
+ # end
52
+ #end
55
53
 
56
54
  module NetHTTP
57
55
  DEPENDENCIES = ['net/http','uri']
58
56
 
59
- def get(url,body=nil,headers=nil)
57
+ def request(klass,url,body=nil,headers=nil)
60
58
  uri = URI.parse(url)
61
59
  http = Net::HTTP.new(uri.host, uri.port)
62
- request = Net::HTTP::Get.new(uri.request_uri)
60
+ request = klass.new(uri.request_uri)
63
61
  headers.map{|k,v|request[k]=v} if headers
62
+ (request.body = body) if body
64
63
  response = http.request(request)
65
64
  end
66
65
 
67
- def post(url,body=nil,headers=nil)
68
- uri = URI.parse(url)
69
- http = Net::HTTP.new(uri.host, uri.port)
70
- request = Net::HTTP::Post.new(uri.request_uri)
71
- headers.map{|k,v|request[k]=v} if headers
72
- (request.body = body) if body
73
- response = http.request(request)
66
+ %w{get post put}.each do |name|
67
+ define_method name do |*args|
68
+ request(Net::HTTP.const_get(name.capitalize),*args)
69
+ end
74
70
  end
71
+
75
72
  end
76
73
 
77
74
  def initialize
@@ -80,12 +77,8 @@ module Wemote
80
77
 
81
78
  private
82
79
 
83
- def _get(lib,url,body=nil,headers=nil)
84
- lib.get(url,{body:body,headers:headers})
85
- end
86
-
87
- def _post(lib,url,body=nil,headers=nil)
88
- lib.post(url,{body:body,headers:headers})
80
+ def _req(lib,method,url,body=nil,headers=nil)
81
+ lib.send(method,url,{body:body,headers:headers})
89
82
  end
90
83
 
91
84
  end
@@ -0,0 +1,31 @@
1
+ module Wemote
2
+ module Collection
3
+
4
+ # This class provides an extension on the basic Array object, in order to
5
+ # facilitate group operations on a collection of Wemote::Switch instances.
6
+ class Switch < Array
7
+
8
+ class << self
9
+
10
+ private
11
+
12
+ # @macro [attach] container.increment
13
+ # @method $1()
14
+ # Calls {Wemote::Switch#$1} on all containing elements and return the results in an
15
+ # array
16
+ # @return [Array]
17
+ def make(name)
18
+ define_method(name){map{|s|s.send(name)}}
19
+ end
20
+
21
+ end
22
+
23
+ make :toggle!
24
+ make :off!
25
+ make :off?
26
+ make :on!
27
+ make :on?
28
+
29
+ end
30
+ end
31
+ end
@@ -1,19 +1,14 @@
1
1
  require 'socket'
2
2
  require 'ipaddr'
3
+ require 'timeout'
3
4
 
4
5
  module Wemote
6
+
7
+ # This class encapsulates an individual Wemo Switch. It provides methods for
8
+ # getting and setting the switch's state, as well as a {#toggle!} method for
9
+ # convenience. Finally, it provides the {#poll} method, which accepts a block
10
+ # to be executed any time the switch changes state.
5
11
  class Switch
6
- MULTICAST_ADDR = '239.255.255.250'
7
- BIND_ADDR = '0.0.0.0'
8
- PORT = 1900
9
- DISCOVERY= <<-EOF
10
- M-SEARCH * HTTP/1.1\r
11
- HOST: 239.255.255.250:1900\r
12
- MAN: "ssdp:discover"\r
13
- MX: 10\r
14
- ST: urn:Belkin:device:lightswitch:1\r
15
- \r
16
- EOF
17
12
 
18
13
  GET_HEADERS = {
19
14
  "SOAPACTION" => '"urn:Belkin:service:basicevent:1#GetBinaryState"',
@@ -26,71 +21,95 @@ EOF
26
21
  }
27
22
 
28
23
  class << self
24
+ # Returns all Switches detected on the local network
25
+ #
26
+ # @param [Boolean] refresh Refresh and redetect Switches
27
+ # @return [Array] all Switches on the network
29
28
  def all(refresh=false)
30
29
  @switches = nil if refresh
31
- @switches ||= fetch_switches
30
+ @switches ||= Wemote::Collection::Switch.new(discover)
32
31
  end
33
32
 
33
+ # Returns a Switch of a given name
34
+ #
35
+ # @param name [String] the friendly name of the Switch
36
+ # @return [Wemote::Switch] a Switch object
34
37
  def find(name)
35
38
  all.detect{|s|s.name == name}
36
39
  end
37
40
 
38
41
  private
39
42
 
40
- def ssdp_socket
41
- socket = UDPSocket.new
42
- socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, IPAddr.new(MULTICAST_ADDR).hton + IPAddr.new(BIND_ADDR).hton)
43
- socket.setsockopt(Socket::IPPROTO_IP, Socket::IP_MULTICAST_TTL, 1)
44
- socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, 1)
45
- socket.bind(BIND_ADDR,PORT)
46
- socket
47
- end
48
-
49
- def fetch_switches
50
- socket = ssdp_socket
51
- switches = []
52
-
53
- 3.times { socket.send(DISCOVERY, 0, MULTICAST_ADDR, PORT) }
54
-
55
- # The following is a bit silly, but is necessary for JRuby support,
56
- # which seems to have some issues with socket interruption.
57
- # If you have a working JRuby solution that doesn't require this
58
- # kind of hackery, by all means, submit a pull request!
59
-
60
- sleep 1
61
-
62
- parser = Thread.start do
63
- loop do
64
- message, _ = socket.recvfrom(1024)
65
- if message.match(/LOCATION.*Belkin/m)
66
- switches << message.match(/LOCATION:\s+http:\/\/([^\/]+)/)[1].split(':')
67
- end
68
- end
69
- end
70
-
71
- sleep 1
72
- parser.kill
73
-
74
- socket.close
75
-
76
- return switches.uniq.map{|s|self.new(*s)}
77
-
43
+ def discover(broadcast = '255.255.255.255')
44
+ `ping -t 1 #{broadcast} > /dev/null && arp -na | grep b4:75`.split("\n").map do |device|
45
+ self.new(/\((\d+\.\d+\.\d+\.\d+)\)/.match(device)[1])
46
+ end.reject{|device| device.instance_variable_get(:@port).nil? }
78
47
  end
79
48
  end
80
49
 
81
50
  attr_accessor :name
82
51
 
83
- def initialize(host,port)
52
+ def initialize(host,port=nil)
84
53
  @host, @port = host, port
85
54
  set_meta
86
55
  end
87
56
 
57
+ # Turn the Switch on or off, based on its current state
88
58
  def toggle!; on? ? off! : on!; end
59
+
60
+ # Turn the Switch off
89
61
  def off!; set_state(0); end
90
- def off?; get_state == :off; end
62
+
63
+ # Turn the Switch on
91
64
  def on!; set_state(1); end
65
+
66
+ # Return whether the Switch is off
67
+ #
68
+ # @return [Boolean]
69
+ def off?; get_state == :off; end
70
+
71
+ # Return whether the Switch is on
72
+ #
73
+ # @return [Boolean]
92
74
  def on?; get_state == :on; end
93
75
 
76
+ # Monitors the state of the Switch via polling, and yields to the block
77
+ # given with the updated state.
78
+ #
79
+ # @example Output when a Switch changes state
80
+ # light.poll do |state|
81
+ # if state == :on
82
+ # puts "The switch turned on"
83
+ # else
84
+ # puts "The switch turned off"
85
+ # end
86
+ # end
87
+ #
88
+ # @param rate [Float] The rate in seconds at which to poll the switch
89
+ # @param async [Boolean] Whether or not to poll the switch in a separate thread
90
+ #
91
+ # @return [Thread] if the method call was asynchronous
92
+ def poll(rate=0.25,async=true,&block)
93
+ old_state = get_state
94
+ poller = Thread.start do
95
+ loop do
96
+ begin
97
+ state = get_state
98
+ if state != old_state
99
+ old_state = state
100
+ yield state
101
+ end
102
+ rescue Exception
103
+ end
104
+ sleep rate
105
+ end
106
+ end
107
+ puts "Monitoring #{@name} for changes"
108
+ async ? poller : poller.join
109
+ end
110
+
111
+ private
112
+
94
113
  def get_state
95
114
  response = begin
96
115
  client.post("http://#{@host}:#{@port}/upnp/control/basicevent1",Wemote::XML.get_binary_state,GET_HEADERS)
@@ -108,15 +127,26 @@ EOF
108
127
  end
109
128
  end
110
129
 
111
- private
112
-
113
130
  def client
114
131
  @client ||= Wemote::Client.new
115
132
  end
116
133
 
117
134
  def set_meta
118
- response = client.get("http://#{@host}:#{@port}/setup.xml")
119
- @name = response.body.match(/<friendlyName>([^<]+)<\/friendlyName>/)[1]
135
+ if @port
136
+ response = client.get("http://#{@host}:#{@port}/setup.xml")
137
+ @name = response.body.match(/<friendlyName>([^<]+)<\/friendlyName>/)[1]
138
+ else
139
+ for port in 49152..49156
140
+ begin
141
+ response = nil
142
+ Timeout::timeout(1){ response = client.get("http://#{@host}:#{port}/setup.xml") }
143
+ @name = response.body.match(/<friendlyName>([^<]+)<\/friendlyName>/)[1]
144
+ @port = port
145
+ break
146
+ rescue Exception
147
+ end
148
+ end
149
+ end
120
150
  end
121
151
 
122
152
  end
@@ -1,3 +1,3 @@
1
1
  module Wemote
2
- VERSION = "0.2.0"
2
+ VERSION = "0.2.1"
3
3
  end
@@ -1,22 +1,23 @@
1
1
  module Wemote
2
2
  module XML
3
3
  class << self
4
- TEMPLATES = {}
4
+ xml_path = File.join(File.dirname(__FILE__),'/../../xml')
5
+ TEMPLATES = {
6
+ get_binary_state: File.read(File.join(xml_path,'get_binary_state.xml')),
7
+ set_binary_state: File.read(File.join(xml_path,'set_binary_state.xml'))
8
+ }
5
9
 
6
- Dir.glob(File.join(File.dirname(__FILE__),'/../../xml/*.xml')).each do |file|
7
- basename = File.basename(file,'.xml')
8
- TEMPLATES[basename] = File.read(file)
9
-
10
- define_method basename do |*args|
11
- t = TEMPLATES[basename].dup
12
- if (replace = t.scan(/{{\d+}}/).size) != args.size
13
- raise ArgumentError, "wrong number of arguments calling `#{basename}` (#{args.size} for #{replace})"
14
- end
15
- (1..replace).map{|i|t.gsub!("{{#{i}}}",args[i-1].to_s)}
16
- t
17
- end
10
+ # @return [String] The required XML body for a Wemo binary state request
11
+ def get_binary_state
12
+ TEMPLATES[:get_binary_state]
13
+ end
18
14
 
15
+ # @param [Integer] state Either 1 or 0, for off and on respectively
16
+ # @return [String] The required XML body for a Wemo binary state set request
17
+ def set_binary_state(state)
18
+ TEMPLATES[:set_binary_state].gsub("{{1}}",state.to_s)
19
19
  end
20
+
20
21
  end
21
22
  end
22
23
  end
@@ -1,4 +1 @@
1
- require 'coveralls'
2
- Coveralls.wear!
3
-
4
1
  require 'wemote'
@@ -72,22 +72,5 @@ describe Wemote::Switch do
72
72
  end
73
73
  end
74
74
 
75
- describe 'get_state' do
76
- it 'should return the binary state of the switch' do
77
-
78
- @switch.client.stub(:post).and_return(double('response',body:'<BinaryState>1</BinaryState>'))
79
- @switch.get_state.should == :on
80
- @switch.client.stub(:post).and_return(double('response',body:'<BinaryState>0</BinaryState>'))
81
- @switch.get_state.should == :off
82
- end
83
- end
84
-
85
- describe 'set_state' do
86
- it 'should set the binary state of the switch' do
87
- @switch.client.stub(:post).and_return(double('response',body:'Called'))
88
- @switch.set_state(1).body.should == 'Called'
89
- end
90
- end
91
-
92
75
  end
93
76
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wemote
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Gisi
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-03-06 00:00:00.000000000 Z
11
+ date: 2014-08-08 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Wemote is a Ruby-agnostic gem for Wemo light switches
14
14
  email:
@@ -17,15 +17,15 @@ executables: []
17
17
  extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
- - .coveralls.yml
21
- - .gitignore
22
- - .travis.yml
20
+ - ".gitignore"
21
+ - ".travis.yml"
23
22
  - Gemfile
24
23
  - LICENSE.txt
25
24
  - README.md
26
25
  - Rakefile
27
26
  - lib/wemote.rb
28
27
  - lib/wemote/client.rb
28
+ - lib/wemote/collection/switch.rb
29
29
  - lib/wemote/switch.rb
30
30
  - lib/wemote/version.rb
31
31
  - lib/wemote/xml.rb
@@ -38,26 +38,27 @@ homepage: https://github.com/gisikw/wemote
38
38
  licenses:
39
39
  - MIT
40
40
  metadata: {}
41
- post_install_message:
41
+ post_install_message:
42
42
  rdoc_options: []
43
43
  require_paths:
44
44
  - lib
45
45
  required_ruby_version: !ruby/object:Gem::Requirement
46
46
  requirements:
47
- - - '>='
47
+ - - ">="
48
48
  - !ruby/object:Gem::Version
49
49
  version: '0'
50
50
  required_rubygems_version: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - '>='
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  requirements: []
56
- rubyforge_project:
56
+ rubyforge_project:
57
57
  rubygems_version: 2.2.2
58
- signing_key:
58
+ signing_key:
59
59
  specification_version: 4
60
60
  summary: Wemote is a Ruby-agnostic gem for Wemo light switches
61
61
  test_files:
62
62
  - spec/spec_helper.rb
63
63
  - spec/wemote/switch_spec.rb
64
+ has_rdoc:
@@ -1 +0,0 @@
1
- service_name: travis-ci